@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.
Files changed (216) hide show
  1. package/LICENSE +34 -0
  2. package/dist/esm/.builder.pid +9 -0
  3. package/dist/esm/build/csp-emit.d.ts +9 -0
  4. package/dist/esm/build/csp-emit.d.ts.map +1 -0
  5. package/dist/esm/build/csp-emit.js +8 -0
  6. package/dist/esm/build/csp-emit.js.map +1 -0
  7. package/dist/esm/build/load-materialized-frontend-providers.d.ts +9 -0
  8. package/dist/esm/build/load-materialized-frontend-providers.d.ts.map +1 -0
  9. package/dist/esm/build/load-materialized-frontend-providers.js +9 -0
  10. package/dist/esm/build/load-materialized-frontend-providers.js.map +1 -0
  11. package/dist/esm/companion/application-documentation/application-organization-role-documentation.d.ts +66 -0
  12. package/dist/esm/companion/application-documentation/application-organization-role-documentation.d.ts.map +1 -0
  13. package/dist/esm/companion/application-documentation/application-organization-role-documentation.js +195 -0
  14. package/dist/esm/companion/application-documentation/application-organization-role-documentation.js.map +1 -0
  15. package/dist/esm/companion/application-documentation/application-organization-unit-resource-documentation.d.ts +36 -0
  16. package/dist/esm/companion/application-documentation/application-organization-unit-resource-documentation.d.ts.map +1 -0
  17. package/dist/esm/companion/application-documentation/application-organization-unit-resource-documentation.js +71 -0
  18. package/dist/esm/companion/application-documentation/application-organization-unit-resource-documentation.js.map +1 -0
  19. package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.d.ts +37 -0
  20. package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.d.ts.map +1 -0
  21. package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.js +1865 -0
  22. package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.js.map +1 -0
  23. package/dist/esm/companion/application-documentation/technical-documentation-operational-evidence.d.ts +22 -0
  24. package/dist/esm/companion/application-documentation/technical-documentation-operational-evidence.d.ts.map +1 -0
  25. package/dist/esm/companion/application-documentation/technical-documentation-operational-evidence.js +31 -0
  26. package/dist/esm/companion/application-documentation/technical-documentation-operational-evidence.js.map +1 -0
  27. package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.d.ts +31 -0
  28. package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.d.ts.map +1 -0
  29. package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.js +33 -0
  30. package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.js.map +1 -0
  31. package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.d.ts +13 -0
  32. package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.d.ts.map +1 -0
  33. package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.js +93 -0
  34. package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.js.map +1 -0
  35. package/dist/esm/companion/content/application-consumer-documentation-content-loader.d.ts +52 -0
  36. package/dist/esm/companion/content/application-consumer-documentation-content-loader.d.ts.map +1 -0
  37. package/dist/esm/companion/content/application-consumer-documentation-content-loader.js +191 -0
  38. package/dist/esm/companion/content/application-consumer-documentation-content-loader.js.map +1 -0
  39. package/dist/esm/companion/index.d.ts +39 -0
  40. package/dist/esm/companion/index.d.ts.map +1 -0
  41. package/dist/esm/companion/index.js +39 -0
  42. package/dist/esm/companion/index.js.map +1 -0
  43. package/dist/esm/companion/openapi-generator.d.ts +94 -0
  44. package/dist/esm/companion/openapi-generator.d.ts.map +1 -0
  45. package/dist/esm/companion/openapi-generator.js +1562 -0
  46. package/dist/esm/companion/openapi-generator.js.map +1 -0
  47. package/dist/esm/companion/operation-projection.schemas.d.ts +797 -0
  48. package/dist/esm/companion/operation-projection.schemas.d.ts.map +1 -0
  49. package/dist/esm/companion/operation-projection.schemas.js +610 -0
  50. package/dist/esm/companion/operation-projection.schemas.js.map +1 -0
  51. package/dist/esm/companion/publish-result.types.d.ts +124 -0
  52. package/dist/esm/companion/publish-result.types.d.ts.map +1 -0
  53. package/dist/esm/companion/publish-result.types.js +28 -0
  54. package/dist/esm/companion/publish-result.types.js.map +1 -0
  55. package/dist/esm/companion/rendering/technical-documentation-authorized-bundle-reader.d.ts +26 -0
  56. package/dist/esm/companion/rendering/technical-documentation-authorized-bundle-reader.d.ts.map +1 -0
  57. package/dist/esm/companion/rendering/technical-documentation-authorized-bundle-reader.js +63 -0
  58. package/dist/esm/companion/rendering/technical-documentation-authorized-bundle-reader.js.map +1 -0
  59. package/dist/esm/companion/rendering/technical-documentation-build-measurement.d.ts +23 -0
  60. package/dist/esm/companion/rendering/technical-documentation-build-measurement.d.ts.map +1 -0
  61. package/dist/esm/companion/rendering/technical-documentation-build-measurement.js +104 -0
  62. package/dist/esm/companion/rendering/technical-documentation-build-measurement.js.map +1 -0
  63. package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.d.ts +9 -0
  64. package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.d.ts.map +1 -0
  65. package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.js +421 -0
  66. package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.js.map +1 -0
  67. package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.d.ts +72 -0
  68. package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.d.ts.map +1 -0
  69. package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.js +204 -0
  70. package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.js.map +1 -0
  71. package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.d.ts +9 -0
  72. package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.d.ts.map +1 -0
  73. package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.js +106 -0
  74. package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.js.map +1 -0
  75. package/dist/esm/companion/rendering/technical-documentation-mdx-escaping.d.ts +43 -0
  76. package/dist/esm/companion/rendering/technical-documentation-mdx-escaping.d.ts.map +1 -0
  77. package/dist/esm/companion/rendering/technical-documentation-mdx-escaping.js +88 -0
  78. package/dist/esm/companion/rendering/technical-documentation-mdx-escaping.js.map +1 -0
  79. package/dist/esm/companion/rendering/technical-documentation-openapi-renderer.d.ts +7 -0
  80. package/dist/esm/companion/rendering/technical-documentation-openapi-renderer.d.ts.map +1 -0
  81. package/dist/esm/companion/rendering/technical-documentation-openapi-renderer.js +51 -0
  82. package/dist/esm/companion/rendering/technical-documentation-openapi-renderer.js.map +1 -0
  83. package/dist/esm/companion/rendering/technical-documentation-render-model.d.ts +45 -0
  84. package/dist/esm/companion/rendering/technical-documentation-render-model.d.ts.map +1 -0
  85. package/dist/esm/companion/rendering/technical-documentation-render-model.js +178 -0
  86. package/dist/esm/companion/rendering/technical-documentation-render-model.js.map +1 -0
  87. package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.d.ts +3 -0
  88. package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.d.ts.map +1 -0
  89. package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.js +49 -0
  90. package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.js.map +1 -0
  91. package/dist/esm/companion/spec-to-operation-doc.d.ts +176 -0
  92. package/dist/esm/companion/spec-to-operation-doc.d.ts.map +1 -0
  93. package/dist/esm/companion/spec-to-operation-doc.js +326 -0
  94. package/dist/esm/companion/spec-to-operation-doc.js.map +1 -0
  95. package/dist/esm/companion/technical-documentation-asset-path.d.ts +16 -0
  96. package/dist/esm/companion/technical-documentation-asset-path.d.ts.map +1 -0
  97. package/dist/esm/companion/technical-documentation-asset-path.js +19 -0
  98. package/dist/esm/companion/technical-documentation-asset-path.js.map +1 -0
  99. package/dist/esm/companion/technical-documentation-capture-execution-port.d.ts +14 -0
  100. package/dist/esm/companion/technical-documentation-capture-execution-port.d.ts.map +1 -0
  101. package/dist/esm/companion/technical-documentation-capture-execution-port.js +1 -0
  102. package/dist/esm/companion/technical-documentation-capture-execution-port.js.map +1 -0
  103. package/dist/esm/companion/technical-documentation-diagram-materializer.d.ts +25 -0
  104. package/dist/esm/companion/technical-documentation-diagram-materializer.d.ts.map +1 -0
  105. package/dist/esm/companion/technical-documentation-diagram-materializer.js +86 -0
  106. package/dist/esm/companion/technical-documentation-diagram-materializer.js.map +1 -0
  107. package/dist/esm/companion/technical-documentation-placeholder-materializer.d.ts +17 -0
  108. package/dist/esm/companion/technical-documentation-placeholder-materializer.d.ts.map +1 -0
  109. package/dist/esm/companion/technical-documentation-placeholder-materializer.js +63 -0
  110. package/dist/esm/companion/technical-documentation-placeholder-materializer.js.map +1 -0
  111. package/dist/esm/companion/zod-to-openapi.d.ts +67 -0
  112. package/dist/esm/companion/zod-to-openapi.d.ts.map +1 -0
  113. package/dist/esm/companion/zod-to-openapi.js +211 -0
  114. package/dist/esm/companion/zod-to-openapi.js.map +1 -0
  115. package/dist/esm/companion-exports.d.ts +32 -0
  116. package/dist/esm/companion-exports.d.ts.map +1 -0
  117. package/dist/esm/companion-exports.js +32 -0
  118. package/dist/esm/companion-exports.js.map +1 -0
  119. package/dist/esm/config/define-tech-doc-config.d.ts +38 -0
  120. package/dist/esm/config/define-tech-doc-config.d.ts.map +1 -0
  121. package/dist/esm/config/define-tech-doc-config.js +40 -0
  122. package/dist/esm/config/define-tech-doc-config.js.map +1 -0
  123. package/dist/esm/config/index.d.ts +22 -0
  124. package/dist/esm/config/index.d.ts.map +1 -0
  125. package/dist/esm/config/index.js +22 -0
  126. package/dist/esm/config/index.js.map +1 -0
  127. package/dist/esm/config/wildo-tech-doc-config.schemas.d.ts +204 -0
  128. package/dist/esm/config/wildo-tech-doc-config.schemas.d.ts.map +1 -0
  129. package/dist/esm/config/wildo-tech-doc-config.schemas.js +192 -0
  130. package/dist/esm/config/wildo-tech-doc-config.schemas.js.map +1 -0
  131. package/dist/esm/content/application-consumer-documentation-content-manifest.schemas.d.ts +82 -0
  132. package/dist/esm/content/application-consumer-documentation-content-manifest.schemas.d.ts.map +1 -0
  133. package/dist/esm/content/application-consumer-documentation-content-manifest.schemas.js +114 -0
  134. package/dist/esm/content/application-consumer-documentation-content-manifest.schemas.js.map +1 -0
  135. package/dist/esm/content/application-consumer-documentation-content.techdoc.d.ts +420 -0
  136. package/dist/esm/content/application-consumer-documentation-content.techdoc.d.ts.map +1 -0
  137. package/dist/esm/content/application-consumer-documentation-content.techdoc.js +9619 -0
  138. package/dist/esm/content/application-consumer-documentation-content.techdoc.js.map +1 -0
  139. package/dist/esm/content.exports.d.ts +9 -0
  140. package/dist/esm/content.exports.d.ts.map +1 -0
  141. package/dist/esm/content.exports.js +9 -0
  142. package/dist/esm/content.exports.js.map +1 -0
  143. package/dist/esm/index.d.ts +26 -0
  144. package/dist/esm/index.d.ts.map +1 -0
  145. package/dist/esm/index.js +26 -0
  146. package/dist/esm/index.js.map +1 -0
  147. package/dist/esm/openapi/api-reference-link-index.d.ts +98 -0
  148. package/dist/esm/openapi/api-reference-link-index.d.ts.map +1 -0
  149. package/dist/esm/openapi/api-reference-link-index.js +301 -0
  150. package/dist/esm/openapi/api-reference-link-index.js.map +1 -0
  151. package/dist/esm/openapi/api-reference-targets.d.ts +71 -0
  152. package/dist/esm/openapi/api-reference-targets.d.ts.map +1 -0
  153. package/dist/esm/openapi/api-reference-targets.js +114 -0
  154. package/dist/esm/openapi/api-reference-targets.js.map +1 -0
  155. package/dist/esm/openapi/index.d.ts +20 -0
  156. package/dist/esm/openapi/index.d.ts.map +1 -0
  157. package/dist/esm/openapi/index.js +20 -0
  158. package/dist/esm/openapi/index.js.map +1 -0
  159. package/dist/esm/openapi/openapi-generation-output.schemas.d.ts +80 -0
  160. package/dist/esm/openapi/openapi-generation-output.schemas.d.ts.map +1 -0
  161. package/dist/esm/openapi/openapi-generation-output.schemas.js +76 -0
  162. package/dist/esm/openapi/openapi-generation-output.schemas.js.map +1 -0
  163. package/dist/esm/openapi-reference-model.exports.d.ts +10 -0
  164. package/dist/esm/openapi-reference-model.exports.d.ts.map +1 -0
  165. package/dist/esm/openapi-reference-model.exports.js +10 -0
  166. package/dist/esm/openapi-reference-model.exports.js.map +1 -0
  167. package/dist/esm/runtime/AuthExchangePage.d.ts +84 -0
  168. package/dist/esm/runtime/AuthExchangePage.d.ts.map +1 -0
  169. package/dist/esm/runtime/AuthExchangePage.js +188 -0
  170. package/dist/esm/runtime/AuthExchangePage.js.map +1 -0
  171. package/dist/esm/runtime/DocsAuthContext.d.ts +119 -0
  172. package/dist/esm/runtime/DocsAuthContext.d.ts.map +1 -0
  173. package/dist/esm/runtime/DocsAuthContext.js +171 -0
  174. package/dist/esm/runtime/DocsAuthContext.js.map +1 -0
  175. package/dist/esm/runtime/decode-jwt-claims.d.ts +39 -0
  176. package/dist/esm/runtime/decode-jwt-claims.d.ts.map +1 -0
  177. package/dist/esm/runtime/decode-jwt-claims.js +86 -0
  178. package/dist/esm/runtime/decode-jwt-claims.js.map +1 -0
  179. package/dist/esm/runtime/docs-auth-client.d.ts +193 -0
  180. package/dist/esm/runtime/docs-auth-client.d.ts.map +1 -0
  181. package/dist/esm/runtime/docs-auth-client.js +211 -0
  182. package/dist/esm/runtime/docs-auth-client.js.map +1 -0
  183. package/dist/esm/runtime/docs-auth-session.schemas.d.ts +77 -0
  184. package/dist/esm/runtime/docs-auth-session.schemas.d.ts.map +1 -0
  185. package/dist/esm/runtime/docs-auth-session.schemas.js +50 -0
  186. package/dist/esm/runtime/docs-auth-session.schemas.js.map +1 -0
  187. package/dist/esm/runtime/frontend-provider-registry.techdoc.d.ts +17 -0
  188. package/dist/esm/runtime/frontend-provider-registry.techdoc.d.ts.map +1 -0
  189. package/dist/esm/runtime/frontend-provider-registry.techdoc.js +23 -0
  190. package/dist/esm/runtime/frontend-provider-registry.techdoc.js.map +1 -0
  191. package/dist/esm/runtime/index.d.ts +57 -0
  192. package/dist/esm/runtime/index.d.ts.map +1 -0
  193. package/dist/esm/runtime/index.js +76 -0
  194. package/dist/esm/runtime/index.js.map +1 -0
  195. package/dist/esm/runtime/openapi-reference-conservation.d.ts +20 -0
  196. package/dist/esm/runtime/openapi-reference-conservation.d.ts.map +1 -0
  197. package/dist/esm/runtime/openapi-reference-conservation.js +102 -0
  198. package/dist/esm/runtime/openapi-reference-conservation.js.map +1 -0
  199. package/dist/esm/runtime/openapi-reference-model.d.ts +224 -0
  200. package/dist/esm/runtime/openapi-reference-model.d.ts.map +1 -0
  201. package/dist/esm/runtime/openapi-reference-model.js +579 -0
  202. package/dist/esm/runtime/openapi-reference-model.js.map +1 -0
  203. package/dist/esm/runtime/openapi-reference-view.d.ts +13 -0
  204. package/dist/esm/runtime/openapi-reference-view.d.ts.map +1 -0
  205. package/dist/esm/runtime/openapi-reference-view.js +284 -0
  206. package/dist/esm/runtime/openapi-reference-view.js.map +1 -0
  207. package/dist/esm/runtime/use-docs-auth-session.d.ts +26 -0
  208. package/dist/esm/runtime/use-docs-auth-session.d.ts.map +1 -0
  209. package/dist/esm/runtime/use-docs-auth-session.js +34 -0
  210. package/dist/esm/runtime/use-docs-auth-session.js.map +1 -0
  211. package/dist/esm/runtime/use-docs-frontend-provider-registry.d.ts +3 -0
  212. package/dist/esm/runtime/use-docs-frontend-provider-registry.d.ts.map +1 -0
  213. package/dist/esm/runtime/use-docs-frontend-provider-registry.js +5 -0
  214. package/dist/esm/runtime/use-docs-frontend-provider-registry.js.map +1 -0
  215. package/dist/tsconfig.build.tsbuildinfo +1 -0
  216. package/package.json +117 -0
@@ -0,0 +1,188 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { isSameOriginRedirectPath } from '@wildo-ai/saas-models/public-runtime';
4
+ import { useDocsAuthSession } from './use-docs-auth-session.js';
5
+ /**
6
+ * Page component mounted at `/auth/exchange` on the docs site
7
+ * (saas-technical-doc.md Step 5).
8
+ *
9
+ * Lifecycle on mount:
10
+ *
11
+ * 1. Read `?code=` and `?return=` from `window.location.search`.
12
+ * 2. If `code` is missing → render the missingCode UI (no network call).
13
+ * 3. Else call `useDocsAuthSession().beginExchange(code)`, which POSTs
14
+ * `/auth/token/exchange` and updates the in-memory context.
15
+ * 4. On success, navigate to `returnPath` (defaults to `/`) using the
16
+ * consumer-supplied `onSuccessNavigate` (Docusaurus / react-router
17
+ * hosts MUST pass a client-side `history.push` — see the prop
18
+ * docstring below for why a full reload is incorrect here).
19
+ * 5. On failure, render the error UI with a link back to `/`.
20
+ *
21
+ * Why a single component (rather than splitting effect + render):
22
+ *
23
+ * - Keeps the exchange contract trivially auditable from one file.
24
+ * - The effect intentionally runs ONCE per mount (`hasRunRef` guard) —
25
+ * `code` values are single-use AUTH_CODE_EXCHANGE consumable tokens
26
+ * and a stray re-mount in React 18 strict-mode dev would burn the
27
+ * code with the second call returning `invalid_or_expired_code`.
28
+ *
29
+ * Visual layer: this component renders a minimal centered "Signing you
30
+ * in…" / error block. Apps that want to brand it can either pass a
31
+ * `render` prop (consumer-controlled UI for each status) or skip this
32
+ * component entirely and call `useDocsAuthSession().beginExchange(code)`
33
+ * from their own page. The default rendering deliberately uses inline
34
+ * styles (no Tailwind / no CSS imports) so the component can be mounted
35
+ * by any Docusaurus theme without dragging in extra build pipeline.
36
+ */
37
+ export const AuthExchangePage = ({ render, onSuccessNavigate }) => {
38
+ const { beginExchange } = useDocsAuthSession();
39
+ const [status, setStatus] = useState('pending');
40
+ const [error, setError] = useState(null);
41
+ const [returnPath, setReturnPath] = useState('/');
42
+ const hasRunRef = useRef(false);
43
+ useEffect(() => {
44
+ if (hasRunRef.current)
45
+ return;
46
+ hasRunRef.current = true;
47
+ /**
48
+ * SSR / build-time safety: Docusaurus may evaluate page modules in
49
+ * Node during the static build pre-render pass. `window` is undefined
50
+ * there. Bailing out keeps the static HTML emission clean — the real
51
+ * exchange runs on hydration in the browser.
52
+ */
53
+ if (typeof window === 'undefined')
54
+ return;
55
+ const params = new URLSearchParams(window.location.search);
56
+ const code = params.get('code');
57
+ /**
58
+ * URL `?return=` is kept as a defense-in-depth fallback for codes
59
+ * minted before the cross-app-jwt-propagation.md Step 5 metadata
60
+ * roll-out (which carries `returnPath` inside the exchange response
61
+ * envelope). The post-exchange `safeReturn` resolution below prefers
62
+ * the response-borne value because the issuer signs both into the
63
+ * same code and the response value cannot be tampered with by
64
+ * rewriting only the URL query string.
65
+ */
66
+ const rawUrlReturn = params.get('return') ?? '/';
67
+ const urlSafeReturn = sanitizeReturnPath(rawUrlReturn);
68
+ setReturnPath(urlSafeReturn);
69
+ if (!code) {
70
+ setStatus('missingCode');
71
+ return;
72
+ }
73
+ setStatus('exchanging');
74
+ beginExchange(code)
75
+ .then(({ claims, returnPath: responseReturnPath }) => {
76
+ /**
77
+ * Source-of-truth ordering for navigation target:
78
+ * 1. `responseReturnPath` — issuer-supplied via the exchange
79
+ * response's TOP-LEVEL `returnPath` field (NOT a `metadata`
80
+ * sub-field — the response has no `metadata` envelope; see
81
+ * `TokenExchangeResponseSchema`). Sanitized again here even
82
+ * though the issuer already validates at minting time; an
83
+ * attacker who can intercept the response body should not be
84
+ * able to weaponise it.
85
+ * 2. `urlSafeReturn` — the URL `?return=` query parameter
86
+ * (already sanitized above). Used when codes were minted
87
+ * without a `returnPath` (OAuth2 callbacks, by contract)
88
+ * OR when the response otherwise omits it.
89
+ * 3. `/` — the docs root. Final fallback so the user is never
90
+ * stranded on `/auth/exchange`.
91
+ */
92
+ const safeReturn = responseReturnPath
93
+ ? sanitizeReturnPath(responseReturnPath)
94
+ : urlSafeReturn;
95
+ setReturnPath(safeReturn);
96
+ if (claims === null) {
97
+ setError('This sign-in link could not be exchanged. It may have expired or already been used. Please re-open the docs from the application.');
98
+ setStatus('error');
99
+ return;
100
+ }
101
+ setStatus('success');
102
+ // Navigate AFTER the React state flush so any other consumers of
103
+ // `useDocsAuthSession()` see the authenticated session before the
104
+ // page disappears. The default `window.location.assign` is the
105
+ // plain-HTML fallback; SPA hosts override via `onSuccessNavigate`
106
+ // (see prop docstring — full reload would tear down the in-memory
107
+ // provider state we just populated).
108
+ const navigate = onSuccessNavigate ?? ((path) => window.location.assign(path));
109
+ window.setTimeout(() => {
110
+ navigate(safeReturn);
111
+ }, 0);
112
+ })
113
+ .catch((cause) => {
114
+ setError(cause instanceof Error ? cause.message : 'Sign-in failed');
115
+ setStatus('error');
116
+ });
117
+ }, [beginExchange, onSuccessNavigate]);
118
+ if (render)
119
+ return _jsx(_Fragment, { children: render({ status, returnPath, error }) });
120
+ return _jsx(DefaultExchangeUi, { status: status, returnPath: returnPath, error: error });
121
+ };
122
+ /**
123
+ * Minimal client-side validation of the `return` query parameter.
124
+ *
125
+ * The cross-frontend handoff endpoint already validates `returnPath` at
126
+ * the schema boundary (see `CrossFrontendHandoff_ReturnPathSchema` in
127
+ * `authentication.shared.dto.schemas.ts`). This duplicate check is
128
+ * defense-in-depth: anyone could construct a docs URL like
129
+ * `/auth/exchange?code=…&return=https://evil.com` directly without
130
+ * going through the backend. Without this guard, the `location.assign`
131
+ * fallback (used by plain-HTML hosts when `onSuccessNavigate` is not
132
+ * supplied) would happily redirect to that origin.
133
+ *
134
+ * The ORIGIN question is delegated to the shared
135
+ * {@link isSameOriginRedirectPath} predicate on the bundle-safe seam, and
136
+ * that delegation is a fix, not tidying. This function was a verbatim copy
137
+ * of the SaaS-app receiver's copy, which was itself a copy of the issuer
138
+ * schema's rules — and all three admitted `/<TAB>/evil.com`, because the
139
+ * URL parser strips ASCII tab / LF / CR from the input BEFORE parsing, so
140
+ * the target the browser sees is `//evil.com`. Every copy banned `\`; none
141
+ * banned the whitespace that becomes a `/`. Three mirrors reflected the
142
+ * same hole, which is why the answer now has one home.
143
+ *
144
+ * The character rules are KEPT as defence-in-depth for the concerns the
145
+ * origin predicate deliberately does not own: path traversal (`..`), markup
146
+ * injection (`<>"`) and the length ceiling.
147
+ */
148
+ function sanitizeReturnPath(raw) {
149
+ // Authoritative: `//evil.com`, `/\evil.com` and the stripped-control-character
150
+ // family all fail here, in one parse-based check.
151
+ if (!isSameOriginRedirectPath(raw))
152
+ return '/';
153
+ if (raw.includes('..'))
154
+ return '/';
155
+ if (raw.includes('\\'))
156
+ return '/';
157
+ if (raw.includes('://'))
158
+ return '/';
159
+ if (/[<>"]/.test(raw))
160
+ return '/';
161
+ if (raw.length > 256)
162
+ return '/';
163
+ return raw;
164
+ }
165
+ /**
166
+ * Default zero-style status block. Apps that brand the docs site override
167
+ * this via the `render` prop on `<AuthExchangePage>`.
168
+ */
169
+ const DefaultExchangeUi = ({ status, returnPath, error }) => {
170
+ const containerStyle = {
171
+ maxWidth: 480,
172
+ margin: '6rem auto',
173
+ padding: '2rem',
174
+ fontFamily: 'system-ui, sans-serif',
175
+ textAlign: 'center',
176
+ };
177
+ if (status === 'pending' || status === 'exchanging') {
178
+ return (_jsxs("div", { style: containerStyle, children: [_jsx("h1", { style: { fontSize: '1.25rem' }, children: "Signing you in\u2026" }), _jsx("p", { style: { color: '#666' }, children: "This will only take a moment." })] }));
179
+ }
180
+ if (status === 'success') {
181
+ return (_jsxs("div", { style: containerStyle, children: [_jsx("h1", { style: { fontSize: '1.25rem' }, children: "You're signed in." }), _jsxs("p", { style: { color: '#666' }, children: ["Redirecting to ", returnPath, "\u2026"] })] }));
182
+ }
183
+ if (status === 'missingCode') {
184
+ return (_jsxs("div", { style: containerStyle, children: [_jsx("h1", { style: { fontSize: '1.25rem' }, children: "This page requires a sign-in code." }), _jsx("p", { style: { color: '#666' }, children: "Please open the documentation from the application." }), _jsx("p", { children: _jsx("a", { href: "/", children: "Go to docs home" }) })] }));
185
+ }
186
+ return (_jsxs("div", { style: containerStyle, children: [_jsx("h1", { style: { fontSize: '1.25rem' }, children: "Sign-in failed" }), _jsx("p", { style: { color: '#666' }, children: error ?? 'Unknown error.' }), _jsx("p", { children: _jsx("a", { href: "/", children: "Go to docs home" }) })] }));
187
+ };
188
+ //# sourceMappingURL=AuthExchangePage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AuthExchangePage.js","sourceRoot":"","sources":["../../../../src/runtime/AuthExchangePage.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, useState, type CSSProperties, type FC, type ReactNode } from 'react';\nimport { isSameOriginRedirectPath } from '@wildo-ai/saas-models/public-runtime';\nimport { useDocsAuthSession } from './use-docs-auth-session';\n\n/**\n * Status of the auth-code exchange the page is performing.\n *\n * - `pending` — initial mount; effect has not yet run.\n * - `exchanging`— the `/auth/token/exchange` POST is in flight.\n * - `success` — tokens received; about to navigate to `returnPath`.\n * - `error` — exchange failed; the page renders a fallback message\n * and a manual link back to the docs root.\n * - `missingCode`— landed on `/auth/exchange` without `?code=…` (someone\n * bookmarked the page or arrived via a stale link).\n */\nexport type AuthExchangeStatus =\n | 'pending'\n | 'exchanging'\n | 'success'\n | 'error'\n | 'missingCode';\n\n/**\n * Page component mounted at `/auth/exchange` on the docs site\n * (saas-technical-doc.md Step 5).\n *\n * Lifecycle on mount:\n *\n * 1. Read `?code=` and `?return=` from `window.location.search`.\n * 2. If `code` is missing → render the missingCode UI (no network call).\n * 3. Else call `useDocsAuthSession().beginExchange(code)`, which POSTs\n * `/auth/token/exchange` and updates the in-memory context.\n * 4. On success, navigate to `returnPath` (defaults to `/`) using the\n * consumer-supplied `onSuccessNavigate` (Docusaurus / react-router\n * hosts MUST pass a client-side `history.push` — see the prop\n * docstring below for why a full reload is incorrect here).\n * 5. On failure, render the error UI with a link back to `/`.\n *\n * Why a single component (rather than splitting effect + render):\n *\n * - Keeps the exchange contract trivially auditable from one file.\n * - The effect intentionally runs ONCE per mount (`hasRunRef` guard) —\n * `code` values are single-use AUTH_CODE_EXCHANGE consumable tokens\n * and a stray re-mount in React 18 strict-mode dev would burn the\n * code with the second call returning `invalid_or_expired_code`.\n *\n * Visual layer: this component renders a minimal centered \"Signing you\n * in…\" / error block. Apps that want to brand it can either pass a\n * `render` prop (consumer-controlled UI for each status) or skip this\n * component entirely and call `useDocsAuthSession().beginExchange(code)`\n * from their own page. The default rendering deliberately uses inline\n * styles (no Tailwind / no CSS imports) so the component can be mounted\n * by any Docusaurus theme without dragging in extra build pipeline.\n */\nexport const AuthExchangePage: FC<{\n /**\n * Override the default rendering. Receives the current status and (when\n * the page reaches a terminal state) a recovery URL the consumer can\n * link back to.\n */\n render?: (props: { status: AuthExchangeStatus; returnPath: string; error: string | null }) => ReactNode;\n /**\n * Post-success navigation. **SPA hosts (Docusaurus, react-router,\n * Vite/React-Router apps) MUST pass a client-side router push** — the\n * default `window.location.assign` triggers a full browser navigation\n * which destroys the in-memory `DocsAuthProvider` state we just\n * populated (the entire JS bundle re-executes from scratch on the\n * destination page, the React tree re-mounts, and `session` resets to\n * `'anonymous'`). For a Docusaurus consumer that looks like:\n *\n * ```tsx\n * import { useHistory } from '@docusaurus/router';\n *\n * function AuthExchangeRoute() {\n * const history = useHistory();\n * return <AuthExchangePage onSuccessNavigate={(p) => history.push(p)} />;\n * }\n * ```\n *\n * The default `window.location.assign` only works for plain-HTML hosts\n * that have no in-memory client state to preserve across the redirect.\n * It is kept as the default so the runtime stays usable in those hosts\n * with zero ceremony, but for SPAs failing to override it would\n * silently break the entire handoff (the user lands on the destination\n * page anonymous and any role-gated content rejects them).\n */\n onSuccessNavigate?: (returnPath: string) => void;\n}> = ({ render, onSuccessNavigate }) => {\n const { beginExchange } = useDocsAuthSession();\n\n const [status, setStatus] = useState<AuthExchangeStatus>('pending');\n const [error, setError] = useState<string | null>(null);\n const [returnPath, setReturnPath] = useState<string>('/');\n\n const hasRunRef = useRef(false);\n\n useEffect(() => {\n if (hasRunRef.current) return;\n hasRunRef.current = true;\n\n /**\n * SSR / build-time safety: Docusaurus may evaluate page modules in\n * Node during the static build pre-render pass. `window` is undefined\n * there. Bailing out keeps the static HTML emission clean — the real\n * exchange runs on hydration in the browser.\n */\n if (typeof window === 'undefined') return;\n\n const params = new URLSearchParams(window.location.search);\n const code = params.get('code');\n /**\n * URL `?return=` is kept as a defense-in-depth fallback for codes\n * minted before the cross-app-jwt-propagation.md Step 5 metadata\n * roll-out (which carries `returnPath` inside the exchange response\n * envelope). The post-exchange `safeReturn` resolution below prefers\n * the response-borne value because the issuer signs both into the\n * same code and the response value cannot be tampered with by\n * rewriting only the URL query string.\n */\n const rawUrlReturn = params.get('return') ?? '/';\n const urlSafeReturn = sanitizeReturnPath(rawUrlReturn);\n setReturnPath(urlSafeReturn);\n\n if (!code) {\n setStatus('missingCode');\n return;\n }\n\n setStatus('exchanging');\n beginExchange(code)\n .then(({ claims, returnPath: responseReturnPath }) => {\n /**\n * Source-of-truth ordering for navigation target:\n * 1. `responseReturnPath` — issuer-supplied via the exchange\n * response's TOP-LEVEL `returnPath` field (NOT a `metadata`\n * sub-field — the response has no `metadata` envelope; see\n * `TokenExchangeResponseSchema`). Sanitized again here even\n * though the issuer already validates at minting time; an\n * attacker who can intercept the response body should not be\n * able to weaponise it.\n * 2. `urlSafeReturn` — the URL `?return=` query parameter\n * (already sanitized above). Used when codes were minted\n * without a `returnPath` (OAuth2 callbacks, by contract)\n * OR when the response otherwise omits it.\n * 3. `/` — the docs root. Final fallback so the user is never\n * stranded on `/auth/exchange`.\n */\n const safeReturn = responseReturnPath\n ? sanitizeReturnPath(responseReturnPath)\n : urlSafeReturn;\n setReturnPath(safeReturn);\n\n if (claims === null) {\n setError(\n 'This sign-in link could not be exchanged. It may have expired or already been used. Please re-open the docs from the application.',\n );\n setStatus('error');\n return;\n }\n setStatus('success');\n // Navigate AFTER the React state flush so any other consumers of\n // `useDocsAuthSession()` see the authenticated session before the\n // page disappears. The default `window.location.assign` is the\n // plain-HTML fallback; SPA hosts override via `onSuccessNavigate`\n // (see prop docstring — full reload would tear down the in-memory\n // provider state we just populated).\n const navigate =\n onSuccessNavigate ?? ((path: string) => window.location.assign(path));\n window.setTimeout(() => {\n navigate(safeReturn);\n }, 0);\n })\n .catch((cause: unknown) => {\n setError(cause instanceof Error ? cause.message : 'Sign-in failed');\n setStatus('error');\n });\n }, [beginExchange, onSuccessNavigate]);\n\n if (render) return <>{render({ status, returnPath, error })}</>;\n\n return <DefaultExchangeUi status={status} returnPath={returnPath} error={error} />;\n};\n\n/**\n * Minimal client-side validation of the `return` query parameter.\n *\n * The cross-frontend handoff endpoint already validates `returnPath` at\n * the schema boundary (see `CrossFrontendHandoff_ReturnPathSchema` in\n * `authentication.shared.dto.schemas.ts`). This duplicate check is\n * defense-in-depth: anyone could construct a docs URL like\n * `/auth/exchange?code=…&return=https://evil.com` directly without\n * going through the backend. Without this guard, the `location.assign`\n * fallback (used by plain-HTML hosts when `onSuccessNavigate` is not\n * supplied) would happily redirect to that origin.\n *\n * The ORIGIN question is delegated to the shared\n * {@link isSameOriginRedirectPath} predicate on the bundle-safe seam, and\n * that delegation is a fix, not tidying. This function was a verbatim copy\n * of the SaaS-app receiver's copy, which was itself a copy of the issuer\n * schema's rules — and all three admitted `/<TAB>/evil.com`, because the\n * URL parser strips ASCII tab / LF / CR from the input BEFORE parsing, so\n * the target the browser sees is `//evil.com`. Every copy banned `\\`; none\n * banned the whitespace that becomes a `/`. Three mirrors reflected the\n * same hole, which is why the answer now has one home.\n *\n * The character rules are KEPT as defence-in-depth for the concerns the\n * origin predicate deliberately does not own: path traversal (`..`), markup\n * injection (`<>\"`) and the length ceiling.\n */\nfunction sanitizeReturnPath(raw: string): string {\n // Authoritative: `//evil.com`, `/\\evil.com` and the stripped-control-character\n // family all fail here, in one parse-based check.\n if (!isSameOriginRedirectPath(raw)) return '/';\n if (raw.includes('..')) return '/';\n if (raw.includes('\\\\')) return '/';\n if (raw.includes('://')) return '/';\n if (/[<>\"]/.test(raw)) return '/';\n if (raw.length > 256) return '/';\n return raw;\n}\n\n/**\n * Default zero-style status block. Apps that brand the docs site override\n * this via the `render` prop on `<AuthExchangePage>`.\n */\nconst DefaultExchangeUi: FC<{\n status: AuthExchangeStatus;\n returnPath: string;\n error: string | null;\n}> = ({ status, returnPath, error }) => {\n const containerStyle: CSSProperties = {\n maxWidth: 480,\n margin: '6rem auto',\n padding: '2rem',\n fontFamily: 'system-ui, sans-serif',\n textAlign: 'center',\n };\n\n if (status === 'pending' || status === 'exchanging') {\n return (\n <div style={containerStyle}>\n <h1 style={{ fontSize: '1.25rem' }}>Signing you in…</h1>\n <p style={{ color: '#666' }}>This will only take a moment.</p>\n </div>\n );\n }\n\n if (status === 'success') {\n return (\n <div style={containerStyle}>\n <h1 style={{ fontSize: '1.25rem' }}>You&apos;re signed in.</h1>\n <p style={{ color: '#666' }}>Redirecting to {returnPath}…</p>\n </div>\n );\n }\n\n if (status === 'missingCode') {\n return (\n <div style={containerStyle}>\n <h1 style={{ fontSize: '1.25rem' }}>This page requires a sign-in code.</h1>\n <p style={{ color: '#666' }}>Please open the documentation from the application.</p>\n <p>\n <a href=\"/\">Go to docs home</a>\n </p>\n </div>\n );\n }\n\n return (\n <div style={containerStyle}>\n <h1 style={{ fontSize: '1.25rem' }}>Sign-in failed</h1>\n <p style={{ color: '#666' }}>{error ?? 'Unknown error.'}</p>\n <p>\n <a href=\"/\">Go to docs home</a>\n </p>\n </div>\n );\n};\n"]}
@@ -0,0 +1,119 @@
1
+ import { type Context, type FC, type ReactNode } from 'react';
2
+ import type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';
3
+ import { DocsAuthClient } from './docs-auth-client';
4
+ import type { DocsAuthSession, DocsJwtClaims } from './docs-auth-session.schemas';
5
+ import { type FrontendTechdocProviderRegistry } from './frontend-provider-registry.techdoc';
6
+ /**
7
+ * Result of a `beginExchange()` call surfaced to `<AuthExchangePage>`.
8
+ *
9
+ * `claims` — decoded `DocsJwtClaims` (or `null` if the access token
10
+ * came back undecodable — treated as anonymous).
11
+ * `returnPath` — the issuer-supplied `returnPath` from the token exchange
12
+ * response (cross-app-jwt-propagation.md Step 5). It is
13
+ * a TOP-LEVEL field on `TokenExchangeResponseSchema` —
14
+ * NOT nested under `metadata`. The earlier draft of this
15
+ * JSDoc said "metadata.returnPath" and the implementation
16
+ * read `response.metadata?.returnPath` (always undefined,
17
+ * because no such key exists on the response). H-3 in the
18
+ * Step 5 second deep-review log fixes both the docstring
19
+ * and the read site.
20
+ * `null` for OAuth2-callback codes (which never carry a
21
+ * `returnPath` at all — discriminator invariant pinned by
22
+ * `TokenExchangeResponseSchema` JSDoc); the exchange page
23
+ * falls back to the URL `?return=` parameter in that case
24
+ * (defense-in-depth).
25
+ */
26
+ export interface DocsBeginExchangeResult {
27
+ readonly claims: DocsJwtClaims | null;
28
+ readonly returnPath: string | null;
29
+ }
30
+ /**
31
+ * Public surface of `useDocsAuthSession()`.
32
+ *
33
+ * `session` — discriminated union (anonymous / exchanging / authenticated).
34
+ * `client` — the singleton `DocsAuthClient` for authenticated docs
35
+ * runtime code that needs to request a handoff to another
36
+ * frontend. Anonymous docs gates deep-link to the SaaS app
37
+ * because handoff issuance requires a bearer.
38
+ * `frontendProviderRegistry`
39
+ * — hydrated registry of the frontend-safe provider entries
40
+ * materialized for the active docs service. Browser-only
41
+ * docs widgets (search, analytics, future SDK protocols)
42
+ * read from this instead of importing generated artifacts.
43
+ * `beginExchange` — called by `<AuthExchangePage>` when a `?code=` arrives.
44
+ * Returns the decoded claims AND the issuer-supplied
45
+ * top-level `returnPath` from the exchange response
46
+ * (the SOURCE OF TRUTH for post-exchange navigation;
47
+ * the URL `?return=` is a defense-in-depth fallback
48
+ * only). NOT `response.metadata.returnPath` — the
49
+ * response has no `metadata` envelope (H-3 fix).
50
+ * `clear` — called when the user explicitly closes the docs
51
+ * session (e.g. a "Sign out of docs" link). NOT called
52
+ * on tab close — ephemeral state evaporates on its own.
53
+ */
54
+ export interface DocsAuthContextValue {
55
+ readonly session: DocsAuthSession;
56
+ readonly client: DocsAuthClient;
57
+ readonly frontendProviderRegistry: FrontendTechdocProviderRegistry;
58
+ beginExchange(code: string): Promise<DocsBeginExchangeResult>;
59
+ clear(): void;
60
+ }
61
+ /**
62
+ * @internal — consumers should call `useDocsAuthSession()` instead of
63
+ * touching this directly. Exported so the matching hook can `useContext`
64
+ * it from a sibling module (Fast Refresh boundary).
65
+ */
66
+ export declare const DocsAuthContext: Context<DocsAuthContextValue | null>;
67
+ /**
68
+ * In-memory React context owning the docs-site authenticated session
69
+ * (saas-technical-doc.md K-9b).
70
+ *
71
+ * Storage characteristics — locked by K-9b:
72
+ *
73
+ * - In-memory only. NO `localStorage`, NO `sessionStorage`, NO cookies.
74
+ * Closing the tab destroys the session.
75
+ * - NO refresh-token loop. `/auth/token/exchange` no longer returns a
76
+ * refresh token in the JSON body; SaaS-app receivers get refresh via the
77
+ * HttpOnly cookie channel, while docs intentionally keeps only the access
78
+ * token in memory. When the access token's `exp` passes, the next access
79
+ * becomes anonymous and the user re-handshakes from the SaaS app on demand.
80
+ * - NO BroadcastChannel cross-tab coordination. Each docs tab owns its
81
+ * own session — opening a new tab is anonymous until it gets its own
82
+ * `?code=` redirect.
83
+ *
84
+ * These choices are deliberate: the docs site is a session RECEIVER
85
+ * (never a session OWNER), and the SaaS app is the authoritative source
86
+ * of authentication. Replicating the SaaS app's session-management stack
87
+ * here would force the docs Docusaurus bundle to ship inversify, the
88
+ * resources framework, and a refresh interceptor — all of which are
89
+ * about to be hardened (auth-hardening.md Slice A) in the SaaS app
90
+ * specifically because they are sensitive surfaces. Keeping the docs
91
+ * site free of those mechanisms keeps its attack surface tiny.
92
+ *
93
+ * The `apiBaseUrl` prop is supplied by the docs site at mount time
94
+ * (typically read from a small `appConfig.json` shipped alongside the
95
+ * Docusaurus bundle, or from an env var injected at build time). It is
96
+ * not pulled from `saas-frontend-lib`'s `useApplication()` because that
97
+ * would force this package to import the inversify-bound `appConfig`
98
+ * provider — exactly the dependency we are avoiding.
99
+ */
100
+ export declare const DocsAuthProvider: FC<{
101
+ children: ReactNode;
102
+ apiBaseUrl: string;
103
+ /**
104
+ * Optional materialized provider block for the active docs service.
105
+ * When omitted, the docs runtime exposes an empty provider registry.
106
+ */
107
+ frontendProviders?: FrontendProvidersBlock;
108
+ /**
109
+ * Runtime frontend service identity used for the `X-Frontend-Service-Name`
110
+ * header on exchange/handoff auth calls.
111
+ */
112
+ frontendServiceName?: string | null;
113
+ /**
114
+ * Optional override for tests. Production code constructs a
115
+ * `DocsAuthClient` from `apiBaseUrl` + the in-memory bearer.
116
+ */
117
+ client?: DocsAuthClient;
118
+ }>;
119
+ //# sourceMappingURL=DocsAuthContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocsAuthContext.d.ts","sourceRoot":"","sources":["../../../../src/runtime/DocsAuthContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,OAAO,EACZ,KAAK,EAAE,EACP,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAEnF,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAClF,OAAO,EAEL,KAAK,+BAA+B,EACrC,MAAM,sCAAsC,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,wBAAwB,EAAE,+BAA+B,CAAC;IACnE,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC9D,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,CAAC,oBAAoB,GAAG,IAAI,CACf,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,gBAAgB,EAAE,EAAE,CAAC;IAChC,QAAQ,EAAE,SAAS,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,sBAAsB,CAAC;IAC3C;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC;;;OAGG;IACH,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB,CA0GA,CAAC"}
@@ -0,0 +1,171 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useEffect, useMemo, useRef, useState, } from 'react';
3
+ import { decodeJwtClaims } from './decode-jwt-claims.js';
4
+ import { DocsAuthClient } from './docs-auth-client.js';
5
+ import { createFrontendTechdocProviderRegistry, } from './frontend-provider-registry.techdoc.js';
6
+ /**
7
+ * @internal — consumers should call `useDocsAuthSession()` instead of
8
+ * touching this directly. Exported so the matching hook can `useContext`
9
+ * it from a sibling module (Fast Refresh boundary).
10
+ */
11
+ export const DocsAuthContext = createContext(null);
12
+ /**
13
+ * In-memory React context owning the docs-site authenticated session
14
+ * (saas-technical-doc.md K-9b).
15
+ *
16
+ * Storage characteristics — locked by K-9b:
17
+ *
18
+ * - In-memory only. NO `localStorage`, NO `sessionStorage`, NO cookies.
19
+ * Closing the tab destroys the session.
20
+ * - NO refresh-token loop. `/auth/token/exchange` no longer returns a
21
+ * refresh token in the JSON body; SaaS-app receivers get refresh via the
22
+ * HttpOnly cookie channel, while docs intentionally keeps only the access
23
+ * token in memory. When the access token's `exp` passes, the next access
24
+ * becomes anonymous and the user re-handshakes from the SaaS app on demand.
25
+ * - NO BroadcastChannel cross-tab coordination. Each docs tab owns its
26
+ * own session — opening a new tab is anonymous until it gets its own
27
+ * `?code=` redirect.
28
+ *
29
+ * These choices are deliberate: the docs site is a session RECEIVER
30
+ * (never a session OWNER), and the SaaS app is the authoritative source
31
+ * of authentication. Replicating the SaaS app's session-management stack
32
+ * here would force the docs Docusaurus bundle to ship inversify, the
33
+ * resources framework, and a refresh interceptor — all of which are
34
+ * about to be hardened (auth-hardening.md Slice A) in the SaaS app
35
+ * specifically because they are sensitive surfaces. Keeping the docs
36
+ * site free of those mechanisms keeps its attack surface tiny.
37
+ *
38
+ * The `apiBaseUrl` prop is supplied by the docs site at mount time
39
+ * (typically read from a small `appConfig.json` shipped alongside the
40
+ * Docusaurus bundle, or from an env var injected at build time). It is
41
+ * not pulled from `saas-frontend-lib`'s `useApplication()` because that
42
+ * would force this package to import the inversify-bound `appConfig`
43
+ * provider — exactly the dependency we are avoiding.
44
+ */
45
+ export const DocsAuthProvider = ({ children, apiBaseUrl, frontendProviders, frontendServiceName = null, client: clientOverride, }) => {
46
+ const [session, setSession] = useState({ status: 'anonymous' });
47
+ /**
48
+ * `getAuthBearer` reads from a closure over `session` so the bearer
49
+ * always reflects the *current* state without re-creating the client
50
+ * on every state change. The `useMemo` below depends only on
51
+ * `apiBaseUrl` so the client identity is stable across re-renders,
52
+ * which keeps `useEffect` dependency arrays in swizzles honest.
53
+ */
54
+ const sessionRef = useLatestRef(session);
55
+ const client = useMemo(() => {
56
+ if (clientOverride)
57
+ return clientOverride;
58
+ return new DocsAuthClient({
59
+ apiBaseUrl,
60
+ frontendServiceName,
61
+ getAuthBearer: () => sessionRef.current.status === 'authenticated'
62
+ ? sessionRef.current.accessToken
63
+ : null,
64
+ });
65
+ }, [apiBaseUrl, frontendServiceName, clientOverride, sessionRef]);
66
+ const frontendProviderRegistry = useMemo(() => createFrontendTechdocProviderRegistry(frontendProviders), [frontendProviders]);
67
+ /**
68
+ * Auto-clear when the access token expires.
69
+ *
70
+ * Sets a single timer based on `claims.exp`; on tick, transitions the
71
+ * session back to `anonymous`. No polling, no refresh — the next
72
+ * gated action will trigger a re-handshake via `requestCrossFrontendHandoff`.
73
+ *
74
+ * The timer is recreated whenever the session transitions to
75
+ * `authenticated`, and cleared on unmount or on transition AWAY from
76
+ * `authenticated` to avoid stale timers firing into a fresh session.
77
+ */
78
+ useEffect(() => {
79
+ if (session.status !== 'authenticated')
80
+ return;
81
+ const msUntilExpiry = session.claims.exp * 1000 - Date.now();
82
+ if (msUntilExpiry <= 0) {
83
+ setSession({ status: 'anonymous' });
84
+ return;
85
+ }
86
+ const handle = setTimeout(() => {
87
+ setSession({ status: 'anonymous' });
88
+ }, msUntilExpiry);
89
+ return () => clearTimeout(handle);
90
+ }, [session]);
91
+ const beginExchange = useCallback(async (code) => {
92
+ setSession({ status: 'exchanging' });
93
+ try {
94
+ const response = await client.exchangeAuthCode(code);
95
+ // `returnPath` lives at the TOP LEVEL of `TokenExchangeResponseSchema`
96
+ // — NOT under `metadata`. Reading `response.metadata?.returnPath`
97
+ // (the previous shape this code targeted) is silently `undefined`
98
+ // because the schema has no `metadata` envelope. The previous
99
+ // implementation made every docs handoff fall through to the URL
100
+ // `?return=` value, defeating Step 5's "response value is the
101
+ // source of truth" contract. See H-3 in the Step 5 second
102
+ // deep-review log on `cross-app-jwt-propagation.md`.
103
+ const returnPath = response.returnPath ?? null;
104
+ const claims = decodeJwtClaims(response.accessToken);
105
+ if (!claims) {
106
+ /**
107
+ * The exchange succeeded but the returned token isn't decodable.
108
+ * Treat as anonymous rather than half-authenticated — the
109
+ * swizzles never have to handle a `claims === null &&
110
+ * status === 'authenticated'` corner case. We still surface
111
+ * `returnPath` so the exchange page can navigate the user
112
+ * somewhere sensible (e.g. `/`) instead of stranding them on
113
+ * `/auth/exchange`.
114
+ */
115
+ setSession({ status: 'anonymous' });
116
+ return { claims: null, returnPath };
117
+ }
118
+ setSession({ status: 'authenticated', accessToken: response.accessToken, claims });
119
+ return { claims, returnPath };
120
+ }
121
+ catch {
122
+ setSession({ status: 'anonymous' });
123
+ return { claims: null, returnPath: null };
124
+ }
125
+ }, [client]);
126
+ const clear = useCallback(() => {
127
+ setSession({ status: 'anonymous' });
128
+ }, []);
129
+ const value = useMemo(() => ({ session, client, frontendProviderRegistry, beginExchange, clear }), [session, client, frontendProviderRegistry, beginExchange, clear]);
130
+ return _jsx(DocsAuthContext.Provider, { value: value, children: children });
131
+ };
132
+ /**
133
+ * Tiny ref-of-latest-value helper. Avoids pulling in a utility lib for
134
+ * one ~6-line function. Used here so the `DocsAuthClient`'s
135
+ * `getAuthBearer` callback always sees the freshest session without
136
+ * re-instantiating the client.
137
+ *
138
+ * Implementation discipline (L-NEW-1 fix from the Step 6 third
139
+ * deep-review log): the previous implementation mutated `ref.current`
140
+ * DURING render (the `ref.current = value;` line ran in the function
141
+ * body), which violates React's "no side effects during render"
142
+ * invariant and produces inconsistent reads under concurrent
143
+ * rendering — a render that gets discarded by React's scheduler can
144
+ * still leave a stale `ref.current` behind for the next render.
145
+ *
146
+ * The fix is the textbook React 18 pattern: hold the value in a real
147
+ * `useRef`, then sync it inside `useEffect` after commit.
148
+ *
149
+ * Trade-off acknowledged: between render and effect-flush,
150
+ * `ref.current` lags by one render. That is SAFE for this consumer
151
+ * because `getAuthBearer()` is only invoked ASYNCHRONOUSLY (inside a
152
+ * `fetch()` call originated by `requestCrossFrontendHandoff()` /
153
+ * `exchangeAuthCode()`) — by the time the bearer is read, every
154
+ * `useEffect` from the triggering render has already flushed and
155
+ * `ref.current` reflects the latest committed session.
156
+ *
157
+ * Concretely: if the user clicks an org-API navbar entry that promotes
158
+ * the session from `anonymous` → `exchanging` → `authenticated`, the
159
+ * next fetch the client issues sees `authenticated` (ref synced after
160
+ * commit) — never the stale `exchanging` value, even though the
161
+ * `useMemo` factory may have captured the closure at the earlier
162
+ * render.
163
+ */
164
+ function useLatestRef(value) {
165
+ const ref = useRef(value);
166
+ useEffect(() => {
167
+ ref.current = value;
168
+ });
169
+ return ref;
170
+ }
171
+ //# sourceMappingURL=DocsAuthContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocsAuthContext.js","sourceRoot":"","sources":["../../../../src/runtime/DocsAuthContext.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n createContext,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type Context,\n type FC,\n type ReactNode,\n} from 'react';\nimport type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';\nimport { decodeJwtClaims } from './decode-jwt-claims';\nimport { DocsAuthClient } from './docs-auth-client';\nimport type { DocsAuthSession, DocsJwtClaims } from './docs-auth-session.schemas';\nimport {\n createFrontendTechdocProviderRegistry,\n type FrontendTechdocProviderRegistry,\n} from './frontend-provider-registry.techdoc';\n\n/**\n * Result of a `beginExchange()` call surfaced to `<AuthExchangePage>`.\n *\n * `claims` — decoded `DocsJwtClaims` (or `null` if the access token\n * came back undecodable — treated as anonymous).\n * `returnPath` — the issuer-supplied `returnPath` from the token exchange\n * response (cross-app-jwt-propagation.md Step 5). It is\n * a TOP-LEVEL field on `TokenExchangeResponseSchema` —\n * NOT nested under `metadata`. The earlier draft of this\n * JSDoc said \"metadata.returnPath\" and the implementation\n * read `response.metadata?.returnPath` (always undefined,\n * because no such key exists on the response). H-3 in the\n * Step 5 second deep-review log fixes both the docstring\n * and the read site.\n * `null` for OAuth2-callback codes (which never carry a\n * `returnPath` at all — discriminator invariant pinned by\n * `TokenExchangeResponseSchema` JSDoc); the exchange page\n * falls back to the URL `?return=` parameter in that case\n * (defense-in-depth).\n */\nexport interface DocsBeginExchangeResult {\n readonly claims: DocsJwtClaims | null;\n readonly returnPath: string | null;\n}\n\n/**\n * Public surface of `useDocsAuthSession()`.\n *\n * `session` — discriminated union (anonymous / exchanging / authenticated).\n * `client` — the singleton `DocsAuthClient` for authenticated docs\n * runtime code that needs to request a handoff to another\n * frontend. Anonymous docs gates deep-link to the SaaS app\n * because handoff issuance requires a bearer.\n * `frontendProviderRegistry`\n * — hydrated registry of the frontend-safe provider entries\n * materialized for the active docs service. Browser-only\n * docs widgets (search, analytics, future SDK protocols)\n * read from this instead of importing generated artifacts.\n * `beginExchange` — called by `<AuthExchangePage>` when a `?code=` arrives.\n * Returns the decoded claims AND the issuer-supplied\n * top-level `returnPath` from the exchange response\n * (the SOURCE OF TRUTH for post-exchange navigation;\n * the URL `?return=` is a defense-in-depth fallback\n * only). NOT `response.metadata.returnPath` — the\n * response has no `metadata` envelope (H-3 fix).\n * `clear` — called when the user explicitly closes the docs\n * session (e.g. a \"Sign out of docs\" link). NOT called\n * on tab close — ephemeral state evaporates on its own.\n */\nexport interface DocsAuthContextValue {\n readonly session: DocsAuthSession;\n readonly client: DocsAuthClient;\n readonly frontendProviderRegistry: FrontendTechdocProviderRegistry;\n beginExchange(code: string): Promise<DocsBeginExchangeResult>;\n clear(): void;\n}\n\n/**\n * @internal — consumers should call `useDocsAuthSession()` instead of\n * touching this directly. Exported so the matching hook can `useContext`\n * it from a sibling module (Fast Refresh boundary).\n */\nexport const DocsAuthContext: Context<DocsAuthContextValue | null> =\n createContext<DocsAuthContextValue | null>(null);\n\n/**\n * In-memory React context owning the docs-site authenticated session\n * (saas-technical-doc.md K-9b).\n *\n * Storage characteristics — locked by K-9b:\n *\n * - In-memory only. NO `localStorage`, NO `sessionStorage`, NO cookies.\n * Closing the tab destroys the session.\n * - NO refresh-token loop. `/auth/token/exchange` no longer returns a\n * refresh token in the JSON body; SaaS-app receivers get refresh via the\n * HttpOnly cookie channel, while docs intentionally keeps only the access\n * token in memory. When the access token's `exp` passes, the next access\n * becomes anonymous and the user re-handshakes from the SaaS app on demand.\n * - NO BroadcastChannel cross-tab coordination. Each docs tab owns its\n * own session — opening a new tab is anonymous until it gets its own\n * `?code=` redirect.\n *\n * These choices are deliberate: the docs site is a session RECEIVER\n * (never a session OWNER), and the SaaS app is the authoritative source\n * of authentication. Replicating the SaaS app's session-management stack\n * here would force the docs Docusaurus bundle to ship inversify, the\n * resources framework, and a refresh interceptor — all of which are\n * about to be hardened (auth-hardening.md Slice A) in the SaaS app\n * specifically because they are sensitive surfaces. Keeping the docs\n * site free of those mechanisms keeps its attack surface tiny.\n *\n * The `apiBaseUrl` prop is supplied by the docs site at mount time\n * (typically read from a small `appConfig.json` shipped alongside the\n * Docusaurus bundle, or from an env var injected at build time). It is\n * not pulled from `saas-frontend-lib`'s `useApplication()` because that\n * would force this package to import the inversify-bound `appConfig`\n * provider — exactly the dependency we are avoiding.\n */\nexport const DocsAuthProvider: FC<{\n children: ReactNode;\n apiBaseUrl: string;\n /**\n * Optional materialized provider block for the active docs service.\n * When omitted, the docs runtime exposes an empty provider registry.\n */\n frontendProviders?: FrontendProvidersBlock;\n /**\n * Runtime frontend service identity used for the `X-Frontend-Service-Name`\n * header on exchange/handoff auth calls.\n */\n frontendServiceName?: string | null;\n /**\n * Optional override for tests. Production code constructs a\n * `DocsAuthClient` from `apiBaseUrl` + the in-memory bearer.\n */\n client?: DocsAuthClient;\n}> = ({\n children,\n apiBaseUrl,\n frontendProviders,\n frontendServiceName = null,\n client: clientOverride,\n}) => {\n const [session, setSession] = useState<DocsAuthSession>({ status: 'anonymous' });\n\n /**\n * `getAuthBearer` reads from a closure over `session` so the bearer\n * always reflects the *current* state without re-creating the client\n * on every state change. The `useMemo` below depends only on\n * `apiBaseUrl` so the client identity is stable across re-renders,\n * which keeps `useEffect` dependency arrays in swizzles honest.\n */\n const sessionRef = useLatestRef(session);\n\n const client = useMemo<DocsAuthClient>(() => {\n if (clientOverride) return clientOverride;\n return new DocsAuthClient({\n apiBaseUrl,\n frontendServiceName,\n getAuthBearer: () =>\n sessionRef.current.status === 'authenticated'\n ? sessionRef.current.accessToken\n : null,\n });\n }, [apiBaseUrl, frontendServiceName, clientOverride, sessionRef]);\n const frontendProviderRegistry = useMemo(\n () => createFrontendTechdocProviderRegistry(frontendProviders),\n [frontendProviders],\n );\n\n /**\n * Auto-clear when the access token expires.\n *\n * Sets a single timer based on `claims.exp`; on tick, transitions the\n * session back to `anonymous`. No polling, no refresh — the next\n * gated action will trigger a re-handshake via `requestCrossFrontendHandoff`.\n *\n * The timer is recreated whenever the session transitions to\n * `authenticated`, and cleared on unmount or on transition AWAY from\n * `authenticated` to avoid stale timers firing into a fresh session.\n */\n useEffect(() => {\n if (session.status !== 'authenticated') return;\n const msUntilExpiry = session.claims.exp * 1000 - Date.now();\n if (msUntilExpiry <= 0) {\n setSession({ status: 'anonymous' });\n return;\n }\n const handle = setTimeout(() => {\n setSession({ status: 'anonymous' });\n }, msUntilExpiry);\n return () => clearTimeout(handle);\n }, [session]);\n\n const beginExchange = useCallback(\n async (code: string): Promise<DocsBeginExchangeResult> => {\n setSession({ status: 'exchanging' });\n try {\n const response = await client.exchangeAuthCode(code);\n // `returnPath` lives at the TOP LEVEL of `TokenExchangeResponseSchema`\n // — NOT under `metadata`. Reading `response.metadata?.returnPath`\n // (the previous shape this code targeted) is silently `undefined`\n // because the schema has no `metadata` envelope. The previous\n // implementation made every docs handoff fall through to the URL\n // `?return=` value, defeating Step 5's \"response value is the\n // source of truth\" contract. See H-3 in the Step 5 second\n // deep-review log on `cross-app-jwt-propagation.md`.\n const returnPath = response.returnPath ?? null;\n const claims = decodeJwtClaims(response.accessToken);\n if (!claims) {\n /**\n * The exchange succeeded but the returned token isn't decodable.\n * Treat as anonymous rather than half-authenticated — the\n * swizzles never have to handle a `claims === null &&\n * status === 'authenticated'` corner case. We still surface\n * `returnPath` so the exchange page can navigate the user\n * somewhere sensible (e.g. `/`) instead of stranding them on\n * `/auth/exchange`.\n */\n setSession({ status: 'anonymous' });\n return { claims: null, returnPath };\n }\n setSession({ status: 'authenticated', accessToken: response.accessToken, claims });\n return { claims, returnPath };\n } catch {\n setSession({ status: 'anonymous' });\n return { claims: null, returnPath: null };\n }\n },\n [client],\n );\n\n const clear = useCallback(() => {\n setSession({ status: 'anonymous' });\n }, []);\n\n const value = useMemo<DocsAuthContextValue>(\n () => ({ session, client, frontendProviderRegistry, beginExchange, clear }),\n [session, client, frontendProviderRegistry, beginExchange, clear],\n );\n\n return <DocsAuthContext.Provider value={value}>{children}</DocsAuthContext.Provider>;\n};\n\n/**\n * Tiny ref-of-latest-value helper. Avoids pulling in a utility lib for\n * one ~6-line function. Used here so the `DocsAuthClient`'s\n * `getAuthBearer` callback always sees the freshest session without\n * re-instantiating the client.\n *\n * Implementation discipline (L-NEW-1 fix from the Step 6 third\n * deep-review log): the previous implementation mutated `ref.current`\n * DURING render (the `ref.current = value;` line ran in the function\n * body), which violates React's \"no side effects during render\"\n * invariant and produces inconsistent reads under concurrent\n * rendering — a render that gets discarded by React's scheduler can\n * still leave a stale `ref.current` behind for the next render.\n *\n * The fix is the textbook React 18 pattern: hold the value in a real\n * `useRef`, then sync it inside `useEffect` after commit.\n *\n * Trade-off acknowledged: between render and effect-flush,\n * `ref.current` lags by one render. That is SAFE for this consumer\n * because `getAuthBearer()` is only invoked ASYNCHRONOUSLY (inside a\n * `fetch()` call originated by `requestCrossFrontendHandoff()` /\n * `exchangeAuthCode()`) — by the time the bearer is read, every\n * `useEffect` from the triggering render has already flushed and\n * `ref.current` reflects the latest committed session.\n *\n * Concretely: if the user clicks an org-API navbar entry that promotes\n * the session from `anonymous` → `exchanging` → `authenticated`, the\n * next fetch the client issues sees `authenticated` (ref synced after\n * commit) — never the stale `exchanging` value, even though the\n * `useMemo` factory may have captured the closure at the earlier\n * render.\n */\nfunction useLatestRef<T>(value: T): { current: T } {\n const ref = useRef<T>(value);\n useEffect(() => {\n ref.current = value;\n });\n return ref;\n}\n"]}
@@ -0,0 +1,39 @@
1
+ import { type DocsJwtClaims } from './docs-auth-session.schemas';
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 declare function decodeJwtClaims(token: string): DocsJwtClaims | null;
39
+ //# sourceMappingURL=decode-jwt-claims.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode-jwt-claims.d.ts","sourceRoot":"","sources":["../../../../src/runtime/decode-jwt-claims.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,6BAA6B,CAAC;AAErC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAoCnE"}