@doxbrix/doxloop 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1153 -0
- package/README.md +422 -226
- package/assets/doxbrix-preview.css +77 -15
- package/contracts/agent-events-v1.schema.json +1 -0
- package/contracts/coverage-v1.schema.json +59 -0
- package/contracts/drift-v1.schema.json +1 -0
- package/contracts/evaluation-v1.schema.json +62 -0
- package/contracts/quality-codes-v1.json +1 -0
- package/contracts/quality-report-v1.schema.json +1 -0
- package/contracts/validation-v1.schema.json +1 -0
- package/dist/agent-failure.d.ts +36 -0
- package/dist/agent-failure.js +98 -0
- package/dist/agent-isolation.d.ts +17 -0
- package/dist/agent-isolation.js +94 -0
- package/dist/agent-log.d.ts +197 -0
- package/dist/agent-log.js +1094 -0
- package/dist/agent-process.d.ts +63 -0
- package/dist/agent-process.js +128 -0
- package/dist/agent-reply.d.ts +47 -0
- package/dist/agent-reply.js +315 -0
- package/dist/agents.d.ts +40 -4
- package/dist/agents.js +115 -11
- package/dist/api-coverage.d.ts +11 -0
- package/dist/api-coverage.js +72 -0
- package/dist/application-probe.d.ts +28 -0
- package/dist/application-probe.js +93 -0
- package/dist/application-url.d.ts +14 -0
- package/dist/application-url.js +26 -0
- package/dist/args.js +15 -0
- package/dist/artifact-deploy.d.ts +9 -0
- package/dist/artifact-deploy.js +28 -8
- package/dist/assets.d.ts +72 -0
- package/dist/assets.js +382 -0
- package/dist/auth.d.ts +10 -0
- package/dist/auth.js +38 -9
- package/dist/author.d.ts +206 -4
- package/dist/author.js +1650 -52
- package/dist/authoring-batches.d.ts +190 -0
- package/dist/authoring-batches.js +489 -0
- package/dist/authoring-postpass.d.ts +79 -0
- package/dist/authoring-postpass.js +1289 -0
- package/dist/authoring-progress.d.ts +95 -0
- package/dist/authoring-progress.js +269 -0
- package/dist/autosync.d.ts +56 -0
- package/dist/autosync.js +442 -0
- package/dist/batch-artifacts.d.ts +81 -0
- package/dist/batch-artifacts.js +412 -0
- package/dist/batch-limits.d.ts +37 -0
- package/dist/batch-limits.js +80 -0
- package/dist/branding.d.ts +41 -0
- package/dist/branding.js +229 -0
- package/dist/bundle-upload.d.ts +26 -0
- package/dist/bundle-upload.js +99 -0
- package/dist/capture-auth.d.ts +95 -0
- package/dist/capture-auth.js +204 -0
- package/dist/capture.d.ts +48 -0
- package/dist/capture.js +20 -1
- package/dist/cli.js +839 -32
- package/dist/content-links.d.ts +10 -0
- package/dist/content-links.js +49 -0
- package/dist/contract-validation.d.ts +4 -0
- package/dist/contract-validation.js +25 -0
- package/dist/coverage-actions.d.ts +8 -0
- package/dist/coverage-actions.js +71 -0
- package/dist/coverage-resolutions.d.ts +16 -0
- package/dist/coverage-resolutions.js +58 -0
- package/dist/db.d.ts +37 -0
- package/dist/db.js +288 -0
- package/dist/demo.d.ts +12 -0
- package/dist/demo.js +122 -0
- package/dist/deploy-credentials.d.ts +7 -0
- package/dist/deploy-credentials.js +76 -0
- package/dist/deploy-targets/github-pages.d.ts +3 -0
- package/dist/deploy-targets/github-pages.js +100 -0
- package/dist/deploy-targets/index.d.ts +8 -0
- package/dist/deploy-targets/index.js +78 -0
- package/dist/deploy-targets/netlify.d.ts +3 -0
- package/dist/deploy-targets/netlify.js +49 -0
- package/dist/deploy-targets/types.d.ts +32 -0
- package/dist/deploy-targets/types.js +2 -0
- package/dist/deploy-targets/vercel.d.ts +3 -0
- package/dist/deploy-targets/vercel.js +95 -0
- package/dist/deploy.d.ts +9 -0
- package/dist/deploy.js +87 -13
- package/dist/deterministic-capture.d.ts +90 -0
- package/dist/deterministic-capture.js +435 -0
- package/dist/direct-edit.d.ts +40 -0
- package/dist/direct-edit.js +159 -0
- package/dist/docs-crawl.d.ts +86 -0
- package/dist/docs-crawl.js +536 -0
- package/dist/docs-site.d.ts +49 -0
- package/dist/docs-site.js +233 -0
- package/dist/doctor.js +8 -0
- package/dist/documentation-collections.d.ts +23 -0
- package/dist/documentation-collections.js +191 -0
- package/dist/documentation-plan.d.ts +253 -0
- package/dist/documentation-plan.js +2563 -0
- package/dist/doxbrix-build.d.ts +19 -0
- package/dist/doxbrix-build.js +167 -0
- package/dist/doxbrix-markdown.d.ts +9 -0
- package/dist/doxbrix-markdown.js +115 -14
- package/dist/drift.d.ts +10 -0
- package/dist/drift.js +164 -0
- package/dist/evaluation.d.ts +46 -0
- package/dist/evaluation.js +113 -0
- package/dist/evidence-pack.d.ts +47 -0
- package/dist/evidence-pack.js +358 -0
- package/dist/evidence.d.ts +25 -0
- package/dist/evidence.js +175 -0
- package/dist/fs.d.ts +8 -2
- package/dist/fs.js +41 -11
- package/dist/generator-api.d.ts +89 -0
- package/dist/generator-preflight.d.ts +27 -0
- package/dist/generator-preflight.js +105 -0
- package/dist/generator-runtime.d.ts +7 -0
- package/dist/generator-runtime.js +17 -1
- package/dist/generators.d.ts +24 -2
- package/dist/generators.js +54 -1
- package/dist/git-delivery.d.ts +17 -0
- package/dist/git-delivery.js +123 -0
- package/dist/globs.d.ts +16 -0
- package/dist/globs.js +65 -0
- package/dist/glossary.d.ts +26 -0
- package/dist/glossary.js +179 -0
- package/dist/history.d.ts +106 -0
- package/dist/history.js +600 -0
- package/dist/html-markdown.d.ts +46 -0
- package/dist/html-markdown.js +423 -0
- package/dist/interactive.js +16 -15
- package/dist/job-events.d.ts +74 -0
- package/dist/job-events.js +377 -0
- package/dist/keep-awake.d.ts +50 -0
- package/dist/keep-awake.js +123 -0
- package/dist/local-source-snapshot.d.ts +20 -0
- package/dist/local-source-snapshot.js +61 -0
- package/dist/mintlify-detect.d.ts +3 -0
- package/dist/mintlify-detect.js +18 -0
- package/dist/mintlify-import.d.ts +75 -0
- package/dist/mintlify-import.js +190 -0
- package/dist/navigation.d.ts +98 -0
- package/dist/navigation.js +310 -0
- package/dist/openapi.d.ts +60 -0
- package/dist/openapi.js +439 -0
- package/dist/page-editor-bridge.d.ts +3 -0
- package/dist/page-editor-bridge.js +109 -0
- package/dist/page-editor-preview.d.ts +10 -0
- package/dist/page-editor-preview.js +55 -0
- package/dist/page-extension.d.ts +9 -0
- package/dist/page-extension.js +15 -0
- package/dist/page-metadata.d.ts +28 -0
- package/dist/page-metadata.js +166 -0
- package/dist/page-operations.d.ts +34 -0
- package/dist/page-operations.js +215 -0
- package/dist/page-routes.d.ts +4 -0
- package/dist/page-routes.js +61 -0
- package/dist/pages.d.ts +20 -0
- package/dist/pages.js +184 -0
- package/dist/plan-generator.d.ts +3 -0
- package/dist/plan-generator.js +21 -0
- package/dist/plan-navigation.d.ts +11 -0
- package/dist/plan-navigation.js +30 -0
- package/dist/planning-captures.d.ts +20 -0
- package/dist/planning-captures.js +143 -0
- package/dist/planning-research.d.ts +135 -0
- package/dist/planning-research.js +472 -0
- package/dist/planning-triage.d.ts +23 -0
- package/dist/planning-triage.js +131 -0
- package/dist/preview.d.ts +24 -0
- package/dist/preview.js +280 -29
- package/dist/project-detect.d.ts +36 -0
- package/dist/project-detect.js +251 -0
- package/dist/project-import.d.ts +54 -0
- package/dist/project-import.js +157 -0
- package/dist/project-lock.d.ts +6 -0
- package/dist/project-lock.js +96 -0
- package/dist/project-registry.d.ts +25 -0
- package/dist/project-registry.js +79 -0
- package/dist/project.d.ts +35 -3
- package/dist/project.js +388 -38
- package/dist/prompts.d.ts +9 -0
- package/dist/prompts.js +33 -4
- package/dist/proposal-replay.d.ts +32 -0
- package/dist/proposal-replay.js +99 -0
- package/dist/quality-claims.d.ts +8 -0
- package/dist/quality-claims.js +168 -0
- package/dist/quality-config.d.ts +5 -0
- package/dist/quality-config.js +84 -0
- package/dist/quality-contract.d.ts +37 -0
- package/dist/quality-contract.js +46 -0
- package/dist/quality-examples.d.ts +4 -0
- package/dist/quality-examples.js +233 -0
- package/dist/quality-gates.d.ts +16 -0
- package/dist/quality-gates.js +192 -0
- package/dist/quality-links.d.ts +7 -0
- package/dist/quality-links.js +149 -0
- package/dist/quality-lint.d.ts +6 -0
- package/dist/quality-lint.js +124 -0
- package/dist/quality-rendered.d.ts +11 -0
- package/dist/quality-rendered.js +222 -0
- package/dist/quality-schema.d.ts +3 -0
- package/dist/quality-schema.js +71 -0
- package/dist/release-notes.d.ts +44 -0
- package/dist/release-notes.js +183 -0
- package/dist/remote-monitor.d.ts +16 -0
- package/dist/remote-monitor.js +74 -0
- package/dist/remote-source.d.ts +34 -0
- package/dist/remote-source.js +426 -0
- package/dist/review-diff.d.ts +82 -0
- package/dist/review-diff.js +400 -0
- package/dist/review-learning.d.ts +11 -0
- package/dist/review-learning.js +60 -0
- package/dist/review-render.d.ts +38 -0
- package/dist/review-render.js +224 -0
- package/dist/review-report.d.ts +9 -0
- package/dist/review-report.js +89 -0
- package/dist/review-ui.d.ts +14 -0
- package/dist/review-ui.js +1248 -0
- package/dist/schedule.d.ts +78 -0
- package/dist/schedule.js +480 -0
- package/dist/screen-capture-provider.d.ts +52 -0
- package/dist/screen-capture-provider.js +218 -0
- package/dist/screenshot-workflow.d.ts +167 -0
- package/dist/screenshot-workflow.js +1237 -0
- package/dist/settings.d.ts +1 -1
- package/dist/settings.js +95 -7
- package/dist/site-export.d.ts +18 -0
- package/dist/site-export.js +87 -0
- package/dist/source-connectors.d.ts +33 -0
- package/dist/source-connectors.js +268 -0
- package/dist/source-discovery.d.ts +132 -0
- package/dist/source-discovery.js +823 -0
- package/dist/source-intelligence.d.ts +9 -0
- package/dist/source-intelligence.js +306 -0
- package/dist/sync-review.d.ts +28 -0
- package/dist/sync-review.js +264 -0
- package/dist/sync-runs.d.ts +192 -0
- package/dist/sync-runs.js +2244 -0
- package/dist/sync.d.ts +35 -0
- package/dist/sync.js +298 -32
- package/dist/text-diff.d.ts +9 -0
- package/dist/text-diff.js +59 -0
- package/dist/types.d.ts +946 -1
- package/dist/ui/assets/doxloop-logo-light-De7Nx7j7.png +0 -0
- package/dist/ui/assets/index-BHBYU2aG.css +1 -0
- package/dist/ui/assets/index-Cq3RPQiC.js +33 -0
- package/dist/ui/index.html +18 -0
- package/dist/ui-server.d.ts +83 -0
- package/dist/ui-server.js +3532 -0
- package/dist/usage-budget.d.ts +28 -0
- package/dist/usage-budget.js +90 -0
- package/dist/validation.d.ts +25 -1
- package/dist/validation.js +312 -26
- package/dist/workspace-tools.d.ts +54 -0
- package/dist/workspace-tools.js +123 -0
- package/docs/agent-compatibility.md +50 -28
- package/docs/ci-and-automation.md +105 -66
- package/docs/doxbrix-http-api.md +8 -1
- package/docs/existing-documentation.md +80 -0
- package/docs/generation-performance.md +108 -0
- package/docs/generator-authoring.md +68 -5
- package/docs/generator-selection.md +50 -13
- package/docs/mintlify-import.md +71 -0
- package/docs/openapi-security.md +25 -0
- package/docs/project-format.md +331 -36
- package/docs/release-quality.md +158 -0
- package/docs/releasing.md +72 -0
- package/docs/review-workflows.md +51 -0
- package/docs/security-model.md +102 -39
- package/docs/troubleshooting.md +226 -93
- package/package.json +46 -15
- package/scripts/test-auto-screenshot.mjs +172 -0
- package/skills/doxloop-authoring/SKILL.md +242 -361
- package/skills/doxloop-authoring/references/existing-documentation.md +94 -0
- package/skills/doxloop-authoring/references/navigation-architecture.md +18 -7
- package/skills/doxloop-authoring/references/page-depth.md +169 -0
- package/skills/doxloop-authoring/references/project-format.md +106 -7
- package/skills/doxloop-authoring/references/quality.md +10 -0
- package/skills/doxloop-authoring/references/screenshot-manifest.md +113 -0
- package/skills/doxloop-authoring/references/screenshots.md +149 -235
- package/skills/doxloop-authoring/references/workflows.md +76 -0
- package/skills/doxloop-doxbrix/SKILL.md +38 -22
- package/skills/doxloop-doxbrix/references/api-endpoints.md +15 -14
- package/skills/doxloop-doxbrix/references/components.md +30 -3
- package/skills/doxloop-doxbrix/references/manifest.md +3 -2
- package/vendor/doxbrix-import/LICENSE +202 -0
- package/vendor/doxbrix-import/README.md +17 -0
- package/vendor/doxbrix-import/UPSTREAM.json +47 -0
- package/vendor/doxbrix-import/dist/docs/frontmatter.d.ts +13 -0
- package/vendor/doxbrix-import/dist/docs/frontmatter.js +83 -0
- package/vendor/doxbrix-import/dist/docs/import.d.ts +21 -0
- package/vendor/doxbrix-import/dist/docs/import.js +147 -0
- package/vendor/doxbrix-import/dist/docs/manifest.d.ts +163 -0
- package/vendor/doxbrix-import/dist/docs/manifest.js +64 -0
- package/vendor/doxbrix-import/dist/docs/project.d.ts +25 -0
- package/vendor/doxbrix-import/dist/docs/project.js +77 -0
- package/vendor/doxbrix-import/dist/docs/starter.d.ts +4 -0
- package/vendor/doxbrix-import/dist/docs/starter.js +11 -0
- package/vendor/doxbrix-import/dist/importer.d.ts +227 -0
- package/vendor/doxbrix-import/dist/importer.js +1567 -0
- package/vendor/doxbrix-import/dist/mintlify-openapi.d.ts +37 -0
- package/vendor/doxbrix-import/dist/mintlify-openapi.js +305 -0
- package/vendor/doxbrix-import/dist/safe-path.d.ts +9 -0
- package/vendor/doxbrix-import/dist/safe-path.js +47 -0
- package/dist/agents.d.ts.map +0 -1
- package/dist/agents.js.map +0 -1
- package/dist/args.d.ts.map +0 -1
- package/dist/args.js.map +0 -1
- package/dist/artifact-deploy.d.ts.map +0 -1
- package/dist/artifact-deploy.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/author.d.ts.map +0 -1
- package/dist/author.js.map +0 -1
- package/dist/capture.d.ts.map +0 -1
- package/dist/capture.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/deploy.d.ts.map +0 -1
- package/dist/deploy.js.map +0 -1
- package/dist/deployment-visibility.d.ts.map +0 -1
- package/dist/deployment-visibility.js.map +0 -1
- package/dist/doctor.d.ts.map +0 -1
- package/dist/doctor.js.map +0 -1
- package/dist/doxbrix-markdown.d.ts.map +0 -1
- package/dist/doxbrix-markdown.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/fs.d.ts.map +0 -1
- package/dist/fs.js.map +0 -1
- package/dist/generator-api.d.ts.map +0 -1
- package/dist/generator-api.js.map +0 -1
- package/dist/generator-manager.d.ts.map +0 -1
- package/dist/generator-manager.js.map +0 -1
- package/dist/generator-runtime.d.ts.map +0 -1
- package/dist/generator-runtime.js.map +0 -1
- package/dist/generators.d.ts.map +0 -1
- package/dist/generators.js.map +0 -1
- package/dist/interactive.d.ts.map +0 -1
- package/dist/interactive.js.map +0 -1
- package/dist/preview.d.ts.map +0 -1
- package/dist/preview.js.map +0 -1
- package/dist/progress.d.ts.map +0 -1
- package/dist/progress.js.map +0 -1
- package/dist/project.d.ts.map +0 -1
- package/dist/project.js.map +0 -1
- package/dist/prompts.d.ts.map +0 -1
- package/dist/prompts.js.map +0 -1
- package/dist/settings.d.ts.map +0 -1
- package/dist/settings.js.map +0 -1
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ApiStructuralDiff, OpenApiSnapshot, OpenApiSummary, SourceBinding } from './types.js';
|
|
2
|
+
export interface LoadedOpenApi {
|
|
3
|
+
content: string;
|
|
4
|
+
hash: string;
|
|
5
|
+
document: Record<string, unknown>;
|
|
6
|
+
summary: OpenApiSummary;
|
|
7
|
+
snapshot: OpenApiSnapshot;
|
|
8
|
+
etag?: string;
|
|
9
|
+
lastModified?: string;
|
|
10
|
+
notModified?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface RemoteOpenApiCache {
|
|
13
|
+
url: string;
|
|
14
|
+
content: string;
|
|
15
|
+
hash: string;
|
|
16
|
+
etag?: string;
|
|
17
|
+
lastModified?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface RemoteFetchOptions {
|
|
20
|
+
fetch?: typeof globalThis.fetch;
|
|
21
|
+
resolveHostname?: (hostname: string) => Promise<string[]>;
|
|
22
|
+
validators?: {
|
|
23
|
+
etag?: string;
|
|
24
|
+
lastModified?: string;
|
|
25
|
+
};
|
|
26
|
+
cached?: RemoteOpenApiCache;
|
|
27
|
+
maxBytes?: number;
|
|
28
|
+
}
|
|
29
|
+
/** Parse and validate a JSON or YAML OpenAPI/Swagger document. */
|
|
30
|
+
export declare function parseOpenApi(content: string, location?: string): LoadedOpenApi;
|
|
31
|
+
/** Load and validate either a local file or a safely fetched remote specification. */
|
|
32
|
+
export declare function loadOpenApiSource(root: string, source: SourceBinding, validators?: {
|
|
33
|
+
etag?: string;
|
|
34
|
+
lastModified?: string;
|
|
35
|
+
}): Promise<LoadedOpenApi>;
|
|
36
|
+
/** Fetch a remote specification with redirect, SSRF, validator, type, and size controls. */
|
|
37
|
+
export declare function fetchRemoteOpenApi(url: string, options?: RemoteFetchOptions): Promise<RemoteOpenApiCache & {
|
|
38
|
+
notModified?: boolean;
|
|
39
|
+
}>;
|
|
40
|
+
export declare function diffOpenApi(previous: OpenApiSnapshot | undefined, current: OpenApiSnapshot): ApiStructuralDiff;
|
|
41
|
+
export declare function openApiChangedIdentifiers(diff: ApiStructuralDiff): string[];
|
|
42
|
+
/** Where the plain-text copy of a remote specification is kept for agents to read. */
|
|
43
|
+
export declare function remoteOpenApiCopyPath(root: string, url: string, content?: string): string;
|
|
44
|
+
/** Make sure a remote specification has a readable local copy; returns its path, or undefined for local specs. */
|
|
45
|
+
export declare function ensureRemoteOpenApiCopy(root: string, source: SourceBinding): Promise<string | undefined>;
|
|
46
|
+
/**
|
|
47
|
+
* The part of a contract one citation names: an operation (`POST /users`,
|
|
48
|
+
* also reached through an error label such as `POST /users 422`), a schema,
|
|
49
|
+
* or a security scheme, with every component it references inlined below it
|
|
50
|
+
* so the writer sees request and response bodies without opening the spec.
|
|
51
|
+
* A citation that names nothing specific gets the contract's index instead.
|
|
52
|
+
*/
|
|
53
|
+
export declare function openApiExcerpt(document: Record<string, unknown>, citation: {
|
|
54
|
+
path: string;
|
|
55
|
+
label?: string;
|
|
56
|
+
}, budget: number): {
|
|
57
|
+
text: string;
|
|
58
|
+
truncated: boolean;
|
|
59
|
+
} | undefined;
|
|
60
|
+
//# sourceMappingURL=openapi.d.ts.map
|
package/dist/openapi.js
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lookup } from 'node:dns/promises';
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { isIP } from 'node:net';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { parseDocument, stringify as stringifyYaml } from 'yaml';
|
|
7
|
+
import { DoxloopError } from './errors.js';
|
|
8
|
+
import { pathExists } from './fs.js';
|
|
9
|
+
import { isSpecUrl } from './project.js';
|
|
10
|
+
const OPENAPI_CACHE_DIRECTORY = join('.doxloop', 'cache', 'openapi');
|
|
11
|
+
const MAX_SPEC_BYTES = 5 * 1024 * 1024;
|
|
12
|
+
const MAX_REDIRECTS = 3;
|
|
13
|
+
const METHODS = ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace'];
|
|
14
|
+
/** Parse and validate a JSON or YAML OpenAPI/Swagger document. */
|
|
15
|
+
export function parseOpenApi(content, location = 'OpenAPI specification') {
|
|
16
|
+
if (Buffer.byteLength(content, 'utf8') > MAX_SPEC_BYTES) {
|
|
17
|
+
throw new DoxloopError(`${location} exceeds the 5 MB OpenAPI safety limit.`);
|
|
18
|
+
}
|
|
19
|
+
let raw;
|
|
20
|
+
try {
|
|
21
|
+
if (content.trimStart().startsWith('{'))
|
|
22
|
+
raw = JSON.parse(content);
|
|
23
|
+
else {
|
|
24
|
+
const yaml = parseDocument(content, { prettyErrors: true });
|
|
25
|
+
if (yaml.errors.length > 0)
|
|
26
|
+
throw yaml.errors[0];
|
|
27
|
+
raw = yaml.toJS({ maxAliasCount: 50 });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
throw new DoxloopError(`${location} is not valid JSON or YAML: ${error instanceof Error ? error.message : String(error)}`);
|
|
32
|
+
}
|
|
33
|
+
if (!isRecord(raw))
|
|
34
|
+
throw new DoxloopError(`${location} must contain an object at the document root.`);
|
|
35
|
+
const version = text(raw.openapi) ?? text(raw.swagger);
|
|
36
|
+
if (!version || (!/^3\.\d+(?:\.\d+)?(?:[-+].*)?$/.test(version) && version !== '2.0')) {
|
|
37
|
+
throw new DoxloopError(`${location} must declare OpenAPI 3.x with "openapi" or Swagger 2.0 with "swagger".`);
|
|
38
|
+
}
|
|
39
|
+
const info = record(raw.info);
|
|
40
|
+
const title = text(info.title);
|
|
41
|
+
const apiVersion = text(info.version);
|
|
42
|
+
if (!title)
|
|
43
|
+
throw new DoxloopError(`${location} is missing the required info.title.`);
|
|
44
|
+
if (!apiVersion)
|
|
45
|
+
throw new DoxloopError(`${location} is missing the required info.version.`);
|
|
46
|
+
const paths = record(raw.paths);
|
|
47
|
+
if (Object.keys(paths).some((path) => !path.startsWith('/'))) {
|
|
48
|
+
throw new DoxloopError(`${location} contains a path key that does not start with "/".`);
|
|
49
|
+
}
|
|
50
|
+
const snapshot = openApiSnapshot(raw, version);
|
|
51
|
+
const servers = array(raw.servers).flatMap((item) => text(record(item).url) ?? []);
|
|
52
|
+
if (version === '2.0' && text(raw.host))
|
|
53
|
+
servers.push(`${array(raw.schemes).flatMap(text)[0] ?? 'https'}://${text(raw.host)}${text(raw.basePath) ?? ''}`);
|
|
54
|
+
const summary = {
|
|
55
|
+
title,
|
|
56
|
+
version: apiVersion,
|
|
57
|
+
specificationVersion: version,
|
|
58
|
+
servers,
|
|
59
|
+
securitySchemes: Object.keys(snapshot.securitySchemes).sort(),
|
|
60
|
+
schemas: Object.keys(snapshot.schemas).sort(),
|
|
61
|
+
operationCount: Object.keys(snapshot.operations).length,
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
content,
|
|
65
|
+
hash: createHash('sha256').update(content).digest('hex'),
|
|
66
|
+
document: raw,
|
|
67
|
+
summary,
|
|
68
|
+
snapshot,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Load and validate either a local file or a safely fetched remote specification. */
|
|
72
|
+
export async function loadOpenApiSource(root, source, validators) {
|
|
73
|
+
if (!isSpecUrl(source.path)) {
|
|
74
|
+
const path = resolve(root, source.path);
|
|
75
|
+
let content;
|
|
76
|
+
try {
|
|
77
|
+
content = await readFile(path, 'utf8');
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new DoxloopError(`The API specification does not exist or cannot be read: ${path}`);
|
|
81
|
+
}
|
|
82
|
+
return parseOpenApi(content, source.path);
|
|
83
|
+
}
|
|
84
|
+
const cached = await readRemoteCache(root, source.path);
|
|
85
|
+
const effectiveValidators = validators ?? (cached ? { ...(cached.etag ? { etag: cached.etag } : {}), ...(cached.lastModified ? { lastModified: cached.lastModified } : {}) } : undefined);
|
|
86
|
+
const remote = await fetchRemoteOpenApi(source.path, { ...(effectiveValidators ? { validators: effectiveValidators } : {}), ...(cached ? { cached } : {}) });
|
|
87
|
+
const parsed = parseOpenApi(remote.content, source.path);
|
|
88
|
+
const result = {
|
|
89
|
+
...parsed,
|
|
90
|
+
...(remote.etag ? { etag: remote.etag } : {}),
|
|
91
|
+
...(remote.lastModified ? { lastModified: remote.lastModified } : {}),
|
|
92
|
+
...(remote.notModified ? { notModified: true } : {}),
|
|
93
|
+
};
|
|
94
|
+
await writeRemoteCache(root, source.path, result);
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
/** Fetch a remote specification with redirect, SSRF, validator, type, and size controls. */
|
|
98
|
+
export async function fetchRemoteOpenApi(url, options = {}) {
|
|
99
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
100
|
+
const resolveHostname = options.resolveHostname ?? defaultResolveHostname;
|
|
101
|
+
const maxBytes = options.maxBytes ?? MAX_SPEC_BYTES;
|
|
102
|
+
let current = safeRemoteUrl(url);
|
|
103
|
+
for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects += 1) {
|
|
104
|
+
await assertPublicRemote(current, resolveHostname);
|
|
105
|
+
const headers = new Headers({ accept: 'application/json, application/yaml, application/x-yaml, text/yaml, text/plain;q=0.5' });
|
|
106
|
+
if (redirects === 0 && options.validators?.etag)
|
|
107
|
+
headers.set('if-none-match', options.validators.etag);
|
|
108
|
+
if (redirects === 0 && options.validators?.lastModified)
|
|
109
|
+
headers.set('if-modified-since', options.validators.lastModified);
|
|
110
|
+
const response = await fetcher(current, { method: 'GET', redirect: 'manual', headers, signal: AbortSignal.timeout(15_000) });
|
|
111
|
+
if (response.status === 304) {
|
|
112
|
+
if (!options.cached)
|
|
113
|
+
throw new DoxloopError(`Remote OpenAPI returned 304 but no cached document is available.`);
|
|
114
|
+
return { ...options.cached, notModified: true };
|
|
115
|
+
}
|
|
116
|
+
if (response.status >= 300 && response.status < 400) {
|
|
117
|
+
if (redirects === MAX_REDIRECTS)
|
|
118
|
+
throw new DoxloopError(`Remote OpenAPI redirect limit exceeded for ${url}.`);
|
|
119
|
+
const location = response.headers.get('location');
|
|
120
|
+
if (!location)
|
|
121
|
+
throw new DoxloopError(`Remote OpenAPI redirect from ${current} did not include a location.`);
|
|
122
|
+
current = safeRemoteUrl(new URL(location, current).toString());
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (response.status === 401 || response.status === 403) {
|
|
126
|
+
throw new DoxloopError(`Remote OpenAPI credentials are required. Configure a credential-free or pre-signed HTTPS URL; credentials are never stored by Doxloop.`);
|
|
127
|
+
}
|
|
128
|
+
if (!response.ok)
|
|
129
|
+
throw new DoxloopError(`Remote OpenAPI request failed with HTTP ${response.status}.`);
|
|
130
|
+
assertOpenApiContentType(response.headers.get('content-type'), current);
|
|
131
|
+
const declared = Number(response.headers.get('content-length'));
|
|
132
|
+
if (Number.isFinite(declared) && declared > maxBytes)
|
|
133
|
+
throw new DoxloopError(`Remote OpenAPI exceeds the ${formatBytes(maxBytes)} safety limit.`);
|
|
134
|
+
const content = await boundedResponseText(response, maxBytes);
|
|
135
|
+
return {
|
|
136
|
+
url: current,
|
|
137
|
+
content,
|
|
138
|
+
hash: createHash('sha256').update(content).digest('hex'),
|
|
139
|
+
...(response.headers.get('etag') ? { etag: response.headers.get('etag') } : {}),
|
|
140
|
+
...(response.headers.get('last-modified') ? { lastModified: response.headers.get('last-modified') } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
throw new DoxloopError(`Remote OpenAPI request could not be completed.`);
|
|
144
|
+
}
|
|
145
|
+
export function diffOpenApi(previous, current) {
|
|
146
|
+
if (!previous) {
|
|
147
|
+
return {
|
|
148
|
+
operations: { added: Object.keys(current.operations), removed: [], changed: [] },
|
|
149
|
+
schemas: { added: Object.keys(current.schemas), removed: [], changed: [] },
|
|
150
|
+
securitySchemes: { added: Object.keys(current.securitySchemes), removed: [], changed: [] },
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
operations: diffSnapshotRecords(previous.operations, current.operations, operationFacets),
|
|
155
|
+
schemas: diffSnapshotRecords(previous.schemas, current.schemas),
|
|
156
|
+
securitySchemes: diffSnapshotRecords(previous.securitySchemes, current.securitySchemes),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export function openApiChangedIdentifiers(diff) {
|
|
160
|
+
return [
|
|
161
|
+
...diff.operations.added,
|
|
162
|
+
...diff.operations.removed,
|
|
163
|
+
...diff.operations.changed.map((item) => item.id),
|
|
164
|
+
...diff.schemas.added.map((item) => `schema:${item}`),
|
|
165
|
+
...diff.schemas.removed.map((item) => `schema:${item}`),
|
|
166
|
+
...diff.schemas.changed.map((item) => `schema:${item.id}`),
|
|
167
|
+
...diff.securitySchemes.added.map((item) => `security:${item}`),
|
|
168
|
+
...diff.securitySchemes.removed.map((item) => `security:${item}`),
|
|
169
|
+
...diff.securitySchemes.changed.map((item) => `security:${item.id}`),
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
function openApiSnapshot(document, specificationVersion) {
|
|
173
|
+
const operations = {};
|
|
174
|
+
for (const [path, rawPath] of Object.entries(record(document.paths))) {
|
|
175
|
+
const pathItem = record(rawPath);
|
|
176
|
+
for (const method of METHODS) {
|
|
177
|
+
if (!(method in pathItem) || !isRecord(pathItem[method]))
|
|
178
|
+
continue;
|
|
179
|
+
const operation = record(pathItem[method]);
|
|
180
|
+
const id = `${method.toUpperCase()} ${path}`;
|
|
181
|
+
const parameters = [...array(pathItem.parameters), ...array(operation.parameters)];
|
|
182
|
+
operations[id] = {
|
|
183
|
+
operationId: text(operation.operationId) ?? '',
|
|
184
|
+
parameters: stableHash(parameters),
|
|
185
|
+
requestBody: stableHash(operation.requestBody),
|
|
186
|
+
responses: stableHash(operation.responses),
|
|
187
|
+
security: stableHash(operation.security ?? document.security),
|
|
188
|
+
examples: stableHash(operationExamples(operation)),
|
|
189
|
+
full: stableHash(operation),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const schemasSource = specificationVersion === '2.0'
|
|
194
|
+
? record(document.definitions)
|
|
195
|
+
: record(record(document.components).schemas);
|
|
196
|
+
const securitySource = specificationVersion === '2.0'
|
|
197
|
+
? record(document.securityDefinitions)
|
|
198
|
+
: record(record(document.components).securitySchemes);
|
|
199
|
+
return {
|
|
200
|
+
schemaVersion: 1,
|
|
201
|
+
specificationVersion,
|
|
202
|
+
operations,
|
|
203
|
+
schemas: hashRecord(schemasSource),
|
|
204
|
+
securitySchemes: hashRecord(securitySource),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function operationFacets(before, after) {
|
|
208
|
+
return ['operationId', 'parameters', 'requestBody', 'responses', 'security', 'examples']
|
|
209
|
+
.filter((facet) => before[facet] !== after[facet]);
|
|
210
|
+
}
|
|
211
|
+
function diffSnapshotRecords(before, after, facets) {
|
|
212
|
+
const beforeKeys = new Set(Object.keys(before));
|
|
213
|
+
const afterKeys = new Set(Object.keys(after));
|
|
214
|
+
const added = [...afterKeys].filter((key) => !beforeKeys.has(key)).sort();
|
|
215
|
+
const removed = [...beforeKeys].filter((key) => !afterKeys.has(key)).sort();
|
|
216
|
+
const changed = [...beforeKeys].filter((key) => afterKeys.has(key) && stableHash(before[key]) !== stableHash(after[key])).sort().map((id) => ({
|
|
217
|
+
id,
|
|
218
|
+
facets: facets && isRecord(before[id]) && isRecord(after[id]) ? facets(before[id], after[id]) : ['definition'],
|
|
219
|
+
}));
|
|
220
|
+
return { added, removed, changed };
|
|
221
|
+
}
|
|
222
|
+
function hashRecord(value) {
|
|
223
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stableHash(item)]));
|
|
224
|
+
}
|
|
225
|
+
function operationExamples(operation) {
|
|
226
|
+
return {
|
|
227
|
+
examples: operation.examples,
|
|
228
|
+
requestBody: record(operation.requestBody).content,
|
|
229
|
+
responses: Object.fromEntries(Object.entries(record(operation.responses)).map(([status, response]) => [status, record(response).content])),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function stableHash(value) {
|
|
233
|
+
return createHash('sha256').update(stableStringify(value)).digest('hex');
|
|
234
|
+
}
|
|
235
|
+
function stableStringify(value) {
|
|
236
|
+
if (Array.isArray(value))
|
|
237
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
238
|
+
if (isRecord(value))
|
|
239
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
|
|
240
|
+
return JSON.stringify(value) ?? 'null';
|
|
241
|
+
}
|
|
242
|
+
async function boundedResponseText(response, maximum) {
|
|
243
|
+
if (!response.body)
|
|
244
|
+
return '';
|
|
245
|
+
const reader = response.body.getReader();
|
|
246
|
+
const chunks = [];
|
|
247
|
+
let size = 0;
|
|
248
|
+
while (true) {
|
|
249
|
+
const next = await reader.read();
|
|
250
|
+
if (next.done)
|
|
251
|
+
break;
|
|
252
|
+
size += next.value.byteLength;
|
|
253
|
+
if (size > maximum) {
|
|
254
|
+
await reader.cancel();
|
|
255
|
+
throw new DoxloopError(`Remote OpenAPI exceeds the ${formatBytes(maximum)} safety limit.`);
|
|
256
|
+
}
|
|
257
|
+
chunks.push(next.value);
|
|
258
|
+
}
|
|
259
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
260
|
+
}
|
|
261
|
+
function safeRemoteUrl(raw) {
|
|
262
|
+
let url;
|
|
263
|
+
try {
|
|
264
|
+
url = new URL(raw);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
throw new DoxloopError(`Invalid remote OpenAPI URL: ${raw}`);
|
|
268
|
+
}
|
|
269
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
270
|
+
throw new DoxloopError('Remote OpenAPI URLs must use HTTP or HTTPS.');
|
|
271
|
+
if (url.username || url.password)
|
|
272
|
+
throw new DoxloopError('Remote OpenAPI URLs cannot contain embedded credentials.');
|
|
273
|
+
if (url.hash)
|
|
274
|
+
url.hash = '';
|
|
275
|
+
return url.toString();
|
|
276
|
+
}
|
|
277
|
+
async function assertPublicRemote(url, resolver) {
|
|
278
|
+
const parsed = new URL(url);
|
|
279
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
280
|
+
if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname.endsWith('.local')) {
|
|
281
|
+
throw new DoxloopError('Remote OpenAPI URLs cannot target localhost or private networks.');
|
|
282
|
+
}
|
|
283
|
+
const addresses = isIP(hostname) ? [hostname] : await resolver(hostname);
|
|
284
|
+
if (addresses.length === 0 || addresses.some(isPrivateAddress)) {
|
|
285
|
+
throw new DoxloopError('Remote OpenAPI URLs cannot target localhost or private networks.');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function defaultResolveHostname(hostname) {
|
|
289
|
+
try {
|
|
290
|
+
return (await lookup(hostname, { all: true })).map((item) => item.address);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
throw new DoxloopError(`Remote OpenAPI host could not be resolved: ${hostname}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function isPrivateAddress(address) {
|
|
297
|
+
const normalized = address.toLowerCase().replace(/^::ffff:/, '');
|
|
298
|
+
if (normalized === '::1' || normalized === '::' || normalized.startsWith('fc') || normalized.startsWith('fd') || normalized.startsWith('fe8') || normalized.startsWith('fe9') || normalized.startsWith('fea') || normalized.startsWith('feb'))
|
|
299
|
+
return true;
|
|
300
|
+
const parts = normalized.split('.').map(Number);
|
|
301
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part)))
|
|
302
|
+
return false;
|
|
303
|
+
const first = parts[0] ?? -1;
|
|
304
|
+
return first === 0 || first === 10 || first === 127 || (first === 169 && parts[1] === 254) || (first === 172 && (parts[1] ?? 0) >= 16 && (parts[1] ?? 0) <= 31) || (first === 192 && parts[1] === 168) || first >= 224;
|
|
305
|
+
}
|
|
306
|
+
function assertOpenApiContentType(raw, url) {
|
|
307
|
+
const type = raw?.split(';')[0]?.trim().toLowerCase();
|
|
308
|
+
const supported = new Set(['application/json', 'application/yaml', 'application/x-yaml', 'text/yaml', 'text/x-yaml', 'text/plain', 'application/octet-stream']);
|
|
309
|
+
if (type && !supported.has(type) && !/\.(json|ya?ml)$/i.test(new URL(url).pathname)) {
|
|
310
|
+
throw new DoxloopError(`Remote OpenAPI returned unsupported content type "${type}". Expected JSON or YAML.`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function readRemoteCache(root, url) {
|
|
314
|
+
const path = remoteCachePath(root, url);
|
|
315
|
+
if (!(await pathExists(path)))
|
|
316
|
+
return undefined;
|
|
317
|
+
try {
|
|
318
|
+
const value = JSON.parse(await readFile(path, 'utf8'));
|
|
319
|
+
return value.url === url && typeof value.content === 'string' && typeof value.hash === 'string' ? value : undefined;
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async function writeRemoteCache(root, url, loaded) {
|
|
326
|
+
const path = remoteCachePath(root, url);
|
|
327
|
+
await mkdir(dirname(path), { recursive: true });
|
|
328
|
+
await writeFile(path, `${JSON.stringify({ url, content: loaded.content, hash: loaded.hash, etag: loaded.etag, lastModified: loaded.lastModified }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
329
|
+
// Writing agents run without network access and cannot read the URL; they
|
|
330
|
+
// read this plain copy of the specification instead.
|
|
331
|
+
await writeFile(remoteOpenApiCopyPath(root, url, loaded.content), loaded.content, { encoding: 'utf8', mode: 0o600 });
|
|
332
|
+
}
|
|
333
|
+
/** Where the plain-text copy of a remote specification is kept for agents to read. */
|
|
334
|
+
export function remoteOpenApiCopyPath(root, url, content) {
|
|
335
|
+
const json = content !== undefined ? content.trimStart().startsWith('{') : /\.json(?:$|[?#])/i.test(url);
|
|
336
|
+
return join(root, OPENAPI_CACHE_DIRECTORY, `${createHash('sha256').update(url).digest('hex')}.spec.${json ? 'json' : 'yaml'}`);
|
|
337
|
+
}
|
|
338
|
+
/** Make sure a remote specification has a readable local copy; returns its path, or undefined for local specs. */
|
|
339
|
+
export async function ensureRemoteOpenApiCopy(root, source) {
|
|
340
|
+
if (!isSpecUrl(source.path))
|
|
341
|
+
return undefined;
|
|
342
|
+
const cached = await readRemoteCache(root, source.path);
|
|
343
|
+
if (cached) {
|
|
344
|
+
const copy = remoteOpenApiCopyPath(root, source.path, cached.content);
|
|
345
|
+
if (!(await pathExists(copy)))
|
|
346
|
+
await writeFile(copy, cached.content, { encoding: 'utf8', mode: 0o600 });
|
|
347
|
+
return copy;
|
|
348
|
+
}
|
|
349
|
+
const loaded = await loadOpenApiSource(root, source);
|
|
350
|
+
return remoteOpenApiCopyPath(root, source.path, loaded.content);
|
|
351
|
+
}
|
|
352
|
+
function remoteCachePath(root, url) {
|
|
353
|
+
return join(root, OPENAPI_CACHE_DIRECTORY, `${createHash('sha256').update(url).digest('hex')}.json`);
|
|
354
|
+
}
|
|
355
|
+
function formatBytes(value) { return `${Math.round(value / 1024 / 1024)} MB`; }
|
|
356
|
+
function record(value) { return isRecord(value) ? value : {}; }
|
|
357
|
+
function isRecord(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); }
|
|
358
|
+
function array(value) { return Array.isArray(value) ? value : []; }
|
|
359
|
+
function text(value) { return typeof value === 'string' && value.trim() ? value.trim() : undefined; }
|
|
360
|
+
const OPERATION_LABEL = /^(get|put|post|delete|patch|options|head|trace)\s+(\/\S*)/i;
|
|
361
|
+
/**
|
|
362
|
+
* The part of a contract one citation names: an operation (`POST /users`,
|
|
363
|
+
* also reached through an error label such as `POST /users 422`), a schema,
|
|
364
|
+
* or a security scheme, with every component it references inlined below it
|
|
365
|
+
* so the writer sees request and response bodies without opening the spec.
|
|
366
|
+
* A citation that names nothing specific gets the contract's index instead.
|
|
367
|
+
*/
|
|
368
|
+
export function openApiExcerpt(document, citation, budget) {
|
|
369
|
+
const components = record(document.components);
|
|
370
|
+
const definitions = record(document.definitions);
|
|
371
|
+
const selected = {};
|
|
372
|
+
const wanted = [citation.label, citation.path].filter((value) => Boolean(value?.trim())).map((value) => value.trim());
|
|
373
|
+
for (const value of wanted) {
|
|
374
|
+
const operation = OPERATION_LABEL.exec(value);
|
|
375
|
+
if (operation) {
|
|
376
|
+
const item = record(record(document.paths)[operation[2]])[operation[1].toLowerCase()];
|
|
377
|
+
if (isRecord(item)) {
|
|
378
|
+
selected[`${operation[1].toUpperCase()} ${operation[2]}`] = item;
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const name = value.replace(/^(?:Schema |schema:)/, '');
|
|
383
|
+
const schema = record(components.schemas)[name] ?? definitions[name];
|
|
384
|
+
if (isRecord(schema)) {
|
|
385
|
+
selected[`Schema ${name}`] = schema;
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
const scheme = record(components.securitySchemes)[name] ?? record(document.securityDefinitions)[name];
|
|
389
|
+
if (isRecord(scheme)) {
|
|
390
|
+
selected[`Security scheme ${name}`] = scheme;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (Object.keys(selected).length === 0) {
|
|
395
|
+
const index = {
|
|
396
|
+
info: document.info,
|
|
397
|
+
servers: document.servers ?? (document.host ? [`${document.host}${text(document.basePath) ?? ''}`] : undefined),
|
|
398
|
+
security: document.security,
|
|
399
|
+
securitySchemes: components.securitySchemes ?? document.securityDefinitions,
|
|
400
|
+
operations: Object.fromEntries(Object.entries(record(document.paths)).flatMap(([path, item]) => METHODS.filter((method) => isRecord(record(item)[method])).map((method) => [`${method.toUpperCase()} ${path}`, text(record(record(item)[method]).summary) ?? '']))),
|
|
401
|
+
};
|
|
402
|
+
return clip(stringifyYaml(index, { lineWidth: 0 }), budget);
|
|
403
|
+
}
|
|
404
|
+
// Inline referenced components, breadth first, until the budget is spent.
|
|
405
|
+
const referenced = {};
|
|
406
|
+
const queue = [...collectRefs(selected)];
|
|
407
|
+
while (queue.length > 0 && Object.keys(referenced).length < 40) {
|
|
408
|
+
const ref = queue.shift();
|
|
409
|
+
if (ref in referenced)
|
|
410
|
+
continue;
|
|
411
|
+
const target = ref.replace(/^#\//, '').split('/').reduce((node, key) => record(node)[key.replace(/~1/g, '/').replace(/~0/g, '~')], document);
|
|
412
|
+
if (target === undefined)
|
|
413
|
+
continue;
|
|
414
|
+
referenced[ref] = target;
|
|
415
|
+
queue.push(...collectRefs(target));
|
|
416
|
+
}
|
|
417
|
+
const body = { ...selected, ...(Object.keys(referenced).length > 0 ? { 'Referenced components': referenced } : {}) };
|
|
418
|
+
return clip(stringifyYaml(body, { lineWidth: 0 }), budget);
|
|
419
|
+
}
|
|
420
|
+
function collectRefs(value, found = new Set()) {
|
|
421
|
+
if (Array.isArray(value))
|
|
422
|
+
for (const item of value)
|
|
423
|
+
collectRefs(item, found);
|
|
424
|
+
else if (isRecord(value))
|
|
425
|
+
for (const [key, item] of Object.entries(value)) {
|
|
426
|
+
if (key === '$ref' && typeof item === 'string' && item.startsWith('#/'))
|
|
427
|
+
found.add(item);
|
|
428
|
+
else
|
|
429
|
+
collectRefs(item, found);
|
|
430
|
+
}
|
|
431
|
+
return found;
|
|
432
|
+
}
|
|
433
|
+
function clip(value, budget) {
|
|
434
|
+
if (value.length <= budget)
|
|
435
|
+
return { text: value, truncated: false };
|
|
436
|
+
const cut = value.slice(0, budget);
|
|
437
|
+
return { text: cut.slice(0, Math.max(cut.lastIndexOf('\n'), budget - 200)), truncated: true };
|
|
438
|
+
}
|
|
439
|
+
//# sourceMappingURL=openapi.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** Runs inside the isolated editor iframe. Keep this function self-contained. */
|
|
2
|
+
export function installPageEditorBridge() {
|
|
3
|
+
const originals = new WeakMap();
|
|
4
|
+
const elements = [...document.querySelectorAll('[data-edit-start]')];
|
|
5
|
+
const linkLabels = [];
|
|
6
|
+
const children = (node) => [...node.childNodes].map(serialize).join('');
|
|
7
|
+
function serialize(node) {
|
|
8
|
+
if (node.nodeType === Node.TEXT_NODE)
|
|
9
|
+
return (node.textContent ?? '').replaceAll('\u00a0', ' ');
|
|
10
|
+
if (!(node instanceof HTMLElement))
|
|
11
|
+
return '';
|
|
12
|
+
const original = originals.get(node);
|
|
13
|
+
if (original && node.innerHTML === original.html)
|
|
14
|
+
return original.markdown;
|
|
15
|
+
if (node.dataset.mdAtom !== undefined)
|
|
16
|
+
return node.dataset.mdAtom;
|
|
17
|
+
const text = children(node);
|
|
18
|
+
switch (node.tagName) {
|
|
19
|
+
case 'STRONG':
|
|
20
|
+
case 'B': return text ? `**${text}**` : '';
|
|
21
|
+
case 'EM':
|
|
22
|
+
case 'I': return text ? `*${text}*` : '';
|
|
23
|
+
case 'S':
|
|
24
|
+
case 'STRIKE': return text ? `~~${text}~~` : '';
|
|
25
|
+
case 'CODE': return text ? '`' + (node.textContent ?? '') + '`' : '';
|
|
26
|
+
case 'A': return '[' + text + (node.dataset.mdSuffix ?? `](${node.getAttribute('href') ?? ''})`);
|
|
27
|
+
case 'BR': return '\n';
|
|
28
|
+
case 'DIV':
|
|
29
|
+
case 'P': return (node.previousSibling ? '\n' : '') + text;
|
|
30
|
+
default: return text;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const publish = (el) => {
|
|
34
|
+
let text;
|
|
35
|
+
const original = originals.get(el);
|
|
36
|
+
if (original && el.innerHTML === original.html)
|
|
37
|
+
text = original.markdown;
|
|
38
|
+
else if (el.dataset.editFormat === 'yaml')
|
|
39
|
+
text = JSON.stringify(el.innerText.replace(/\n/g, ' '));
|
|
40
|
+
else if (el.dataset.editFormat === 'plain')
|
|
41
|
+
text = el.innerText;
|
|
42
|
+
else
|
|
43
|
+
text = children(el).replace(/\n/g, ' ');
|
|
44
|
+
// Preserve the document's line-ending convention when editing fenced code.
|
|
45
|
+
if (el.dataset.editSource?.includes('\r\n'))
|
|
46
|
+
text = text.replace(/\r?\n/g, '\r\n');
|
|
47
|
+
parent.postMessage({ type: 'doxloop:edit-block', start: Number(el.dataset.editStart), end: Number(el.dataset.editEnd), text }, '*');
|
|
48
|
+
};
|
|
49
|
+
elements.forEach(el => {
|
|
50
|
+
const plain = el.dataset.editFormat !== 'markdown';
|
|
51
|
+
// Give links a dedicated editing host. Chromium otherwise moves the caret
|
|
52
|
+
// outside the anchor after replacing its label's final character.
|
|
53
|
+
el.querySelectorAll('a').forEach(link => {
|
|
54
|
+
const label = document.createElement('span');
|
|
55
|
+
label.dataset.editLinkLabel = '';
|
|
56
|
+
label.contentEditable = 'true';
|
|
57
|
+
link.contentEditable = 'false';
|
|
58
|
+
while (link.firstChild)
|
|
59
|
+
label.append(link.firstChild);
|
|
60
|
+
link.append(label);
|
|
61
|
+
linkLabels.push(label);
|
|
62
|
+
});
|
|
63
|
+
el.contentEditable = plain ? 'plaintext-only' : 'true';
|
|
64
|
+
el.setAttribute('role', 'textbox');
|
|
65
|
+
el.setAttribute('aria-label', 'Edit ' + (el.tagName === 'CODE' ? 'code' : /^H\d$/.test(el.tagName) ? 'heading' : el.tagName === 'LI' ? 'list item' : /^(TH|TD)$/.test(el.tagName) ? 'table cell' : 'paragraph'));
|
|
66
|
+
// Cache nested mark serialization so changing nearby words leaves their source intact.
|
|
67
|
+
Array.from(el.querySelectorAll('strong,em,s,code,a,[data-md-atom]')).reverse().forEach(node => {
|
|
68
|
+
originals.set(node, { html: node.innerHTML, markdown: serialize(node) });
|
|
69
|
+
});
|
|
70
|
+
originals.set(el, { html: el.innerHTML, markdown: el.dataset.editSource ?? '' });
|
|
71
|
+
el.addEventListener('input', () => publish(el));
|
|
72
|
+
el.addEventListener('beforeinput', event => {
|
|
73
|
+
const input = event;
|
|
74
|
+
if (el.dataset.editFormat !== 'plain' && (input.inputType === 'insertParagraph' || input.inputType === 'insertLineBreak')) {
|
|
75
|
+
// Each region owns one Markdown block; a newline must not split its list/table syntax.
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
document.execCommand('insertText', false, ' ');
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
el.addEventListener('paste', event => {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
const text = event.clipboardData?.getData('text/plain') ?? '';
|
|
83
|
+
document.execCommand('insertText', false, el.dataset.editFormat === 'plain' ? text : text.replace(/\r?\n/g, ' '));
|
|
84
|
+
});
|
|
85
|
+
el.addEventListener('drop', event => event.preventDefault());
|
|
86
|
+
});
|
|
87
|
+
addEventListener('message', event => {
|
|
88
|
+
if (event.source === parent && event.data?.type === 'doxloop:editor-lock') {
|
|
89
|
+
linkLabels.forEach(label => { label.contentEditable = event.data.locked ? 'false' : 'true'; });
|
|
90
|
+
elements.forEach(el => { el.contentEditable = event.data.locked ? 'false' : el.dataset.editFormat !== 'markdown' ? 'plaintext-only' : 'true'; });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// Prevent selection/replacement across separate source regions.
|
|
94
|
+
document.addEventListener('beforeinput', event => {
|
|
95
|
+
const selection = getSelection();
|
|
96
|
+
const region = (node) => (node instanceof Element ? node : node?.parentElement)?.closest('[data-edit-start]');
|
|
97
|
+
if (selection && region(selection.anchorNode) !== region(selection.focusNode))
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
});
|
|
100
|
+
document.addEventListener('click', event => { if (event.target.closest('a'))
|
|
101
|
+
event.preventDefault(); });
|
|
102
|
+
document.addEventListener('keydown', event => {
|
|
103
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 's') {
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
parent.postMessage({ type: 'doxloop:save-draft' }, '*');
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=page-editor-bridge.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** A sandboxed native rendering of a draft; this endpoint never writes a file. */
|
|
2
|
+
export declare function pageEditorPreview(root: string, path: string, content: string, base: string): Promise<{
|
|
3
|
+
html: string;
|
|
4
|
+
blocks: {
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
text: string;
|
|
8
|
+
}[];
|
|
9
|
+
}>;
|
|
10
|
+
//# sourceMappingURL=page-editor-preview.d.ts.map
|