@wildo-ai/saas-technical-doc 1.1.1
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/LICENSE +34 -0
- package/dist/esm/.builder.pid +9 -0
- package/dist/esm/build/csp-emit.d.ts +9 -0
- package/dist/esm/build/csp-emit.d.ts.map +1 -0
- package/dist/esm/build/csp-emit.js +8 -0
- package/dist/esm/build/csp-emit.js.map +1 -0
- package/dist/esm/build/load-materialized-frontend-providers.d.ts +9 -0
- package/dist/esm/build/load-materialized-frontend-providers.d.ts.map +1 -0
- package/dist/esm/build/load-materialized-frontend-providers.js +9 -0
- package/dist/esm/build/load-materialized-frontend-providers.js.map +1 -0
- package/dist/esm/companion/application-documentation/application-organization-role-documentation.d.ts +66 -0
- package/dist/esm/companion/application-documentation/application-organization-role-documentation.d.ts.map +1 -0
- package/dist/esm/companion/application-documentation/application-organization-role-documentation.js +195 -0
- package/dist/esm/companion/application-documentation/application-organization-role-documentation.js.map +1 -0
- package/dist/esm/companion/application-documentation/application-organization-unit-resource-documentation.d.ts +36 -0
- package/dist/esm/companion/application-documentation/application-organization-unit-resource-documentation.d.ts.map +1 -0
- package/dist/esm/companion/application-documentation/application-organization-unit-resource-documentation.js +71 -0
- package/dist/esm/companion/application-documentation/application-organization-unit-resource-documentation.js.map +1 -0
- package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.d.ts +37 -0
- package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.d.ts.map +1 -0
- package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.js +1865 -0
- package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.js.map +1 -0
- package/dist/esm/companion/application-documentation/technical-documentation-operational-evidence.d.ts +22 -0
- package/dist/esm/companion/application-documentation/technical-documentation-operational-evidence.d.ts.map +1 -0
- package/dist/esm/companion/application-documentation/technical-documentation-operational-evidence.js +31 -0
- package/dist/esm/companion/application-documentation/technical-documentation-operational-evidence.js.map +1 -0
- package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.d.ts +31 -0
- package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.d.ts.map +1 -0
- package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.js +33 -0
- package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.js.map +1 -0
- package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.d.ts +13 -0
- package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.d.ts.map +1 -0
- package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.js +93 -0
- package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.js.map +1 -0
- package/dist/esm/companion/content/application-consumer-documentation-content-loader.d.ts +52 -0
- package/dist/esm/companion/content/application-consumer-documentation-content-loader.d.ts.map +1 -0
- package/dist/esm/companion/content/application-consumer-documentation-content-loader.js +191 -0
- package/dist/esm/companion/content/application-consumer-documentation-content-loader.js.map +1 -0
- package/dist/esm/companion/index.d.ts +39 -0
- package/dist/esm/companion/index.d.ts.map +1 -0
- package/dist/esm/companion/index.js +39 -0
- package/dist/esm/companion/index.js.map +1 -0
- package/dist/esm/companion/openapi-generator.d.ts +94 -0
- package/dist/esm/companion/openapi-generator.d.ts.map +1 -0
- package/dist/esm/companion/openapi-generator.js +1562 -0
- package/dist/esm/companion/openapi-generator.js.map +1 -0
- package/dist/esm/companion/operation-projection.schemas.d.ts +797 -0
- package/dist/esm/companion/operation-projection.schemas.d.ts.map +1 -0
- package/dist/esm/companion/operation-projection.schemas.js +610 -0
- package/dist/esm/companion/operation-projection.schemas.js.map +1 -0
- package/dist/esm/companion/publish-result.types.d.ts +124 -0
- package/dist/esm/companion/publish-result.types.d.ts.map +1 -0
- package/dist/esm/companion/publish-result.types.js +28 -0
- package/dist/esm/companion/publish-result.types.js.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-authorized-bundle-reader.d.ts +26 -0
- package/dist/esm/companion/rendering/technical-documentation-authorized-bundle-reader.d.ts.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-authorized-bundle-reader.js +63 -0
- package/dist/esm/companion/rendering/technical-documentation-authorized-bundle-reader.js.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-build-measurement.d.ts +23 -0
- package/dist/esm/companion/rendering/technical-documentation-build-measurement.d.ts.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-build-measurement.js +104 -0
- package/dist/esm/companion/rendering/technical-documentation-build-measurement.js.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.d.ts +9 -0
- package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.d.ts.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.js +421 -0
- package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.js.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.d.ts +72 -0
- package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.d.ts.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.js +204 -0
- package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.js.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.d.ts +9 -0
- package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.d.ts.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.js +106 -0
- package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.js.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-mdx-escaping.d.ts +43 -0
- package/dist/esm/companion/rendering/technical-documentation-mdx-escaping.d.ts.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-mdx-escaping.js +88 -0
- package/dist/esm/companion/rendering/technical-documentation-mdx-escaping.js.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-openapi-renderer.d.ts +7 -0
- package/dist/esm/companion/rendering/technical-documentation-openapi-renderer.d.ts.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-openapi-renderer.js +51 -0
- package/dist/esm/companion/rendering/technical-documentation-openapi-renderer.js.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-render-model.d.ts +45 -0
- package/dist/esm/companion/rendering/technical-documentation-render-model.d.ts.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-render-model.js +178 -0
- package/dist/esm/companion/rendering/technical-documentation-render-model.js.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.d.ts +3 -0
- package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.d.ts.map +1 -0
- package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.js +49 -0
- package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.js.map +1 -0
- package/dist/esm/companion/spec-to-operation-doc.d.ts +176 -0
- package/dist/esm/companion/spec-to-operation-doc.d.ts.map +1 -0
- package/dist/esm/companion/spec-to-operation-doc.js +326 -0
- package/dist/esm/companion/spec-to-operation-doc.js.map +1 -0
- package/dist/esm/companion/technical-documentation-asset-path.d.ts +16 -0
- package/dist/esm/companion/technical-documentation-asset-path.d.ts.map +1 -0
- package/dist/esm/companion/technical-documentation-asset-path.js +19 -0
- package/dist/esm/companion/technical-documentation-asset-path.js.map +1 -0
- package/dist/esm/companion/technical-documentation-capture-execution-port.d.ts +14 -0
- package/dist/esm/companion/technical-documentation-capture-execution-port.d.ts.map +1 -0
- package/dist/esm/companion/technical-documentation-capture-execution-port.js +1 -0
- package/dist/esm/companion/technical-documentation-capture-execution-port.js.map +1 -0
- package/dist/esm/companion/technical-documentation-diagram-materializer.d.ts +25 -0
- package/dist/esm/companion/technical-documentation-diagram-materializer.d.ts.map +1 -0
- package/dist/esm/companion/technical-documentation-diagram-materializer.js +86 -0
- package/dist/esm/companion/technical-documentation-diagram-materializer.js.map +1 -0
- package/dist/esm/companion/technical-documentation-placeholder-materializer.d.ts +17 -0
- package/dist/esm/companion/technical-documentation-placeholder-materializer.d.ts.map +1 -0
- package/dist/esm/companion/technical-documentation-placeholder-materializer.js +63 -0
- package/dist/esm/companion/technical-documentation-placeholder-materializer.js.map +1 -0
- package/dist/esm/companion/zod-to-openapi.d.ts +67 -0
- package/dist/esm/companion/zod-to-openapi.d.ts.map +1 -0
- package/dist/esm/companion/zod-to-openapi.js +211 -0
- package/dist/esm/companion/zod-to-openapi.js.map +1 -0
- package/dist/esm/companion-exports.d.ts +32 -0
- package/dist/esm/companion-exports.d.ts.map +1 -0
- package/dist/esm/companion-exports.js +32 -0
- package/dist/esm/companion-exports.js.map +1 -0
- package/dist/esm/config/define-tech-doc-config.d.ts +38 -0
- package/dist/esm/config/define-tech-doc-config.d.ts.map +1 -0
- package/dist/esm/config/define-tech-doc-config.js +40 -0
- package/dist/esm/config/define-tech-doc-config.js.map +1 -0
- package/dist/esm/config/index.d.ts +22 -0
- package/dist/esm/config/index.d.ts.map +1 -0
- package/dist/esm/config/index.js +22 -0
- package/dist/esm/config/index.js.map +1 -0
- package/dist/esm/config/wildo-tech-doc-config.schemas.d.ts +204 -0
- package/dist/esm/config/wildo-tech-doc-config.schemas.d.ts.map +1 -0
- package/dist/esm/config/wildo-tech-doc-config.schemas.js +192 -0
- package/dist/esm/config/wildo-tech-doc-config.schemas.js.map +1 -0
- package/dist/esm/content/application-consumer-documentation-content-manifest.schemas.d.ts +82 -0
- package/dist/esm/content/application-consumer-documentation-content-manifest.schemas.d.ts.map +1 -0
- package/dist/esm/content/application-consumer-documentation-content-manifest.schemas.js +114 -0
- package/dist/esm/content/application-consumer-documentation-content-manifest.schemas.js.map +1 -0
- package/dist/esm/content/application-consumer-documentation-content.techdoc.d.ts +420 -0
- package/dist/esm/content/application-consumer-documentation-content.techdoc.d.ts.map +1 -0
- package/dist/esm/content/application-consumer-documentation-content.techdoc.js +9619 -0
- package/dist/esm/content/application-consumer-documentation-content.techdoc.js.map +1 -0
- package/dist/esm/content.exports.d.ts +9 -0
- package/dist/esm/content.exports.d.ts.map +1 -0
- package/dist/esm/content.exports.js +9 -0
- package/dist/esm/content.exports.js.map +1 -0
- package/dist/esm/index.d.ts +26 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +26 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/openapi/api-reference-link-index.d.ts +98 -0
- package/dist/esm/openapi/api-reference-link-index.d.ts.map +1 -0
- package/dist/esm/openapi/api-reference-link-index.js +301 -0
- package/dist/esm/openapi/api-reference-link-index.js.map +1 -0
- package/dist/esm/openapi/api-reference-targets.d.ts +71 -0
- package/dist/esm/openapi/api-reference-targets.d.ts.map +1 -0
- package/dist/esm/openapi/api-reference-targets.js +114 -0
- package/dist/esm/openapi/api-reference-targets.js.map +1 -0
- package/dist/esm/openapi/index.d.ts +20 -0
- package/dist/esm/openapi/index.d.ts.map +1 -0
- package/dist/esm/openapi/index.js +20 -0
- package/dist/esm/openapi/index.js.map +1 -0
- package/dist/esm/openapi/openapi-generation-output.schemas.d.ts +80 -0
- package/dist/esm/openapi/openapi-generation-output.schemas.d.ts.map +1 -0
- package/dist/esm/openapi/openapi-generation-output.schemas.js +76 -0
- package/dist/esm/openapi/openapi-generation-output.schemas.js.map +1 -0
- package/dist/esm/openapi-reference-model.exports.d.ts +10 -0
- package/dist/esm/openapi-reference-model.exports.d.ts.map +1 -0
- package/dist/esm/openapi-reference-model.exports.js +10 -0
- package/dist/esm/openapi-reference-model.exports.js.map +1 -0
- package/dist/esm/runtime/AuthExchangePage.d.ts +84 -0
- package/dist/esm/runtime/AuthExchangePage.d.ts.map +1 -0
- package/dist/esm/runtime/AuthExchangePage.js +188 -0
- package/dist/esm/runtime/AuthExchangePage.js.map +1 -0
- package/dist/esm/runtime/DocsAuthContext.d.ts +119 -0
- package/dist/esm/runtime/DocsAuthContext.d.ts.map +1 -0
- package/dist/esm/runtime/DocsAuthContext.js +171 -0
- package/dist/esm/runtime/DocsAuthContext.js.map +1 -0
- package/dist/esm/runtime/decode-jwt-claims.d.ts +39 -0
- package/dist/esm/runtime/decode-jwt-claims.d.ts.map +1 -0
- package/dist/esm/runtime/decode-jwt-claims.js +86 -0
- package/dist/esm/runtime/decode-jwt-claims.js.map +1 -0
- package/dist/esm/runtime/docs-auth-client.d.ts +193 -0
- package/dist/esm/runtime/docs-auth-client.d.ts.map +1 -0
- package/dist/esm/runtime/docs-auth-client.js +211 -0
- package/dist/esm/runtime/docs-auth-client.js.map +1 -0
- package/dist/esm/runtime/docs-auth-session.schemas.d.ts +77 -0
- package/dist/esm/runtime/docs-auth-session.schemas.d.ts.map +1 -0
- package/dist/esm/runtime/docs-auth-session.schemas.js +50 -0
- package/dist/esm/runtime/docs-auth-session.schemas.js.map +1 -0
- package/dist/esm/runtime/frontend-provider-registry.techdoc.d.ts +17 -0
- package/dist/esm/runtime/frontend-provider-registry.techdoc.d.ts.map +1 -0
- package/dist/esm/runtime/frontend-provider-registry.techdoc.js +23 -0
- package/dist/esm/runtime/frontend-provider-registry.techdoc.js.map +1 -0
- package/dist/esm/runtime/index.d.ts +57 -0
- package/dist/esm/runtime/index.d.ts.map +1 -0
- package/dist/esm/runtime/index.js +76 -0
- package/dist/esm/runtime/index.js.map +1 -0
- package/dist/esm/runtime/openapi-reference-conservation.d.ts +20 -0
- package/dist/esm/runtime/openapi-reference-conservation.d.ts.map +1 -0
- package/dist/esm/runtime/openapi-reference-conservation.js +102 -0
- package/dist/esm/runtime/openapi-reference-conservation.js.map +1 -0
- package/dist/esm/runtime/openapi-reference-model.d.ts +224 -0
- package/dist/esm/runtime/openapi-reference-model.d.ts.map +1 -0
- package/dist/esm/runtime/openapi-reference-model.js +579 -0
- package/dist/esm/runtime/openapi-reference-model.js.map +1 -0
- package/dist/esm/runtime/openapi-reference-view.d.ts +13 -0
- package/dist/esm/runtime/openapi-reference-view.d.ts.map +1 -0
- package/dist/esm/runtime/openapi-reference-view.js +284 -0
- package/dist/esm/runtime/openapi-reference-view.js.map +1 -0
- package/dist/esm/runtime/use-docs-auth-session.d.ts +26 -0
- package/dist/esm/runtime/use-docs-auth-session.d.ts.map +1 -0
- package/dist/esm/runtime/use-docs-auth-session.js +34 -0
- package/dist/esm/runtime/use-docs-auth-session.js.map +1 -0
- package/dist/esm/runtime/use-docs-frontend-provider-registry.d.ts +3 -0
- package/dist/esm/runtime/use-docs-frontend-provider-registry.d.ts.map +1 -0
- package/dist/esm/runtime/use-docs-frontend-provider-registry.js +5 -0
- package/dist/esm/runtime/use-docs-frontend-provider-registry.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +117 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { DocsJwtClaimsSchema, DocsRole, } from './docs-auth-session.schemas.js';
|
|
2
|
+
/**
|
|
3
|
+
* Hand-rolled JWT-claims decoder for the docs-site auth runtime.
|
|
4
|
+
*
|
|
5
|
+
* Rationale (saas-technical-doc.md K-9b): the docs Docusaurus bundle is
|
|
6
|
+
* intentionally lean. Pulling in `jwt-decode` (or any general-purpose
|
|
7
|
+
* JWT library) for the SOLE purpose of base64-decoding the middle segment
|
|
8
|
+
* is a 30 KB+ bundle cost we refuse to pay for ~20 lines of code.
|
|
9
|
+
*
|
|
10
|
+
* SECURITY MODEL — important:
|
|
11
|
+
*
|
|
12
|
+
* This decoder DOES NOT verify the JWT signature. It cannot — the
|
|
13
|
+
* verification key lives on the backend, not in the browser bundle.
|
|
14
|
+
* The decoded claims are treated as DISPLAY HINTS ONLY: the docs
|
|
15
|
+
* navbar uses them to decide whether to render the "Organization API"
|
|
16
|
+
* link, and the DocItem swizzle uses them to decide whether to gate
|
|
17
|
+
* `/api/organization/*` pages.
|
|
18
|
+
*
|
|
19
|
+
* The REAL gating happens server-side: the OpenAPI YAMLs for the
|
|
20
|
+
* organization API are served by the backend behind the same
|
|
21
|
+
* authorization decorators as the underlying resources. A user who
|
|
22
|
+
* forges a JWT with `role: 'org_admin'` will see the docs navbar link
|
|
23
|
+
* appear, but every actual API call from those docs to the backend
|
|
24
|
+
* will still 401/403. The docs-site gating exists for UX, not security.
|
|
25
|
+
*
|
|
26
|
+
* This is the SAME contract the SaaS app's frontend has: it decodes
|
|
27
|
+
* the access-token claims for UI gating without verifying signatures,
|
|
28
|
+
* and the backend is the authoritative authorizer.
|
|
29
|
+
*
|
|
30
|
+
* Returns `null` for any malformed input — never throws. The session
|
|
31
|
+
* context (`DocsAuthContext`) treats `null` as "fall back to anonymous"
|
|
32
|
+
* rather than surfacing a UI error, because by the time we are decoding
|
|
33
|
+
* the token we have already accepted it from the `/auth/token/exchange`
|
|
34
|
+
* response and the only remaining failure modes are clock skew, library
|
|
35
|
+
* mis-issuance, or actual tampering — none of which yield a useful error
|
|
36
|
+
* message at the docs-site level.
|
|
37
|
+
*/
|
|
38
|
+
export function decodeJwtClaims(token) {
|
|
39
|
+
const segments = token.split('.');
|
|
40
|
+
if (segments.length !== 3)
|
|
41
|
+
return null;
|
|
42
|
+
const payloadSegment = segments[1];
|
|
43
|
+
if (!payloadSegment)
|
|
44
|
+
return null;
|
|
45
|
+
let parsed;
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(base64UrlDecode(payloadSegment));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
53
|
+
return null;
|
|
54
|
+
/**
|
|
55
|
+
* Normalize the raw role string → `DocsRole`.
|
|
56
|
+
*
|
|
57
|
+
* The access token's `role` claim can be any of the SaaS app's roles
|
|
58
|
+
* (`org_admin`, `org_member`, `app_user`, …); the docs site only cares
|
|
59
|
+
* about the org-admin / not-org-admin distinction (K-11).
|
|
60
|
+
*/
|
|
61
|
+
const raw = parsed;
|
|
62
|
+
const rawRole = typeof raw.role === 'string' ? raw.role : '';
|
|
63
|
+
const role = rawRole === DocsRole.ORG_ADMIN ? DocsRole.ORG_ADMIN : DocsRole.OTHER;
|
|
64
|
+
const candidate = {
|
|
65
|
+
userId: typeof raw.userId === 'string' ? raw.userId : raw.sub,
|
|
66
|
+
email: typeof raw.email === 'string' ? raw.email : undefined,
|
|
67
|
+
role,
|
|
68
|
+
exp: typeof raw.exp === 'number' ? raw.exp : NaN,
|
|
69
|
+
};
|
|
70
|
+
const result = DocsJwtClaimsSchema.safeParse(candidate);
|
|
71
|
+
return result.success ? result.data : null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Base64url → string. Pads the input to a multiple of 4 and translates
|
|
75
|
+
* `-`/`_` back to `+`/`/` before delegating to the platform's `atob`.
|
|
76
|
+
*
|
|
77
|
+
* `atob` is available in every browser and in Node ≥ 16 — both of which
|
|
78
|
+
* the docs site supports (Docusaurus build runs in Node, runtime in the
|
|
79
|
+
* browser).
|
|
80
|
+
*/
|
|
81
|
+
function base64UrlDecode(input) {
|
|
82
|
+
const padded = input + '='.repeat((4 - (input.length % 4)) % 4);
|
|
83
|
+
const base64 = padded.replace(/-/g, '+').replace(/_/g, '/');
|
|
84
|
+
return atob(base64);
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=decode-jwt-claims.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decode-jwt-claims.js","sourceRoot":"","sources":["../../../../src/runtime/decode-jwt-claims.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,QAAQ,GAET,MAAM,6BAA6B,CAAC;AAErC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvC,MAAM,cAAc,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IACnC,IAAI,CAAC,cAAc;QAAE,OAAO,IAAI,CAAC;IAEjC,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAE/D;;;;;;OAMG;IACH,MAAM,GAAG,GAAG,MAAiC,CAAC;IAC9C,MAAM,OAAO,GAAG,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7D,MAAM,IAAI,GAAG,OAAO,KAAK,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;IAElF,MAAM,SAAS,GAAG;QAChB,MAAM,EAAE,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG;QAC7D,KAAK,EAAE,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;QAC5D,IAAI;QACJ,GAAG,EAAE,OAAO,GAAG,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;KACjD,CAAC;IAEF,MAAM,MAAM,GAAG,mBAAmB,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IACxD,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7C,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,eAAe,CAAC,KAAa;IACpC,MAAM,MAAM,GAAG,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC5D,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC","sourcesContent":["import {\n DocsJwtClaimsSchema,\n DocsRole,\n type DocsJwtClaims,\n} from './docs-auth-session.schemas';\n\n/**\n * Hand-rolled JWT-claims decoder for the docs-site auth runtime.\n *\n * Rationale (saas-technical-doc.md K-9b): the docs Docusaurus bundle is\n * intentionally lean. Pulling in `jwt-decode` (or any general-purpose\n * JWT library) for the SOLE purpose of base64-decoding the middle segment\n * is a 30 KB+ bundle cost we refuse to pay for ~20 lines of code.\n *\n * SECURITY MODEL — important:\n *\n * This decoder DOES NOT verify the JWT signature. It cannot — the\n * verification key lives on the backend, not in the browser bundle.\n * The decoded claims are treated as DISPLAY HINTS ONLY: the docs\n * navbar uses them to decide whether to render the \"Organization API\"\n * link, and the DocItem swizzle uses them to decide whether to gate\n * `/api/organization/*` pages.\n *\n * The REAL gating happens server-side: the OpenAPI YAMLs for the\n * organization API are served by the backend behind the same\n * authorization decorators as the underlying resources. A user who\n * forges a JWT with `role: 'org_admin'` will see the docs navbar link\n * appear, but every actual API call from those docs to the backend\n * will still 401/403. The docs-site gating exists for UX, not security.\n *\n * This is the SAME contract the SaaS app's frontend has: it decodes\n * the access-token claims for UI gating without verifying signatures,\n * and the backend is the authoritative authorizer.\n *\n * Returns `null` for any malformed input — never throws. The session\n * context (`DocsAuthContext`) treats `null` as \"fall back to anonymous\"\n * rather than surfacing a UI error, because by the time we are decoding\n * the token we have already accepted it from the `/auth/token/exchange`\n * response and the only remaining failure modes are clock skew, library\n * mis-issuance, or actual tampering — none of which yield a useful error\n * message at the docs-site level.\n */\nexport function decodeJwtClaims(token: string): DocsJwtClaims | null {\n const segments = token.split('.');\n if (segments.length !== 3) return null;\n\n const payloadSegment = segments[1];\n if (!payloadSegment) return null;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(base64UrlDecode(payloadSegment));\n } catch {\n return null;\n }\n\n if (typeof parsed !== 'object' || parsed === null) return null;\n\n /**\n * Normalize the raw role string → `DocsRole`.\n *\n * The access token's `role` claim can be any of the SaaS app's roles\n * (`org_admin`, `org_member`, `app_user`, …); the docs site only cares\n * about the org-admin / not-org-admin distinction (K-11).\n */\n const raw = parsed as Record<string, unknown>;\n const rawRole = typeof raw.role === 'string' ? raw.role : '';\n const role = rawRole === DocsRole.ORG_ADMIN ? DocsRole.ORG_ADMIN : DocsRole.OTHER;\n\n const candidate = {\n userId: typeof raw.userId === 'string' ? raw.userId : raw.sub,\n email: typeof raw.email === 'string' ? raw.email : undefined,\n role,\n exp: typeof raw.exp === 'number' ? raw.exp : NaN,\n };\n\n const result = DocsJwtClaimsSchema.safeParse(candidate);\n return result.success ? result.data : null;\n}\n\n/**\n * Base64url → string. Pads the input to a multiple of 4 and translates\n * `-`/`_` back to `+`/`/` before delegating to the platform's `atob`.\n *\n * `atob` is available in every browser and in Node ≥ 16 — both of which\n * the docs site supports (Docusaurus build runs in Node, runtime in the\n * browser).\n */\nfunction base64UrlDecode(input: string): string {\n const padded = input + '='.repeat((4 - (input.length % 4)) % 4);\n const base64 = padded.replace(/-/g, '+').replace(/_/g, '/');\n return atob(base64);\n}\n"]}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Imports come from `@wildo-ai/saas-models/public-runtime` — NOT the root
|
|
3
|
+
* barrel (saas-models-public-runtime.md K-7).
|
|
4
|
+
*
|
|
5
|
+
* Why the subpath matters here: the root barrel pulls the full ~150-
|
|
6
|
+
* schema graph (resources, presets, business semantics, …) — none of
|
|
7
|
+
* which the docs Docusaurus bundle consumes. The `/public-runtime`
|
|
8
|
+
* subpath exposes just the handful of atoms the docs auth runtime
|
|
9
|
+
* actually reads (cross-frontend-handoff DTOs, token-exchange response).
|
|
10
|
+
*
|
|
11
|
+
* The Zod decorator augmentation itself (the augmented `ZodObject`
|
|
12
|
+
* interface authored by `@wildo-ai/zod-decorators`) IS booted by the
|
|
13
|
+
* docs runtime — see `runtime/index.ts`'s `ensureZodDecoratorsLoaded(z)`
|
|
14
|
+
* side-effect. That guarantees `z.infer<…>` results in this bundle
|
|
15
|
+
* match what the SaaS app sees (the cross-package agreement that H-3 /
|
|
16
|
+
* M-7 turned out to depend on).
|
|
17
|
+
*
|
|
18
|
+
* Do NOT change this import to the root barrel — the
|
|
19
|
+
* `engine/saas-technical-doc/__tests__/runtime-bundle-isolation.test.ts`
|
|
20
|
+
* boundary test will fail.
|
|
21
|
+
*/
|
|
22
|
+
import { CrossFrontendHandoffErrorCode, type CrossFrontendHandoffRequest, type CrossFrontendHandoffResponse, type TokenExchangeResponse } from '@wildo-ai/saas-models/public-runtime';
|
|
23
|
+
/**
|
|
24
|
+
* Lightweight, fetch-based HTTP client used by the docs Docusaurus runtime
|
|
25
|
+
* (saas-technical-doc.md K-9b).
|
|
26
|
+
*
|
|
27
|
+
* Knows EXACTLY two endpoints:
|
|
28
|
+
* - `POST /api/v1/auth/token/exchange` — consumes an
|
|
29
|
+
* `AUTH_CODE_EXCHANGE` code
|
|
30
|
+
* arriving on `?code=…`.
|
|
31
|
+
* - `POST /api/v1/auth/cross-frontend-handoff` — issues a one-time code for
|
|
32
|
+
* an already-authenticated docs
|
|
33
|
+
* session to move to another
|
|
34
|
+
* frontend.
|
|
35
|
+
*
|
|
36
|
+
* Deliberately does NOT include:
|
|
37
|
+
* - Axios / inversify / a resource registry (would balloon the bundle).
|
|
38
|
+
* - Request interceptors (no refresh loop here — see
|
|
39
|
+
* K-9b's "no refresh loop" rule).
|
|
40
|
+
* - Retries / backoff (the docs site is read-mostly;
|
|
41
|
+
* if a network call fails the
|
|
42
|
+
* session falls back to anonymous).
|
|
43
|
+
* - Any cookie / `withCredentials: true` (the platform is bearer-only;
|
|
44
|
+
* the cookie story belongs to
|
|
45
|
+
* auth-hardening.md Slice A
|
|
46
|
+
* which is `SAAS_APP`-scoped).
|
|
47
|
+
*
|
|
48
|
+
* SSR / build-time safety: the constructor does not touch `window`, `document`,
|
|
49
|
+
* `localStorage`, or `fetch`. `fetch` is only called inside the two methods,
|
|
50
|
+
* which are only invoked from React effects (always client-side under
|
|
51
|
+
* Docusaurus). This makes the class safe to import from a Docusaurus theme
|
|
52
|
+
* file that gets evaluated during the static build.
|
|
53
|
+
*/
|
|
54
|
+
export declare class DocsAuthClient {
|
|
55
|
+
private readonly apiBaseUrl;
|
|
56
|
+
private readonly getAuthBearer;
|
|
57
|
+
private readonly frontendServiceName;
|
|
58
|
+
constructor(args: {
|
|
59
|
+
/**
|
|
60
|
+
* Origin of the SaaS backend API (e.g. `https://app.example.com` or
|
|
61
|
+
* `https://api.example.com` depending on the app's ingress topology).
|
|
62
|
+
* This is a backend ORIGIN, not an `/api/v1` root. A trailing `/api/v1`
|
|
63
|
+
* is tolerated and normalized away to avoid double-prefixing.
|
|
64
|
+
*
|
|
65
|
+
* Authored by the docs site at runtime from `appConfig.runtime.endPoints
|
|
66
|
+
* .main_backend_api.publicUrl` — the same shape `saas-frontend-lib`
|
|
67
|
+
* reads. Trailing slashes are tolerated and stripped by the constructor.
|
|
68
|
+
*/
|
|
69
|
+
apiBaseUrl: string;
|
|
70
|
+
/**
|
|
71
|
+
* Returns the current access token to attach as `Authorization: Bearer …`
|
|
72
|
+
* on the cross-frontend-handoff request.
|
|
73
|
+
*
|
|
74
|
+
* The exchange endpoint (`/api/v1/auth/token/exchange`) does NOT require a
|
|
75
|
+
* bearer — the `code` itself is the credential — so this callback is
|
|
76
|
+
* only consulted by `requestCrossFrontendHandoff()`.
|
|
77
|
+
*
|
|
78
|
+
* Returning `null` signals "no session"; in that case
|
|
79
|
+
* `requestCrossFrontendHandoff` rejects synchronously with an
|
|
80
|
+
* `'unauthenticated'` error rather than calling the network. This
|
|
81
|
+
* matches the docs site's intended UX (the DocItem swizzle handles
|
|
82
|
+
* anonymous users by deep-linking the SaaS app's login, not by
|
|
83
|
+
* calling the handoff endpoint).
|
|
84
|
+
*/
|
|
85
|
+
getAuthBearer?: () => string | null;
|
|
86
|
+
/**
|
|
87
|
+
* Runtime frontend service identity. Required for redeeming handoff-issued
|
|
88
|
+
* AUTH_CODE_EXCHANGE codes because the backend binds each code to the
|
|
89
|
+
* intended target service at redemption time. Optional for tests and for
|
|
90
|
+
* non-handoff OAuth2-like exchanges.
|
|
91
|
+
*/
|
|
92
|
+
frontendServiceName?: string | null;
|
|
93
|
+
});
|
|
94
|
+
/**
|
|
95
|
+
* Consume an `AUTH_CODE_EXCHANGE` consumable token (60s TTL) and receive
|
|
96
|
+
* a fresh JWT pair plus the cross-frontend-handoff context fields.
|
|
97
|
+
*
|
|
98
|
+
* Called by `<AuthExchangePage>` when the user lands on
|
|
99
|
+
* `/auth/exchange?code=…&return=…` after a cross-frontend handoff.
|
|
100
|
+
*
|
|
101
|
+
* Returns the full `TokenExchangeResponseSchema` shape (sourced from
|
|
102
|
+
* `@wildo-ai/saas-models/public-runtime`) — including the optional
|
|
103
|
+
* top-level `returnPath`, `sourceFrontendServiceName`, and
|
|
104
|
+
* `targetServiceKey` fields populated by the issuer for handoff-issued
|
|
105
|
+
* codes (cross-app-jwt-propagation.md Step 5). These three fields live
|
|
106
|
+
* at the TOP LEVEL of the response — there is intentionally NO
|
|
107
|
+
* `metadata` envelope (the discriminator invariant pinned by
|
|
108
|
+
* `TokenExchangeResponseSchema` JSDoc says: presence of `returnPath`
|
|
109
|
+
* marks the response as a cross-frontend handoff vs. an OAuth2
|
|
110
|
+
* callback). The exchange page treats `returnPath` as the SOURCE OF
|
|
111
|
+
* TRUTH for navigation — the URL `?return=` value is kept only as a
|
|
112
|
+
* defense-in-depth fallback when the response omits it.
|
|
113
|
+
*
|
|
114
|
+
* Maps the backend's stable `AUTHENTICATION_INVALID_TOKEN` error onto
|
|
115
|
+
* a `DocsAuthClientError` with `code: 'invalid_or_expired_code'` so the
|
|
116
|
+
* exchange page can render a clean "this link expired — please re-open
|
|
117
|
+
* docs from the app" message without leaking backend wording.
|
|
118
|
+
*
|
|
119
|
+
* Bundle note: `TokenExchangeResponseSchema.safeParse` is bundle-safe
|
|
120
|
+
* because the schema is exported from `@wildo-ai/saas-models/public-runtime`,
|
|
121
|
+
* which is already on the runtime allow-list — see
|
|
122
|
+
* `runtime-bundle-isolation.test.ts`.
|
|
123
|
+
*/
|
|
124
|
+
exchangeAuthCode(code: string): Promise<TokenExchangeResponse>;
|
|
125
|
+
/**
|
|
126
|
+
* Ask the SaaS backend to issue a one-time code that hands the current
|
|
127
|
+
* session to a target frontend service (typically the SaaS app itself).
|
|
128
|
+
*
|
|
129
|
+
* This method is only usable when the docs tab already holds an access
|
|
130
|
+
* token. Anonymous docs visitors cannot call the endpoint; the Wonder Todos
|
|
131
|
+
* swizzle deep-links them to the SaaS app instead, where login/session
|
|
132
|
+
* ownership lives.
|
|
133
|
+
*
|
|
134
|
+
* The error surface mirrors the backend's `CrossFrontendHandoffErrorCode`
|
|
135
|
+
* enum 1:1 — the swizzle can branch on `error.code` to render the right
|
|
136
|
+
* "configuration is wrong, contact support" message
|
|
137
|
+
* (UNKNOWN_TARGET_FRONTEND, TARGET_FRONTEND_HOST_NOT_CONFIGURED).
|
|
138
|
+
*/
|
|
139
|
+
requestCrossFrontendHandoff(request: CrossFrontendHandoffRequest): Promise<CrossFrontendHandoffResponse>;
|
|
140
|
+
/**
|
|
141
|
+
* Best-effort parse of the backend's `{ error: { details: { code, … } } }`
|
|
142
|
+
* envelope.
|
|
143
|
+
*
|
|
144
|
+
* The path is `error.details.code` (NOT `error.code`) — see the
|
|
145
|
+
* `error-handler.backend.service.ts` response shape, which spreads
|
|
146
|
+
* `sanitizedError.context` under `details`. The error builder's
|
|
147
|
+
* sanitization allowlist explicitly promotes `code` so cross-frontend
|
|
148
|
+
* handoff failures expose the stable `CrossFrontendHandoffErrorCode`
|
|
149
|
+
* enum value (cross-app-jwt-propagation.md C-4 contract).
|
|
150
|
+
*
|
|
151
|
+
* Falls back to `'malformed_response'` when the body is missing or its
|
|
152
|
+
* `error.details.code` is not a known `CrossFrontendHandoffErrorCode`.
|
|
153
|
+
* This keeps the client side strictly typed (`DocsAuthClientErrorCode`)
|
|
154
|
+
* even when faced with a transitional / proxy / CDN response that does
|
|
155
|
+
* not match the platform envelope.
|
|
156
|
+
*/
|
|
157
|
+
private parseHandoffErrorCode;
|
|
158
|
+
private joinUrl;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Stable, finite set of error codes the runtime can surface to the
|
|
162
|
+
* Docusaurus swizzles + `<AuthExchangePage>`.
|
|
163
|
+
*
|
|
164
|
+
* Mirrors `CrossFrontendHandoffErrorCode` for the handoff path PLUS three
|
|
165
|
+
* runtime-only codes:
|
|
166
|
+
*
|
|
167
|
+
* - `unauthenticated` — the caller asked for a handoff while no
|
|
168
|
+
* session was loaded; surfaced WITHOUT a
|
|
169
|
+
* network call so the DocItem swizzle can
|
|
170
|
+
* redirect to the SaaS app's login instead.
|
|
171
|
+
* - `invalid_or_expired_code` — the `/api/v1/auth/token/exchange` call returned
|
|
172
|
+
* non-200; covers expired, already-consumed,
|
|
173
|
+
* or otherwise rejected codes.
|
|
174
|
+
* - `malformed_response` — a transport / CDN / parser hiccup; rare,
|
|
175
|
+
* but typed so the swizzle never has to
|
|
176
|
+
* handle "untyped error" branches.
|
|
177
|
+
*/
|
|
178
|
+
export type DocsAuthClientErrorCode = CrossFrontendHandoffErrorCode | 'unauthenticated' | 'invalid_or_expired_code' | 'malformed_response';
|
|
179
|
+
/**
|
|
180
|
+
* Typed error thrown by every `DocsAuthClient` method.
|
|
181
|
+
*
|
|
182
|
+
* Subclasses native `Error` (not `WildoBackendError`) on purpose — pulling
|
|
183
|
+
* `WildoBackendError` here would force a dependency on `@wildo-ai/saas-models`
|
|
184
|
+
* runtime utilities that the docs bundle has no other use for. The
|
|
185
|
+
* swizzles `instanceof DocsAuthClientError` to distinguish from generic
|
|
186
|
+
* fetch/abort errors.
|
|
187
|
+
*/
|
|
188
|
+
export declare class DocsAuthClientError extends Error {
|
|
189
|
+
readonly code: DocsAuthClientErrorCode;
|
|
190
|
+
readonly httpStatus: number;
|
|
191
|
+
constructor(code: DocsAuthClientErrorCode, message: string, httpStatus: number);
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=docs-auth-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docs-auth-client.d.ts","sourceRoot":"","sources":["../../../../src/runtime/docs-auth-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EACL,6BAA6B,EAK7B,KAAK,2BAA2B,EAChC,KAAK,4BAA4B,EACjC,KAAK,qBAAqB,EAC3B,MAAM,sCAAsC,CAAC;AAM9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAsB;IACpD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAgB;gBAExC,IAAI,EAAE;QAChB;;;;;;;;;WASG;QACH,UAAU,EAAE,MAAM,CAAC;QAEnB;;;;;;;;;;;;;;WAcG;QACH,aAAa,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;QACpC;;;;;WAKG;QACH,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACrC;IAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACG,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IA+BpE;;;;;;;;;;;;;OAaG;IACG,2BAA2B,CAC/B,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,4BAA4B,CAAC;IAwCxC;;;;;;;;;;;;;;;;OAgBG;YACW,qBAAqB;IAkBnC,OAAO,CAAC,OAAO;CAGhB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,uBAAuB,GAC/B,6BAA6B,GAC7B,iBAAiB,GACjB,yBAAyB,GACzB,oBAAoB,CAAC;AAEzB;;;;;;;;GAQG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,IAAI,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;CAM/E"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Imports come from `@wildo-ai/saas-models/public-runtime` — NOT the root
|
|
3
|
+
* barrel (saas-models-public-runtime.md K-7).
|
|
4
|
+
*
|
|
5
|
+
* Why the subpath matters here: the root barrel pulls the full ~150-
|
|
6
|
+
* schema graph (resources, presets, business semantics, …) — none of
|
|
7
|
+
* which the docs Docusaurus bundle consumes. The `/public-runtime`
|
|
8
|
+
* subpath exposes just the handful of atoms the docs auth runtime
|
|
9
|
+
* actually reads (cross-frontend-handoff DTOs, token-exchange response).
|
|
10
|
+
*
|
|
11
|
+
* The Zod decorator augmentation itself (the augmented `ZodObject`
|
|
12
|
+
* interface authored by `@wildo-ai/zod-decorators`) IS booted by the
|
|
13
|
+
* docs runtime — see `runtime/index.ts`'s `ensureZodDecoratorsLoaded(z)`
|
|
14
|
+
* side-effect. That guarantees `z.infer<…>` results in this bundle
|
|
15
|
+
* match what the SaaS app sees (the cross-package agreement that H-3 /
|
|
16
|
+
* M-7 turned out to depend on).
|
|
17
|
+
*
|
|
18
|
+
* Do NOT change this import to the root barrel — the
|
|
19
|
+
* `engine/saas-technical-doc/__tests__/runtime-bundle-isolation.test.ts`
|
|
20
|
+
* boundary test will fail.
|
|
21
|
+
*/
|
|
22
|
+
import { CrossFrontendHandoffErrorCode, MAIN_API_BASE_PATH, TokenExchangeRequestSchema, TokenExchangeResponseSchema, WildoHeaderKeys, } from '@wildo-ai/saas-models/public-runtime';
|
|
23
|
+
const MANUAL_API_PREFIX = MAIN_API_BASE_PATH;
|
|
24
|
+
const TOKEN_EXCHANGE_PATH = `${MANUAL_API_PREFIX}/auth/token/exchange`;
|
|
25
|
+
const CROSS_FRONTEND_HANDOFF_PATH = `${MANUAL_API_PREFIX}/auth/cross-frontend-handoff`;
|
|
26
|
+
/**
|
|
27
|
+
* Lightweight, fetch-based HTTP client used by the docs Docusaurus runtime
|
|
28
|
+
* (saas-technical-doc.md K-9b).
|
|
29
|
+
*
|
|
30
|
+
* Knows EXACTLY two endpoints:
|
|
31
|
+
* - `POST /api/v1/auth/token/exchange` — consumes an
|
|
32
|
+
* `AUTH_CODE_EXCHANGE` code
|
|
33
|
+
* arriving on `?code=…`.
|
|
34
|
+
* - `POST /api/v1/auth/cross-frontend-handoff` — issues a one-time code for
|
|
35
|
+
* an already-authenticated docs
|
|
36
|
+
* session to move to another
|
|
37
|
+
* frontend.
|
|
38
|
+
*
|
|
39
|
+
* Deliberately does NOT include:
|
|
40
|
+
* - Axios / inversify / a resource registry (would balloon the bundle).
|
|
41
|
+
* - Request interceptors (no refresh loop here — see
|
|
42
|
+
* K-9b's "no refresh loop" rule).
|
|
43
|
+
* - Retries / backoff (the docs site is read-mostly;
|
|
44
|
+
* if a network call fails the
|
|
45
|
+
* session falls back to anonymous).
|
|
46
|
+
* - Any cookie / `withCredentials: true` (the platform is bearer-only;
|
|
47
|
+
* the cookie story belongs to
|
|
48
|
+
* auth-hardening.md Slice A
|
|
49
|
+
* which is `SAAS_APP`-scoped).
|
|
50
|
+
*
|
|
51
|
+
* SSR / build-time safety: the constructor does not touch `window`, `document`,
|
|
52
|
+
* `localStorage`, or `fetch`. `fetch` is only called inside the two methods,
|
|
53
|
+
* which are only invoked from React effects (always client-side under
|
|
54
|
+
* Docusaurus). This makes the class safe to import from a Docusaurus theme
|
|
55
|
+
* file that gets evaluated during the static build.
|
|
56
|
+
*/
|
|
57
|
+
export class DocsAuthClient {
|
|
58
|
+
apiBaseUrl;
|
|
59
|
+
getAuthBearer;
|
|
60
|
+
frontendServiceName;
|
|
61
|
+
constructor(args) {
|
|
62
|
+
this.apiBaseUrl = args.apiBaseUrl.replace(/\/+$/, '').replace(/\/api\/v1$/, '');
|
|
63
|
+
this.getAuthBearer = args.getAuthBearer ?? (() => null);
|
|
64
|
+
this.frontendServiceName = args.frontendServiceName ?? null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Consume an `AUTH_CODE_EXCHANGE` consumable token (60s TTL) and receive
|
|
68
|
+
* a fresh JWT pair plus the cross-frontend-handoff context fields.
|
|
69
|
+
*
|
|
70
|
+
* Called by `<AuthExchangePage>` when the user lands on
|
|
71
|
+
* `/auth/exchange?code=…&return=…` after a cross-frontend handoff.
|
|
72
|
+
*
|
|
73
|
+
* Returns the full `TokenExchangeResponseSchema` shape (sourced from
|
|
74
|
+
* `@wildo-ai/saas-models/public-runtime`) — including the optional
|
|
75
|
+
* top-level `returnPath`, `sourceFrontendServiceName`, and
|
|
76
|
+
* `targetServiceKey` fields populated by the issuer for handoff-issued
|
|
77
|
+
* codes (cross-app-jwt-propagation.md Step 5). These three fields live
|
|
78
|
+
* at the TOP LEVEL of the response — there is intentionally NO
|
|
79
|
+
* `metadata` envelope (the discriminator invariant pinned by
|
|
80
|
+
* `TokenExchangeResponseSchema` JSDoc says: presence of `returnPath`
|
|
81
|
+
* marks the response as a cross-frontend handoff vs. an OAuth2
|
|
82
|
+
* callback). The exchange page treats `returnPath` as the SOURCE OF
|
|
83
|
+
* TRUTH for navigation — the URL `?return=` value is kept only as a
|
|
84
|
+
* defense-in-depth fallback when the response omits it.
|
|
85
|
+
*
|
|
86
|
+
* Maps the backend's stable `AUTHENTICATION_INVALID_TOKEN` error onto
|
|
87
|
+
* a `DocsAuthClientError` with `code: 'invalid_or_expired_code'` so the
|
|
88
|
+
* exchange page can render a clean "this link expired — please re-open
|
|
89
|
+
* docs from the app" message without leaking backend wording.
|
|
90
|
+
*
|
|
91
|
+
* Bundle note: `TokenExchangeResponseSchema.safeParse` is bundle-safe
|
|
92
|
+
* because the schema is exported from `@wildo-ai/saas-models/public-runtime`,
|
|
93
|
+
* which is already on the runtime allow-list — see
|
|
94
|
+
* `runtime-bundle-isolation.test.ts`.
|
|
95
|
+
*/
|
|
96
|
+
async exchangeAuthCode(code) {
|
|
97
|
+
const headers = { 'content-type': 'application/json' };
|
|
98
|
+
if (this.frontendServiceName) {
|
|
99
|
+
headers[WildoHeaderKeys.FRONTEND_SERVICE_NAME] = this.frontendServiceName;
|
|
100
|
+
}
|
|
101
|
+
const response = await fetch(this.joinUrl(TOKEN_EXCHANGE_PATH), {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers,
|
|
104
|
+
body: JSON.stringify(TokenExchangeRequestSchema.parse({ code })),
|
|
105
|
+
});
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
throw new DocsAuthClientError('invalid_or_expired_code', `Token exchange failed (HTTP ${response.status})`, response.status);
|
|
108
|
+
}
|
|
109
|
+
const parsed = TokenExchangeResponseSchema.safeParse(await response.json());
|
|
110
|
+
if (!parsed.success) {
|
|
111
|
+
throw new DocsAuthClientError('invalid_or_expired_code', 'Token exchange response failed schema validation', response.status);
|
|
112
|
+
}
|
|
113
|
+
return parsed.data;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Ask the SaaS backend to issue a one-time code that hands the current
|
|
117
|
+
* session to a target frontend service (typically the SaaS app itself).
|
|
118
|
+
*
|
|
119
|
+
* This method is only usable when the docs tab already holds an access
|
|
120
|
+
* token. Anonymous docs visitors cannot call the endpoint; the Wonder Todos
|
|
121
|
+
* swizzle deep-links them to the SaaS app instead, where login/session
|
|
122
|
+
* ownership lives.
|
|
123
|
+
*
|
|
124
|
+
* The error surface mirrors the backend's `CrossFrontendHandoffErrorCode`
|
|
125
|
+
* enum 1:1 — the swizzle can branch on `error.code` to render the right
|
|
126
|
+
* "configuration is wrong, contact support" message
|
|
127
|
+
* (UNKNOWN_TARGET_FRONTEND, TARGET_FRONTEND_HOST_NOT_CONFIGURED).
|
|
128
|
+
*/
|
|
129
|
+
async requestCrossFrontendHandoff(request) {
|
|
130
|
+
const bearer = this.getAuthBearer();
|
|
131
|
+
if (!bearer) {
|
|
132
|
+
throw new DocsAuthClientError('unauthenticated', 'Cross-frontend handoff requires an authenticated docs session', 401);
|
|
133
|
+
}
|
|
134
|
+
const response = await fetch(this.joinUrl(CROSS_FRONTEND_HANDOFF_PATH), {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: {
|
|
137
|
+
'content-type': 'application/json',
|
|
138
|
+
authorization: `Bearer ${bearer}`,
|
|
139
|
+
...(this.frontendServiceName ? { [WildoHeaderKeys.FRONTEND_SERVICE_NAME]: this.frontendServiceName } : {}),
|
|
140
|
+
},
|
|
141
|
+
body: JSON.stringify(request),
|
|
142
|
+
});
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
const code = await this.parseHandoffErrorCode(response);
|
|
145
|
+
throw new DocsAuthClientError(code, `Cross-frontend handoff failed (HTTP ${response.status})`, response.status);
|
|
146
|
+
}
|
|
147
|
+
const json = (await response.json());
|
|
148
|
+
if (typeof json.handoffUrl !== 'string' || typeof json.expiresAt !== 'string') {
|
|
149
|
+
throw new DocsAuthClientError('malformed_response', 'Cross-frontend handoff response missing required fields', response.status);
|
|
150
|
+
}
|
|
151
|
+
return { handoffUrl: json.handoffUrl, expiresAt: json.expiresAt };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Best-effort parse of the backend's `{ error: { details: { code, … } } }`
|
|
155
|
+
* envelope.
|
|
156
|
+
*
|
|
157
|
+
* The path is `error.details.code` (NOT `error.code`) — see the
|
|
158
|
+
* `error-handler.backend.service.ts` response shape, which spreads
|
|
159
|
+
* `sanitizedError.context` under `details`. The error builder's
|
|
160
|
+
* sanitization allowlist explicitly promotes `code` so cross-frontend
|
|
161
|
+
* handoff failures expose the stable `CrossFrontendHandoffErrorCode`
|
|
162
|
+
* enum value (cross-app-jwt-propagation.md C-4 contract).
|
|
163
|
+
*
|
|
164
|
+
* Falls back to `'malformed_response'` when the body is missing or its
|
|
165
|
+
* `error.details.code` is not a known `CrossFrontendHandoffErrorCode`.
|
|
166
|
+
* This keeps the client side strictly typed (`DocsAuthClientErrorCode`)
|
|
167
|
+
* even when faced with a transitional / proxy / CDN response that does
|
|
168
|
+
* not match the platform envelope.
|
|
169
|
+
*/
|
|
170
|
+
async parseHandoffErrorCode(response) {
|
|
171
|
+
let body;
|
|
172
|
+
try {
|
|
173
|
+
body = await response.json();
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return 'malformed_response';
|
|
177
|
+
}
|
|
178
|
+
const code = body
|
|
179
|
+
?.error?.details?.code;
|
|
180
|
+
if (typeof code !== 'string')
|
|
181
|
+
return 'malformed_response';
|
|
182
|
+
const handoffCodes = Object.values(CrossFrontendHandoffErrorCode);
|
|
183
|
+
if (handoffCodes.includes(code)) {
|
|
184
|
+
return code;
|
|
185
|
+
}
|
|
186
|
+
return 'malformed_response';
|
|
187
|
+
}
|
|
188
|
+
joinUrl(path) {
|
|
189
|
+
return `${this.apiBaseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Typed error thrown by every `DocsAuthClient` method.
|
|
194
|
+
*
|
|
195
|
+
* Subclasses native `Error` (not `WildoBackendError`) on purpose — pulling
|
|
196
|
+
* `WildoBackendError` here would force a dependency on `@wildo-ai/saas-models`
|
|
197
|
+
* runtime utilities that the docs bundle has no other use for. The
|
|
198
|
+
* swizzles `instanceof DocsAuthClientError` to distinguish from generic
|
|
199
|
+
* fetch/abort errors.
|
|
200
|
+
*/
|
|
201
|
+
export class DocsAuthClientError extends Error {
|
|
202
|
+
code;
|
|
203
|
+
httpStatus;
|
|
204
|
+
constructor(code, message, httpStatus) {
|
|
205
|
+
super(message);
|
|
206
|
+
this.name = 'DocsAuthClientError';
|
|
207
|
+
this.code = code;
|
|
208
|
+
this.httpStatus = httpStatus;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=docs-auth-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docs-auth-client.js","sourceRoot":"","sources":["../../../../src/runtime/docs-auth-client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Imports come from `@wildo-ai/saas-models/public-runtime` — NOT the root\n * barrel (saas-models-public-runtime.md K-7).\n *\n * Why the subpath matters here: the root barrel pulls the full ~150-\n * schema graph (resources, presets, business semantics, …) — none of\n * which the docs Docusaurus bundle consumes. The `/public-runtime`\n * subpath exposes just the handful of atoms the docs auth runtime\n * actually reads (cross-frontend-handoff DTOs, token-exchange response).\n *\n * The Zod decorator augmentation itself (the augmented `ZodObject`\n * interface authored by `@wildo-ai/zod-decorators`) IS booted by the\n * docs runtime — see `runtime/index.ts`'s `ensureZodDecoratorsLoaded(z)`\n * side-effect. That guarantees `z.infer<…>` results in this bundle\n * match what the SaaS app sees (the cross-package agreement that H-3 /\n * M-7 turned out to depend on).\n *\n * Do NOT change this import to the root barrel — the\n * `engine/saas-technical-doc/__tests__/runtime-bundle-isolation.test.ts`\n * boundary test will fail.\n */\nimport {\n CrossFrontendHandoffErrorCode,\n MAIN_API_BASE_PATH,\n TokenExchangeRequestSchema,\n TokenExchangeResponseSchema,\n WildoHeaderKeys,\n type CrossFrontendHandoffRequest,\n type CrossFrontendHandoffResponse,\n type TokenExchangeResponse,\n} from '@wildo-ai/saas-models/public-runtime';\n\nconst MANUAL_API_PREFIX = MAIN_API_BASE_PATH;\nconst TOKEN_EXCHANGE_PATH = `${MANUAL_API_PREFIX}/auth/token/exchange`;\nconst CROSS_FRONTEND_HANDOFF_PATH = `${MANUAL_API_PREFIX}/auth/cross-frontend-handoff`;\n\n/**\n * Lightweight, fetch-based HTTP client used by the docs Docusaurus runtime\n * (saas-technical-doc.md K-9b).\n *\n * Knows EXACTLY two endpoints:\n * - `POST /api/v1/auth/token/exchange` — consumes an\n * `AUTH_CODE_EXCHANGE` code\n * arriving on `?code=…`.\n * - `POST /api/v1/auth/cross-frontend-handoff` — issues a one-time code for\n * an already-authenticated docs\n * session to move to another\n * frontend.\n *\n * Deliberately does NOT include:\n * - Axios / inversify / a resource registry (would balloon the bundle).\n * - Request interceptors (no refresh loop here — see\n * K-9b's \"no refresh loop\" rule).\n * - Retries / backoff (the docs site is read-mostly;\n * if a network call fails the\n * session falls back to anonymous).\n * - Any cookie / `withCredentials: true` (the platform is bearer-only;\n * the cookie story belongs to\n * auth-hardening.md Slice A\n * which is `SAAS_APP`-scoped).\n *\n * SSR / build-time safety: the constructor does not touch `window`, `document`,\n * `localStorage`, or `fetch`. `fetch` is only called inside the two methods,\n * which are only invoked from React effects (always client-side under\n * Docusaurus). This makes the class safe to import from a Docusaurus theme\n * file that gets evaluated during the static build.\n */\nexport class DocsAuthClient {\n private readonly apiBaseUrl: string;\n private readonly getAuthBearer: () => string | null;\n private readonly frontendServiceName: string | null;\n\n constructor(args: {\n /**\n * Origin of the SaaS backend API (e.g. `https://app.example.com` or\n * `https://api.example.com` depending on the app's ingress topology).\n * This is a backend ORIGIN, not an `/api/v1` root. A trailing `/api/v1`\n * is tolerated and normalized away to avoid double-prefixing.\n *\n * Authored by the docs site at runtime from `appConfig.runtime.endPoints\n * .main_backend_api.publicUrl` — the same shape `saas-frontend-lib`\n * reads. Trailing slashes are tolerated and stripped by the constructor.\n */\n apiBaseUrl: string;\n\n /**\n * Returns the current access token to attach as `Authorization: Bearer …`\n * on the cross-frontend-handoff request.\n *\n * The exchange endpoint (`/api/v1/auth/token/exchange`) does NOT require a\n * bearer — the `code` itself is the credential — so this callback is\n * only consulted by `requestCrossFrontendHandoff()`.\n *\n * Returning `null` signals \"no session\"; in that case\n * `requestCrossFrontendHandoff` rejects synchronously with an\n * `'unauthenticated'` error rather than calling the network. This\n * matches the docs site's intended UX (the DocItem swizzle handles\n * anonymous users by deep-linking the SaaS app's login, not by\n * calling the handoff endpoint).\n */\n getAuthBearer?: () => string | null;\n /**\n * Runtime frontend service identity. Required for redeeming handoff-issued\n * AUTH_CODE_EXCHANGE codes because the backend binds each code to the\n * intended target service at redemption time. Optional for tests and for\n * non-handoff OAuth2-like exchanges.\n */\n frontendServiceName?: string | null;\n }) {\n this.apiBaseUrl = args.apiBaseUrl.replace(/\\/+$/, '').replace(/\\/api\\/v1$/, '');\n this.getAuthBearer = args.getAuthBearer ?? (() => null);\n this.frontendServiceName = args.frontendServiceName ?? null;\n }\n\n /**\n * Consume an `AUTH_CODE_EXCHANGE` consumable token (60s TTL) and receive\n * a fresh JWT pair plus the cross-frontend-handoff context fields.\n *\n * Called by `<AuthExchangePage>` when the user lands on\n * `/auth/exchange?code=…&return=…` after a cross-frontend handoff.\n *\n * Returns the full `TokenExchangeResponseSchema` shape (sourced from\n * `@wildo-ai/saas-models/public-runtime`) — including the optional\n * top-level `returnPath`, `sourceFrontendServiceName`, and\n * `targetServiceKey` fields populated by the issuer for handoff-issued\n * codes (cross-app-jwt-propagation.md Step 5). These three fields live\n * at the TOP LEVEL of the response — there is intentionally NO\n * `metadata` envelope (the discriminator invariant pinned by\n * `TokenExchangeResponseSchema` JSDoc says: presence of `returnPath`\n * marks the response as a cross-frontend handoff vs. an OAuth2\n * callback). The exchange page treats `returnPath` as the SOURCE OF\n * TRUTH for navigation — the URL `?return=` value is kept only as a\n * defense-in-depth fallback when the response omits it.\n *\n * Maps the backend's stable `AUTHENTICATION_INVALID_TOKEN` error onto\n * a `DocsAuthClientError` with `code: 'invalid_or_expired_code'` so the\n * exchange page can render a clean \"this link expired — please re-open\n * docs from the app\" message without leaking backend wording.\n *\n * Bundle note: `TokenExchangeResponseSchema.safeParse` is bundle-safe\n * because the schema is exported from `@wildo-ai/saas-models/public-runtime`,\n * which is already on the runtime allow-list — see\n * `runtime-bundle-isolation.test.ts`.\n */\n async exchangeAuthCode(code: string): Promise<TokenExchangeResponse> {\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n if (this.frontendServiceName) {\n headers[WildoHeaderKeys.FRONTEND_SERVICE_NAME] = this.frontendServiceName;\n }\n\n const response = await fetch(this.joinUrl(TOKEN_EXCHANGE_PATH), {\n method: 'POST',\n headers,\n body: JSON.stringify(TokenExchangeRequestSchema.parse({ code })),\n });\n\n if (!response.ok) {\n throw new DocsAuthClientError(\n 'invalid_or_expired_code',\n `Token exchange failed (HTTP ${response.status})`,\n response.status,\n );\n }\n\n const parsed = TokenExchangeResponseSchema.safeParse(await response.json());\n if (!parsed.success) {\n throw new DocsAuthClientError(\n 'invalid_or_expired_code',\n 'Token exchange response failed schema validation',\n response.status,\n );\n }\n return parsed.data;\n }\n\n /**\n * Ask the SaaS backend to issue a one-time code that hands the current\n * session to a target frontend service (typically the SaaS app itself).\n *\n * This method is only usable when the docs tab already holds an access\n * token. Anonymous docs visitors cannot call the endpoint; the Wonder Todos\n * swizzle deep-links them to the SaaS app instead, where login/session\n * ownership lives.\n *\n * The error surface mirrors the backend's `CrossFrontendHandoffErrorCode`\n * enum 1:1 — the swizzle can branch on `error.code` to render the right\n * \"configuration is wrong, contact support\" message\n * (UNKNOWN_TARGET_FRONTEND, TARGET_FRONTEND_HOST_NOT_CONFIGURED).\n */\n async requestCrossFrontendHandoff(\n request: CrossFrontendHandoffRequest,\n ): Promise<CrossFrontendHandoffResponse> {\n const bearer = this.getAuthBearer();\n if (!bearer) {\n throw new DocsAuthClientError(\n 'unauthenticated',\n 'Cross-frontend handoff requires an authenticated docs session',\n 401,\n );\n }\n\n const response = await fetch(this.joinUrl(CROSS_FRONTEND_HANDOFF_PATH), {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${bearer}`,\n ...(this.frontendServiceName ? { [WildoHeaderKeys.FRONTEND_SERVICE_NAME]: this.frontendServiceName } : {}),\n },\n body: JSON.stringify(request),\n });\n\n if (!response.ok) {\n const code = await this.parseHandoffErrorCode(response);\n throw new DocsAuthClientError(\n code,\n `Cross-frontend handoff failed (HTTP ${response.status})`,\n response.status,\n );\n }\n\n const json = (await response.json()) as Partial<CrossFrontendHandoffResponse>;\n if (typeof json.handoffUrl !== 'string' || typeof json.expiresAt !== 'string') {\n throw new DocsAuthClientError(\n 'malformed_response',\n 'Cross-frontend handoff response missing required fields',\n response.status,\n );\n }\n return { handoffUrl: json.handoffUrl, expiresAt: json.expiresAt };\n }\n\n /**\n * Best-effort parse of the backend's `{ error: { details: { code, … } } }`\n * envelope.\n *\n * The path is `error.details.code` (NOT `error.code`) — see the\n * `error-handler.backend.service.ts` response shape, which spreads\n * `sanitizedError.context` under `details`. The error builder's\n * sanitization allowlist explicitly promotes `code` so cross-frontend\n * handoff failures expose the stable `CrossFrontendHandoffErrorCode`\n * enum value (cross-app-jwt-propagation.md C-4 contract).\n *\n * Falls back to `'malformed_response'` when the body is missing or its\n * `error.details.code` is not a known `CrossFrontendHandoffErrorCode`.\n * This keeps the client side strictly typed (`DocsAuthClientErrorCode`)\n * even when faced with a transitional / proxy / CDN response that does\n * not match the platform envelope.\n */\n private async parseHandoffErrorCode(response: Response): Promise<DocsAuthClientErrorCode> {\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n return 'malformed_response';\n }\n const code = (body as { error?: { details?: { code?: unknown } } } | null)\n ?.error?.details?.code;\n if (typeof code !== 'string') return 'malformed_response';\n\n const handoffCodes = Object.values(CrossFrontendHandoffErrorCode) as string[];\n if (handoffCodes.includes(code)) {\n return code as CrossFrontendHandoffErrorCode;\n }\n return 'malformed_response';\n }\n\n private joinUrl(path: string): string {\n return `${this.apiBaseUrl}${path.startsWith('/') ? path : `/${path}`}`;\n }\n}\n\n/**\n * Stable, finite set of error codes the runtime can surface to the\n * Docusaurus swizzles + `<AuthExchangePage>`.\n *\n * Mirrors `CrossFrontendHandoffErrorCode` for the handoff path PLUS three\n * runtime-only codes:\n *\n * - `unauthenticated` — the caller asked for a handoff while no\n * session was loaded; surfaced WITHOUT a\n * network call so the DocItem swizzle can\n * redirect to the SaaS app's login instead.\n * - `invalid_or_expired_code` — the `/api/v1/auth/token/exchange` call returned\n * non-200; covers expired, already-consumed,\n * or otherwise rejected codes.\n * - `malformed_response` — a transport / CDN / parser hiccup; rare,\n * but typed so the swizzle never has to\n * handle \"untyped error\" branches.\n */\nexport type DocsAuthClientErrorCode =\n | CrossFrontendHandoffErrorCode\n | 'unauthenticated'\n | 'invalid_or_expired_code'\n | 'malformed_response';\n\n/**\n * Typed error thrown by every `DocsAuthClient` method.\n *\n * Subclasses native `Error` (not `WildoBackendError`) on purpose — pulling\n * `WildoBackendError` here would force a dependency on `@wildo-ai/saas-models`\n * runtime utilities that the docs bundle has no other use for. The\n * swizzles `instanceof DocsAuthClientError` to distinguish from generic\n * fetch/abort errors.\n */\nexport class DocsAuthClientError extends Error {\n readonly code: DocsAuthClientErrorCode;\n readonly httpStatus: number;\n\n constructor(code: DocsAuthClientErrorCode, message: string, httpStatus: number) {\n super(message);\n this.name = 'DocsAuthClientError';\n this.code = code;\n this.httpStatus = httpStatus;\n }\n}\n"]}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Roles relevant to the docs-site gating decisions.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the role axis the SaaS app uses on access tokens. The docs site
|
|
6
|
+
* itself only cares about ONE distinction: "is this user an org admin or
|
|
7
|
+
* not?" — used by the Navbar swizzle to hide the "Organization API" link
|
|
8
|
+
* and by the DocItem swizzle to gate `/api/organization/*` pages
|
|
9
|
+
* (saas-technical-doc.md K-11).
|
|
10
|
+
*
|
|
11
|
+
* `ORG_ADMIN` value matches the string carried by the access token's
|
|
12
|
+
* `role` claim issued by `auth-token-issuer.backend.service.ts`. The
|
|
13
|
+
* generic `OTHER` bucket covers every other role (regular org member,
|
|
14
|
+
* viewer, application-level admin, etc.) — none of them grant Org-API
|
|
15
|
+
* visibility on the docs site.
|
|
16
|
+
*/
|
|
17
|
+
export declare enum DocsRole {
|
|
18
|
+
ORG_ADMIN = "org_admin",
|
|
19
|
+
OTHER = "other"
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Subset of access-token claims the docs site cares about.
|
|
23
|
+
*
|
|
24
|
+
* The full access-token payload issued by the SaaS backend carries many
|
|
25
|
+
* more fields (organization id, application id, scopes, …) — we
|
|
26
|
+
* deliberately project only the minimum needed by the docs-site swizzles
|
|
27
|
+
* so that:
|
|
28
|
+
* 1. A future change to the access-token shape that adds fields
|
|
29
|
+
* doesn't ripple into the docs site.
|
|
30
|
+
* 2. The docs site never accidentally renders or transmits sensitive
|
|
31
|
+
* claims it has no business consuming.
|
|
32
|
+
*
|
|
33
|
+
* `userId` and `email` are present for display purposes ("Signed in as
|
|
34
|
+
* <email>" in the navbar — done by the Navbar swizzle, not this package).
|
|
35
|
+
*
|
|
36
|
+
* `role` is the gating axis (see {@link DocsRole}).
|
|
37
|
+
*
|
|
38
|
+
* `exp` (UNIX seconds) lets the context clear the in-memory session as
|
|
39
|
+
* soon as the access token expires — without ever calling the network.
|
|
40
|
+
* No refresh loop (K-9b): when `exp` passes, the user is anonymous again
|
|
41
|
+
* until they re-handshake from the SaaS app.
|
|
42
|
+
*/
|
|
43
|
+
export declare const DocsJwtClaimsSchema: z.ZodObject<{
|
|
44
|
+
userId: z.ZodString;
|
|
45
|
+
email: z.ZodOptional<z.ZodEmail>;
|
|
46
|
+
role: z.ZodEnum<typeof DocsRole>;
|
|
47
|
+
exp: z.ZodNumber;
|
|
48
|
+
}, z.core.$strict>;
|
|
49
|
+
export type DocsJwtClaims = z.infer<typeof DocsJwtClaimsSchema>;
|
|
50
|
+
/**
|
|
51
|
+
* Public shape exposed by `useDocsAuthSession()` (saas-technical-doc.md K-11).
|
|
52
|
+
*
|
|
53
|
+
* Three mutually-exclusive states:
|
|
54
|
+
* - `status: 'anonymous'` — no handoff has been completed.
|
|
55
|
+
* - `status: 'exchanging'` — `/auth/exchange?code=…` is in flight.
|
|
56
|
+
* - `status: 'authenticated'` — `claims` and `accessToken` are present.
|
|
57
|
+
*
|
|
58
|
+
* Consumers (the three swizzles) discriminate on `status` and never reach
|
|
59
|
+
* for `accessToken`/`claims` while in the `anonymous` or `exchanging`
|
|
60
|
+
* states — the type narrows correctly thanks to the discriminated union.
|
|
61
|
+
*
|
|
62
|
+
* Does NOT model an `error` state directly: errors during the exchange
|
|
63
|
+
* are surfaced via the `AuthExchangePage` component's local UI, after
|
|
64
|
+
* which the session falls back to `anonymous`. Keeping the session shape
|
|
65
|
+
* tight prevents the swizzles from having to render error-recovery UI
|
|
66
|
+
* for events they did not cause.
|
|
67
|
+
*/
|
|
68
|
+
export type DocsAuthSession = {
|
|
69
|
+
status: 'anonymous';
|
|
70
|
+
} | {
|
|
71
|
+
status: 'exchanging';
|
|
72
|
+
} | {
|
|
73
|
+
status: 'authenticated';
|
|
74
|
+
accessToken: string;
|
|
75
|
+
claims: DocsJwtClaims;
|
|
76
|
+
};
|
|
77
|
+
//# sourceMappingURL=docs-auth-session.schemas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docs-auth-session.schemas.d.ts","sourceRoot":"","sources":["../../../../src/runtime/docs-auth-session.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;GAcG;AACH,oBAAY,QAAQ;IAClB,SAAS,cAAc;IACvB,KAAK,UAAU;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,mBAAmB;;;;;kBAK9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GACvB;IAAE,MAAM,EAAE,YAAY,CAAA;CAAE,GACxB;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAE,CAAC"}
|