mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-09-30 08:23:14 +00:00
feat(api): swap /docs from Swagger UI to Redoc (vendored offline) (#129)
Some checks are pending
Deploy / deploy (push) Waiting to run
Some checks are pending
Deploy / deploy (push) Waiting to run
Redoc 2.5.3 standalone bundle (MIT) replaces the three swagger-ui-dist assets: one ~1.05MB JS file instead of ~1.8MB of JS+CSS+preset, and the layout traps (StandaloneLayout/BaseLayout) go away. Redoc is read-only by design, which matches the existing posture: try-it-out was already disabled since data routes need SigV4 (Postman for live calls). Unchanged: single token-gated response, offline vendoring (no CDN), per-request server-URL injection, script-breakout guards, cached shell with per-request spec splice. Bundle self-containment verified: the search worker is an inlined Blob, and the only new Worker(filename) path is Prism's async mode, which Redoc never invokes. Verified via headless-Chrome render of the real handler output: all routes, the OpenAPI 3.1 webhooks section, and planned-route markers render; no placeholder leakage.
This commit is contained in:
parent
8156b275a9
commit
24c89b43e5
11 changed files with 1899 additions and 112 deletions
|
|
@ -96,7 +96,7 @@ A read-only REST API (API Gateway + the `procurement-api` Lambda, `lambdas/api/`
|
|||
| `GET /work-orders/{id}` · `/purchase-orders/{id}` · `/verified-sites/{siteCode}` | IAM SigV4 | GetItem (404 on miss) |
|
||||
| `GET /work-orders/{id}/comments` | IAM SigV4 | Query on the partition key, paginated |
|
||||
| `POST /work-orders/{id}/comments`, `PATCH /work-orders/{id}` | — | **phase-2 planned** (`x-planned` in the spec); handler answers 501 |
|
||||
| `GET /docs`, `GET /openapi.json` | shared docs token (`X-Auth-Token` header or `?token=` in a browser) | stock Swagger UI (vendored offline, no CDN) with the spec inlined / the committed spec |
|
||||
| `GET /docs`, `GET /openapi.json` | shared docs token (`X-Auth-Token` header or `?token=` in a browser) | Redoc reference docs (vendored offline, no CDN) with the spec inlined / the committed spec |
|
||||
|
||||
- **Spec is source of truth:** `lambdas/api/openapi.json` (OpenAPI 3.1). Its top-level `webhooks` section documents the outbound SHOC work-order feed, so one page describes both directions (call + be-called). `tests/test_api_spec_drift.py` pins the spec's paths to the router table, so spec and implementation cannot drift.
|
||||
- **Auth:** data routes use API Gateway `AWS_IAM` (SigV4) plus a resource policy allowing exactly `arn:aws:iam::396287094661:role/shoc-backend-dev` on `GET/*`; same-account admin callers authorize via identity policy (Postman signs SigV4 natively). Docs routes are auth `NONE` at the gateway (resource-policy carve-out for exactly those two GETs) but the handler fails closed on the shared token (`lambdas/shared/web_ui_auth.py`, secret `procurement-ingest/web-ui-auth-token`) — not an unauthenticated data path (INFRA-74 posture).
|
||||
|
|
@ -436,9 +436,8 @@ lambdas/ # Phase 2: shared Code.from_asset("../lambdas") bundling
|
|||
wo_repo.py # WorkOrders/WorkOrderComments reads (paginated Scan / PK Query)
|
||||
po_repo.py # purchase-orders/verified-sites reads (no VendorReplies -- dead table)
|
||||
openapi.json # OpenAPI 3.1 source of truth (paths + outbound `webhooks` section)
|
||||
docs.html # Swagger UI shell; handler inlines the css/js/spec at request time (single token-gated request, no CDN)
|
||||
swagger-ui-bundle.js # vendored stock Swagger UI (swagger-ui-dist 5.17.14, Apache-2.0), inlined into /docs
|
||||
swagger-ui.css # vendored Swagger UI stylesheet, inlined into /docs
|
||||
docs.html # Redoc shell; handler inlines the bundle/spec at request time (single token-gated request, no CDN)
|
||||
redoc.standalone.js # vendored Redoc bundle (redoc 2.5.3, MIT), inlined into /docs; read-only docs, no try-it-out
|
||||
po/ # PO pipeline Lambdas
|
||||
email_processor/ # Phase 5: God-handler decomposed into flat siblings (bare-name
|
||||
# imports; the Phase 0/2/3 `cp <pipeline>/email_processor/*.py`
|
||||
|
|
|
|||
|
|
@ -111,16 +111,14 @@ class ProcurementApiStack(Stack):
|
|||
"-c",
|
||||
# Non-recursive glob ships every api/ sibling (the
|
||||
# allowlist-omission trap from PR #105/PR #2); the spec,
|
||||
# docs page, and the vendored Swagger UI assets ride
|
||||
# along because the handler serves them from its own
|
||||
# package dir. web_ui_auth.py must land FLAT beside
|
||||
# handler.py for the bare import.
|
||||
# docs page, and the vendored Redoc bundle ride along
|
||||
# because the handler serves them from its own package
|
||||
# dir. web_ui_auth.py must land FLAT beside handler.py
|
||||
# for the bare import.
|
||||
"cp api/*.py /asset-output/ && "
|
||||
"cp api/openapi.json /asset-output/ && "
|
||||
"cp api/docs.html /asset-output/ && "
|
||||
"cp api/swagger-ui-bundle.js /asset-output/ && "
|
||||
"cp api/swagger-ui-standalone-preset.js /asset-output/ && "
|
||||
"cp api/swagger-ui.css /asset-output/ && "
|
||||
"cp api/redoc.standalone.js /asset-output/ && "
|
||||
"cp shared/web_ui_auth.py /asset-output/ && "
|
||||
"rm -rf /asset-output/__pycache__",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -5,50 +5,37 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Procurement Ingest API</title>
|
||||
<!--
|
||||
Stock Swagger UI (swagger-ui-dist, Apache-2.0), vendored offline and inlined
|
||||
server-side so /docs is a single token-gated request with no CDN or
|
||||
follow-up asset fetch. The CSS, swagger-ui-bundle.js, the standalone preset,
|
||||
and the OpenAPI spec are substituted for the placeholders below by
|
||||
lambdas/api/handler.py::_load_docs_html. The initializer is the canonical
|
||||
swagger-ui-dist recipe (StandaloneLayout + both presets); the topbar's
|
||||
spec-URL bar is hidden since the spec is fixed and inlined.
|
||||
Redoc (redoc.standalone.js, MIT), vendored offline and inlined server-side
|
||||
so /docs is a single token-gated request with no CDN or follow-up asset
|
||||
fetch. The bundle and the OpenAPI spec are substituted for the placeholders
|
||||
below by lambdas/api/handler.py::_load_docs_html. Redoc is read-only by
|
||||
design -- there is no try-it-out to disable; live calls go through Postman
|
||||
(Authorization type "AWS Signature") since data routes require SigV4.
|
||||
The theme pins a system font stack: Redoc defaults to Roboto, which is not
|
||||
vendored and must not be fetched from Google Fonts.
|
||||
-->
|
||||
<style>__SWAGGER_UI_CSS__</style>
|
||||
<style>
|
||||
html { box-sizing: border-box; overflow-y: scroll; }
|
||||
*, *:before, *:after { box-sizing: inherit; }
|
||||
body { margin: 0; background: #fafafa; }
|
||||
.swagger-ui .topbar { display: none; }
|
||||
/* Give the description prose room: Swagger UI's default markdown line-height
|
||||
is tight, so inline code chips (background + padding) visually collide
|
||||
across lines. Loosen line-height and keep chips from wrapping. */
|
||||
.swagger-ui .info { margin: 30px 0 20px; }
|
||||
.swagger-ui .info .markdown p,
|
||||
.swagger-ui .info .renderedMarkdown p { line-height: 1.75; margin: 0 0 12px; }
|
||||
.swagger-ui .info code,
|
||||
.swagger-ui .renderedMarkdown code { padding: 1px 5px; line-height: 1.6; white-space: nowrap; }
|
||||
.swagger-ui .scheme-container { margin: 0 0 20px; padding: 20px 0; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script>__SWAGGER_UI_JS__</script>
|
||||
<script>__SWAGGER_UI_STANDALONE_PRESET_JS__</script>
|
||||
<div id="redoc"></div>
|
||||
<script>__REDOC_JS__</script>
|
||||
<script>
|
||||
window.ui = SwaggerUIBundle({
|
||||
spec: __OPENAPI_SPEC_JSON__,
|
||||
dom_id: "#swagger-ui",
|
||||
deepLinking: true,
|
||||
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
||||
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
|
||||
layout: "StandaloneLayout",
|
||||
docExpansion: "list",
|
||||
defaultModelsExpandDepth: 1,
|
||||
// Try-it-out is disabled: data routes require AWS SigV4, which a browser
|
||||
// can't sign. Use Postman (Authorization type "AWS Signature") against the
|
||||
// same spec for live calls.
|
||||
supportedSubmitMethods: []
|
||||
});
|
||||
Redoc.init(
|
||||
__OPENAPI_SPEC_JSON__,
|
||||
{
|
||||
theme: {
|
||||
typography: {
|
||||
fontFamily: 'system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif',
|
||||
headings: {
|
||||
fontFamily: 'system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
document.getElementById("redoc")
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -27,17 +27,13 @@ logger.setLevel(logging.INFO)
|
|||
_MODULE_DIR = Path(__file__).resolve().parent
|
||||
_SPEC_PATH = _MODULE_DIR / "openapi.json"
|
||||
_DOCS_PATH = _MODULE_DIR / "docs.html"
|
||||
_SWAGGER_CSS_PATH = _MODULE_DIR / "swagger-ui.css"
|
||||
_SWAGGER_JS_PATH = _MODULE_DIR / "swagger-ui-bundle.js"
|
||||
_SWAGGER_PRESET_PATH = _MODULE_DIR / "swagger-ui-standalone-preset.js"
|
||||
_REDOC_JS_PATH = _MODULE_DIR / "redoc.standalone.js"
|
||||
# The docs page template carries these placeholders, filled server-side so
|
||||
# /docs is a single token-gated request (a browser can't attach the auth
|
||||
# header to a follow-up asset fetch) with the stock Swagger UI + spec inlined,
|
||||
# no CDN.
|
||||
# header to a follow-up asset fetch) with the vendored Redoc bundle + spec
|
||||
# inlined, no CDN.
|
||||
_SPEC_PLACEHOLDER = "__OPENAPI_SPEC_JSON__"
|
||||
_CSS_PLACEHOLDER = "__SWAGGER_UI_CSS__"
|
||||
_JS_PLACEHOLDER = "__SWAGGER_UI_JS__"
|
||||
_PRESET_PLACEHOLDER = "__SWAGGER_UI_STANDALONE_PRESET_JS__"
|
||||
_JS_PLACEHOLDER = "__REDOC_JS__"
|
||||
|
||||
_spec_cache = None
|
||||
_docs_shell_cache = None
|
||||
|
|
@ -96,39 +92,30 @@ def _spec_for_request(event: dict) -> str:
|
|||
|
||||
|
||||
def _docs_shell() -> str:
|
||||
"""The Swagger UI page with the vendored CSS/JS/preset inlined; the SPEC
|
||||
placeholder is left intact so the per-request server-injected spec splices
|
||||
in cheaply (the heavy ~1.8MB assets are assembled once and cached).
|
||||
"""The Redoc page with the vendored bundle inlined; the SPEC placeholder
|
||||
is left intact so the per-request server-injected spec splices in cheaply
|
||||
(the heavy ~1.1MB bundle is assembled once and cached).
|
||||
|
||||
Each blob is spliced into a <style>/<script> block, where the HTML parser
|
||||
ends the element at the first literal "</style"/"</script" regardless of
|
||||
quoting, so guard each against a breakout a future asset update could add
|
||||
(the pinned assets have none today): JS/preset "</script" -> "<\\/script"
|
||||
(equivalent inside JS string/regex), CSS "</style" -> "<\\/style".
|
||||
The bundle is spliced into a <script> block, where the HTML parser ends
|
||||
the element at the first literal "</script" regardless of quoting, so
|
||||
guard against a breakout a future asset update could add (the pinned
|
||||
bundle has none today): "</script" -> "<\\/script" (equivalent inside a
|
||||
JS string/regex).
|
||||
"""
|
||||
global _docs_shell_cache
|
||||
if _docs_shell_cache is None:
|
||||
css = _SWAGGER_CSS_PATH.read_text(encoding="utf-8").replace(
|
||||
"</style", "<\\/style"
|
||||
)
|
||||
js = _SWAGGER_JS_PATH.read_text(encoding="utf-8").replace(
|
||||
js = _REDOC_JS_PATH.read_text(encoding="utf-8").replace(
|
||||
"</script", "<\\/script"
|
||||
)
|
||||
preset = _SWAGGER_PRESET_PATH.read_text(encoding="utf-8").replace(
|
||||
"</script", "<\\/script"
|
||||
)
|
||||
_docs_shell_cache = (
|
||||
_DOCS_PATH.read_text(encoding="utf-8")
|
||||
.replace(_CSS_PLACEHOLDER, css)
|
||||
.replace(_JS_PLACEHOLDER, js)
|
||||
.replace(_PRESET_PLACEHOLDER, preset)
|
||||
_docs_shell_cache = _DOCS_PATH.read_text(encoding="utf-8").replace(
|
||||
_JS_PLACEHOLDER, js
|
||||
)
|
||||
return _docs_shell_cache
|
||||
|
||||
|
||||
def _render_docs_html(event: dict) -> str:
|
||||
# "<" -> < so a spec string can never close the <script> block (same
|
||||
# value to JSON.parse); the spec object is inlined into SwaggerUIBundle.
|
||||
# value to JSON.parse); the spec object is inlined into Redoc.init.
|
||||
spec = _spec_for_request(event).replace("<", "\\u003c")
|
||||
return _docs_shell().replace(_SPEC_PLACEHOLDER, spec)
|
||||
|
||||
|
|
|
|||
1838
lambdas/api/redoc.standalone.js
Normal file
1838
lambdas/api/redoc.standalone.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -113,16 +113,12 @@ def test_docs_served_when_authenticated(api, monkeypatch):
|
|||
docs = mod.handler(_event("GET", "/docs"), None)
|
||||
assert docs["statusCode"] == 200
|
||||
assert docs["headers"]["Content-Type"].startswith("text/html")
|
||||
# Stock Swagger UI is inlined and the spec (carrying the title) with it.
|
||||
assert "SwaggerUIBundle" in docs["body"]
|
||||
# The vendored Redoc bundle is inlined and the spec (carrying the title)
|
||||
# with it.
|
||||
assert "Redoc.init" in docs["body"]
|
||||
assert "Procurement Ingest API" in docs["body"]
|
||||
# Every placeholder must be substituted, or the page renders broken.
|
||||
for placeholder in (
|
||||
mod._SPEC_PLACEHOLDER,
|
||||
mod._CSS_PLACEHOLDER,
|
||||
mod._JS_PLACEHOLDER,
|
||||
mod._PRESET_PLACEHOLDER,
|
||||
):
|
||||
for placeholder in (mod._SPEC_PLACEHOLDER, mod._JS_PLACEHOLDER):
|
||||
assert placeholder not in docs["body"]
|
||||
|
||||
spec = mod.handler(_event("GET", "/openapi.json"), None)
|
||||
|
|
|
|||
|
|
@ -106,29 +106,20 @@ def test_spec_has_no_script_breakout_sequence():
|
|||
|
||||
def test_docs_and_spec_files_ship_with_the_handler():
|
||||
# The handler serves these from its own package dir; if any file is missing
|
||||
# from lambdas/api/ the bundle would 500 at runtime. Swagger UI is vendored
|
||||
# from lambdas/api/ the bundle would 500 at runtime. Redoc is vendored
|
||||
# offline (no CDN) and inlined into the single token-gated response.
|
||||
api_dir = Path(REPO_ROOT) / "lambdas" / "api"
|
||||
assert (api_dir / "openapi.json").is_file()
|
||||
assert (api_dir / "docs.html").is_file()
|
||||
assert (api_dir / "swagger-ui-bundle.js").is_file()
|
||||
assert (api_dir / "swagger-ui-standalone-preset.js").is_file()
|
||||
assert (api_dir / "swagger-ui.css").is_file()
|
||||
assert (api_dir / "redoc.standalone.js").is_file()
|
||||
docs = (api_dir / "docs.html").read_text(encoding="utf-8")
|
||||
assert "__OPENAPI_SPEC_JSON__" in docs
|
||||
assert "__SWAGGER_UI_CSS__" in docs
|
||||
assert "__SWAGGER_UI_JS__" in docs
|
||||
assert "__SWAGGER_UI_STANDALONE_PRESET_JS__" in docs
|
||||
assert "SwaggerUIStandalonePreset" in docs
|
||||
assert "StandaloneLayout" in docs
|
||||
# The vendored Swagger UI blob must carry no raw </script>: it is inlined
|
||||
assert "__REDOC_JS__" in docs
|
||||
assert "Redoc.init" in docs
|
||||
# The vendored Redoc bundle must carry no raw </script>: it is inlined
|
||||
# into a <script> block, and a literal </script would break it out. (The
|
||||
# handler also escapes it defensively, but keeping the pinned asset clean
|
||||
# is the load-bearing guarantee and catches a bad version bump here.)
|
||||
assert "</script" not in (api_dir / "swagger-ui-bundle.js").read_text(
|
||||
assert "</script" not in (api_dir / "redoc.standalone.js").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "</script" not in (api_dir / "swagger-ui-standalone-preset.js").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "</style" not in (api_dir / "swagger-ui.css").read_text(encoding="utf-8")
|
||||
|
|
|
|||
|
|
@ -107,14 +107,12 @@ API_PIPELINE_MODULES = frozenset(
|
|||
}
|
||||
)
|
||||
# Non-.py files the api bundle must also EXECUTE cps for: the handler serves
|
||||
# the spec, docs page, and the vendored Swagger UI assets from its own package
|
||||
# the spec, docs page, and the vendored Redoc bundle from its own package
|
||||
# dir, so dropping any cp 500s /docs at runtime with green CI.
|
||||
API_DATA_FILES_CP_RES = (
|
||||
r"(?:^|\s)api/openapi\.json(?:\s|$)",
|
||||
r"(?:^|\s)api/docs\.html(?:\s|$)",
|
||||
r"(?:^|\s)api/swagger-ui-bundle\.js(?:\s|$)",
|
||||
r"(?:^|\s)api/swagger-ui-standalone-preset\.js(?:\s|$)",
|
||||
r"(?:^|\s)api/swagger-ui\.css(?:\s|$)",
|
||||
r"(?:^|\s)api/redoc\.standalone\.js(?:\s|$)",
|
||||
)
|
||||
|
||||
# Pipeline-scoped glob shapes the per-stack ships-all pins accept. Phase 2
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue