@tai42/studio-sdk 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +15 -0
- package/README.md +318 -0
- package/SECURITY.md +51 -0
- package/dist/components/badge.d.ts +13 -0
- package/dist/components/badge.d.ts.map +1 -0
- package/dist/components/badge.js +14 -0
- package/dist/components/badge.js.map +1 -0
- package/dist/components/checkbox.d.ts +24 -0
- package/dist/components/checkbox.d.ts.map +1 -0
- package/dist/components/checkbox.js +31 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/code-block.d.ts +7 -0
- package/dist/components/code-block.d.ts.map +1 -0
- package/dist/components/code-block.js +23 -0
- package/dist/components/code-block.js.map +1 -0
- package/dist/components/completion-input.d.ts +31 -0
- package/dist/components/completion-input.d.ts.map +1 -0
- package/dist/components/completion-input.js +61 -0
- package/dist/components/completion-input.js.map +1 -0
- package/dist/components/components.css +2212 -0
- package/dist/components/confirm-dialog.d.ts +40 -0
- package/dist/components/confirm-dialog.d.ts.map +1 -0
- package/dist/components/confirm-dialog.js +11 -0
- package/dist/components/confirm-dialog.js.map +1 -0
- package/dist/components/control-styles.d.ts +12 -0
- package/dist/components/control-styles.d.ts.map +1 -0
- package/dist/components/control-styles.js +14 -0
- package/dist/components/control-styles.js.map +1 -0
- package/dist/components/copy-field.d.ts +8 -0
- package/dist/components/copy-field.d.ts.map +1 -0
- package/dist/components/copy-field.js +106 -0
- package/dist/components/copy-field.js.map +1 -0
- package/dist/components/dialog.d.ts +21 -0
- package/dist/components/dialog.d.ts.map +1 -0
- package/dist/components/dialog.js +25 -0
- package/dist/components/dialog.js.map +1 -0
- package/dist/components/drawer.d.ts +22 -0
- package/dist/components/drawer.d.ts.map +1 -0
- package/dist/components/drawer.js +32 -0
- package/dist/components/drawer.js.map +1 -0
- package/dist/components/error-boundary.d.ts +33 -0
- package/dist/components/error-boundary.d.ts.map +1 -0
- package/dist/components/error-boundary.js +36 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/extension-combo-builder.d.ts +70 -0
- package/dist/components/extension-combo-builder.d.ts.map +1 -0
- package/dist/components/extension-combo-builder.js +154 -0
- package/dist/components/extension-combo-builder.js.map +1 -0
- package/dist/components/extension-grouping.d.ts +65 -0
- package/dist/components/extension-grouping.d.ts.map +1 -0
- package/dist/components/extension-grouping.js +78 -0
- package/dist/components/extension-grouping.js.map +1 -0
- package/dist/components/extension-picker.d.ts +35 -0
- package/dist/components/extension-picker.d.ts.map +1 -0
- package/dist/components/extension-picker.js +31 -0
- package/dist/components/extension-picker.js.map +1 -0
- package/dist/components/external-link.d.ts +19 -0
- package/dist/components/external-link.d.ts.map +1 -0
- package/dist/components/external-link.js +12 -0
- package/dist/components/external-link.js.map +1 -0
- package/dist/components/field.d.ts +64 -0
- package/dist/components/field.d.ts.map +1 -0
- package/dist/components/field.js +56 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/fleet-report.d.ts +39 -0
- package/dist/components/fleet-report.d.ts.map +1 -0
- package/dist/components/fleet-report.js +50 -0
- package/dist/components/fleet-report.js.map +1 -0
- package/dist/components/fonts.css +22 -0
- package/dist/components/icons.d.ts +142 -0
- package/dist/components/icons.d.ts.map +1 -0
- package/dist/components/icons.js +139 -0
- package/dist/components/icons.js.map +1 -0
- package/dist/components/inputs.d.ts +52 -0
- package/dist/components/inputs.d.ts.map +1 -0
- package/dist/components/inputs.js +35 -0
- package/dist/components/inputs.js.map +1 -0
- package/dist/components/json-diff.d.ts +40 -0
- package/dist/components/json-diff.d.ts.map +1 -0
- package/dist/components/json-diff.js +119 -0
- package/dist/components/json-diff.js.map +1 -0
- package/dist/components/json-tree.d.ts +8 -0
- package/dist/components/json-tree.d.ts.map +1 -0
- package/dist/components/json-tree.js +70 -0
- package/dist/components/json-tree.js.map +1 -0
- package/dist/components/modal-focus.d.ts +15 -0
- package/dist/components/modal-focus.d.ts.map +1 -0
- package/dist/components/modal-focus.js +136 -0
- package/dist/components/modal-focus.js.map +1 -0
- package/dist/components/page-header.d.ts +56 -0
- package/dist/components/page-header.d.ts.map +1 -0
- package/dist/components/page-header.js +24 -0
- package/dist/components/page-header.js.map +1 -0
- package/dist/components/primitives.d.ts +186 -0
- package/dist/components/primitives.d.ts.map +1 -0
- package/dist/components/primitives.js +204 -0
- package/dist/components/primitives.js.map +1 -0
- package/dist/components/progress-bar.d.ts +29 -0
- package/dist/components/progress-bar.d.ts.map +1 -0
- package/dist/components/progress-bar.js +48 -0
- package/dist/components/progress-bar.js.map +1 -0
- package/dist/components/radio-group.d.ts +37 -0
- package/dist/components/radio-group.d.ts.map +1 -0
- package/dist/components/radio-group.js +56 -0
- package/dist/components/radio-group.js.map +1 -0
- package/dist/components/reveal-input.d.ts +13 -0
- package/dist/components/reveal-input.d.ts.map +1 -0
- package/dist/components/reveal-input.js +31 -0
- package/dist/components/reveal-input.js.map +1 -0
- package/dist/components/scroll-region.d.ts +85 -0
- package/dist/components/scroll-region.d.ts.map +1 -0
- package/dist/components/scroll-region.js +574 -0
- package/dist/components/scroll-region.js.map +1 -0
- package/dist/components/select.d.ts +53 -0
- package/dist/components/select.d.ts.map +1 -0
- package/dist/components/select.js +33 -0
- package/dist/components/select.js.map +1 -0
- package/dist/components/table.d.ts +59 -0
- package/dist/components/table.d.ts.map +1 -0
- package/dist/components/table.js +20 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/tabs.d.ts +15 -0
- package/dist/components/tabs.d.ts.map +1 -0
- package/dist/components/tabs.js +13 -0
- package/dist/components/tabs.js.map +1 -0
- package/dist/components/tags.d.ts +46 -0
- package/dist/components/tags.d.ts.map +1 -0
- package/dist/components/tags.js +68 -0
- package/dist/components/tags.js.map +1 -0
- package/dist/components/tokens.css +392 -0
- package/dist/components/tokens.d.ts +15 -0
- package/dist/components/tokens.d.ts.map +1 -0
- package/dist/components/tokens.js +101 -0
- package/dist/components/tokens.js.map +1 -0
- package/dist/components/tool-picker.d.ts +24 -0
- package/dist/components/tool-picker.d.ts.map +1 -0
- package/dist/components/tool-picker.js +97 -0
- package/dist/components/tool-picker.js.map +1 -0
- package/dist/components/tooltip.d.ts +17 -0
- package/dist/components/tooltip.d.ts.map +1 -0
- package/dist/components/tooltip.js +18 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/components/version-history-panel.d.ts +71 -0
- package/dist/components/version-history-panel.d.ts.map +1 -0
- package/dist/components/version-history-panel.js +148 -0
- package/dist/components/version-history-panel.js.map +1 -0
- package/dist/download.d.ts +7 -0
- package/dist/download.d.ts.map +1 -0
- package/dist/download.js +18 -0
- package/dist/download.js.map +1 -0
- package/dist/element-slot.d.ts +4 -0
- package/dist/element-slot.d.ts.map +1 -0
- package/dist/element-slot.js +20 -0
- package/dist/element-slot.js.map +1 -0
- package/dist/elicitation/ElicitationForm.d.ts +34 -0
- package/dist/elicitation/ElicitationForm.d.ts.map +1 -0
- package/dist/elicitation/ElicitationForm.js +24 -0
- package/dist/elicitation/ElicitationForm.js.map +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +15 -0
- package/dist/errors.js.map +1 -0
- package/dist/extension-combos.d.ts +13 -0
- package/dist/extension-combos.d.ts.map +1 -0
- package/dist/extension-combos.js +9 -0
- package/dist/extension-combos.js.map +1 -0
- package/dist/hooks/useApi.d.ts +1222 -0
- package/dist/hooks/useApi.d.ts.map +1 -0
- package/dist/hooks/useApi.js +22 -0
- package/dist/hooks/useApi.js.map +1 -0
- package/dist/hooks/useAuth.d.ts +13 -0
- package/dist/hooks/useAuth.d.ts.map +1 -0
- package/dist/hooks/useAuth.js +53 -0
- package/dist/hooks/useAuth.js.map +1 -0
- package/dist/hooks/useBreakpoint.d.ts +10 -0
- package/dist/hooks/useBreakpoint.d.ts.map +1 -0
- package/dist/hooks/useBreakpoint.js +66 -0
- package/dist/hooks/useBreakpoint.js.map +1 -0
- package/dist/hooks/useCapabilities.d.ts +88 -0
- package/dist/hooks/useCapabilities.d.ts.map +1 -0
- package/dist/hooks/useCapabilities.js +203 -0
- package/dist/hooks/useCapabilities.js.map +1 -0
- package/dist/hooks/useSse.d.ts +13 -0
- package/dist/hooks/useSse.d.ts.map +1 -0
- package/dist/hooks/useSse.js +298 -0
- package/dist/hooks/useSse.js.map +1 -0
- package/dist/hooks/useTheme.d.ts +20 -0
- package/dist/hooks/useTheme.d.ts.map +1 -0
- package/dist/hooks/useTheme.js +97 -0
- package/dist/hooks/useTheme.js.map +1 -0
- package/dist/hooks/useUnauthorized.d.ts +3 -0
- package/dist/hooks/useUnauthorized.d.ts.map +1 -0
- package/dist/hooks/useUnauthorized.js +18 -0
- package/dist/hooks/useUnauthorized.js.map +1 -0
- package/dist/host.d.ts +11 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +10 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation/context.d.ts +25 -0
- package/dist/navigation/context.d.ts.map +1 -0
- package/dist/navigation/context.js +45 -0
- package/dist/navigation/context.js.map +1 -0
- package/dist/navigation/index.d.ts +4 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +2 -0
- package/dist/navigation/index.js.map +1 -0
- package/dist/navigation/types.d.ts +97 -0
- package/dist/navigation/types.d.ts.map +1 -0
- package/dist/navigation/types.js +16 -0
- package/dist/navigation/types.js.map +1 -0
- package/dist/plugin/host-state.d.ts +45 -0
- package/dist/plugin/host-state.d.ts.map +1 -0
- package/dist/plugin/host-state.js +62 -0
- package/dist/plugin/host-state.js.map +1 -0
- package/dist/plugin/registry.d.ts +35 -0
- package/dist/plugin/registry.d.ts.map +1 -0
- package/dist/plugin/registry.js +129 -0
- package/dist/plugin/registry.js.map +1 -0
- package/dist/plugin/types.d.ts +165 -0
- package/dist/plugin/types.d.ts.map +1 -0
- package/dist/plugin/types.js +2 -0
- package/dist/plugin/types.js.map +1 -0
- package/dist/plugin/version.d.ts +34 -0
- package/dist/plugin/version.d.ts.map +1 -0
- package/dist/plugin/version.js +37 -0
- package/dist/plugin/version.js.map +1 -0
- package/dist/query-keys.d.ts +31 -0
- package/dist/query-keys.d.ts.map +1 -0
- package/dist/query-keys.js +31 -0
- package/dist/query-keys.js.map +1 -0
- package/dist/schema-editor/SchemaEditor.d.ts +43 -0
- package/dist/schema-editor/SchemaEditor.d.ts.map +1 -0
- package/dist/schema-editor/SchemaEditor.js +106 -0
- package/dist/schema-editor/SchemaEditor.js.map +1 -0
- package/dist/schema-editor/index.d.ts +10 -0
- package/dist/schema-editor/index.d.ts.map +1 -0
- package/dist/schema-editor/index.js +8 -0
- package/dist/schema-editor/index.js.map +1 -0
- package/dist/schema-editor/lint.d.ts +37 -0
- package/dist/schema-editor/lint.d.ts.map +1 -0
- package/dist/schema-editor/lint.js +49 -0
- package/dist/schema-editor/lint.js.map +1 -0
- package/dist/schema-form/SchemaForm.d.ts +70 -0
- package/dist/schema-form/SchemaForm.d.ts.map +1 -0
- package/dist/schema-form/SchemaForm.js +13 -0
- package/dist/schema-form/SchemaForm.js.map +1 -0
- package/dist/schema-form/array-field.d.ts +20 -0
- package/dist/schema-form/array-field.d.ts.map +1 -0
- package/dist/schema-form/array-field.js +39 -0
- package/dist/schema-form/array-field.js.map +1 -0
- package/dist/schema-form/classify.d.ts +87 -0
- package/dist/schema-form/classify.d.ts.map +1 -0
- package/dist/schema-form/classify.js +199 -0
- package/dist/schema-form/classify.js.map +1 -0
- package/dist/schema-form/context.d.ts +4 -0
- package/dist/schema-form/context.d.ts.map +1 -0
- package/dist/schema-form/context.js +14 -0
- package/dist/schema-form/context.js.map +1 -0
- package/dist/schema-form/default-value.d.ts +8 -0
- package/dist/schema-form/default-value.d.ts.map +1 -0
- package/dist/schema-form/default-value.js +64 -0
- package/dist/schema-form/default-value.js.map +1 -0
- package/dist/schema-form/enum-field.d.ts +19 -0
- package/dist/schema-form/enum-field.d.ts.map +1 -0
- package/dist/schema-form/enum-field.js +26 -0
- package/dist/schema-form/enum-field.js.map +1 -0
- package/dist/schema-form/field-group.d.ts +9 -0
- package/dist/schema-form/field-group.d.ts.map +1 -0
- package/dist/schema-form/field-group.js +32 -0
- package/dist/schema-form/field-group.js.map +1 -0
- package/dist/schema-form/field-node.d.ts +24 -0
- package/dist/schema-form/field-node.d.ts.map +1 -0
- package/dist/schema-form/field-node.js +51 -0
- package/dist/schema-form/field-node.js.map +1 -0
- package/dist/schema-form/index.d.ts +13 -0
- package/dist/schema-form/index.d.ts.map +1 -0
- package/dist/schema-form/index.js +11 -0
- package/dist/schema-form/index.js.map +1 -0
- package/dist/schema-form/media-field.d.ts +24 -0
- package/dist/schema-form/media-field.d.ts.map +1 -0
- package/dist/schema-form/media-field.js +169 -0
- package/dist/schema-form/media-field.js.map +1 -0
- package/dist/schema-form/media.d.ts +40 -0
- package/dist/schema-form/media.d.ts.map +1 -0
- package/dist/schema-form/media.js +65 -0
- package/dist/schema-form/media.js.map +1 -0
- package/dist/schema-form/object-fields.d.ts +20 -0
- package/dist/schema-form/object-fields.d.ts.map +1 -0
- package/dist/schema-form/object-fields.js +24 -0
- package/dist/schema-form/object-fields.js.map +1 -0
- package/dist/schema-form/resolve.d.ts +41 -0
- package/dist/schema-form/resolve.d.ts.map +1 -0
- package/dist/schema-form/resolve.js +102 -0
- package/dist/schema-form/resolve.js.map +1 -0
- package/dist/schema-form/string-field.d.ts +20 -0
- package/dist/schema-form/string-field.d.ts.map +1 -0
- package/dist/schema-form/string-field.js +68 -0
- package/dist/schema-form/string-field.js.map +1 -0
- package/dist/schema-form/styles.d.ts +18 -0
- package/dist/schema-form/styles.d.ts.map +1 -0
- package/dist/schema-form/styles.js +18 -0
- package/dist/schema-form/styles.js.map +1 -0
- package/dist/schema-form/types.d.ts +67 -0
- package/dist/schema-form/types.d.ts.map +1 -0
- package/dist/schema-form/types.js +10 -0
- package/dist/schema-form/types.js.map +1 -0
- package/dist/schema-form/union-field.d.ts +24 -0
- package/dist/schema-form/union-field.d.ts.map +1 -0
- package/dist/schema-form/union-field.js +55 -0
- package/dist/schema-form/union-field.js.map +1 -0
- package/dist/schema-form/union.d.ts +4 -0
- package/dist/schema-form/union.d.ts.map +1 -0
- package/dist/schema-form/union.js +68 -0
- package/dist/schema-form/union.js.map +1 -0
- package/dist/schema-form/unsupported-notice.d.ts +18 -0
- package/dist/schema-form/unsupported-notice.d.ts.map +1 -0
- package/dist/schema-form/unsupported-notice.js +8 -0
- package/dist/schema-form/unsupported-notice.js.map +1 -0
- package/dist/schema-form/validate.d.ts +18 -0
- package/dist/schema-form/validate.d.ts.map +1 -0
- package/dist/schema-form/validate.js +162 -0
- package/dist/schema-form/validate.js.map +1 -0
- package/dist/structured-output/StructuredOutput.d.ts +27 -0
- package/dist/structured-output/StructuredOutput.d.ts.map +1 -0
- package/dist/structured-output/StructuredOutput.js +41 -0
- package/dist/structured-output/StructuredOutput.js.map +1 -0
- package/dist/testing.d.ts +80 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +207 -0
- package/dist/testing.js.map +1 -0
- package/package.json +101 -0
- package/src/components/badge.tsx +31 -0
- package/src/components/checkbox.tsx +93 -0
- package/src/components/code-block.tsx +37 -0
- package/src/components/completion-input.tsx +137 -0
- package/src/components/components.css +2212 -0
- package/src/components/confirm-dialog.tsx +77 -0
- package/src/components/control-styles.ts +14 -0
- package/src/components/copy-field.tsx +178 -0
- package/src/components/dialog.tsx +76 -0
- package/src/components/drawer.tsx +83 -0
- package/src/components/error-boundary.tsx +56 -0
- package/src/components/extension-combo-builder.tsx +305 -0
- package/src/components/extension-grouping.ts +116 -0
- package/src/components/extension-picker.tsx +97 -0
- package/src/components/external-link.tsx +40 -0
- package/src/components/field.tsx +169 -0
- package/src/components/fleet-report.tsx +136 -0
- package/src/components/fonts.css +22 -0
- package/src/components/icons.tsx +504 -0
- package/src/components/inputs.tsx +125 -0
- package/src/components/json-diff.tsx +192 -0
- package/src/components/json-tree.tsx +129 -0
- package/src/components/modal-focus.ts +150 -0
- package/src/components/page-header.tsx +107 -0
- package/src/components/primitives.tsx +489 -0
- package/src/components/progress-bar.tsx +100 -0
- package/src/components/radio-group.tsx +160 -0
- package/src/components/reveal-input.tsx +87 -0
- package/src/components/scroll-region.tsx +703 -0
- package/src/components/select.tsx +149 -0
- package/src/components/table.tsx +94 -0
- package/src/components/tabs.tsx +51 -0
- package/src/components/tags.tsx +137 -0
- package/src/components/tokens.css +392 -0
- package/src/components/tokens.ts +102 -0
- package/src/components/tool-picker.tsx +175 -0
- package/src/components/tooltip.tsx +46 -0
- package/src/components/version-history-panel.tsx +407 -0
- package/src/css.d.ts +2 -0
- package/src/download.ts +17 -0
- package/src/element-slot.ts +22 -0
- package/src/elicitation/ElicitationForm.tsx +87 -0
- package/src/errors.ts +12 -0
- package/src/extension-combos.ts +18 -0
- package/src/hooks/useApi.ts +27 -0
- package/src/hooks/useAuth.ts +67 -0
- package/src/hooks/useBreakpoint.ts +78 -0
- package/src/hooks/useCapabilities.ts +242 -0
- package/src/hooks/useSse.ts +314 -0
- package/src/hooks/useTheme.ts +133 -0
- package/src/hooks/useUnauthorized.ts +20 -0
- package/src/host.ts +15 -0
- package/src/index.ts +267 -0
- package/src/navigation/context.tsx +90 -0
- package/src/navigation/index.ts +9 -0
- package/src/navigation/types.ts +90 -0
- package/src/plugin/host-state.ts +93 -0
- package/src/plugin/registry.ts +174 -0
- package/src/plugin/types.ts +178 -0
- package/src/plugin/version.ts +44 -0
- package/src/query-keys.ts +34 -0
- package/src/schema-editor/SchemaEditor.tsx +186 -0
- package/src/schema-editor/index.ts +9 -0
- package/src/schema-editor/lint.ts +70 -0
- package/src/schema-form/SchemaForm.tsx +107 -0
- package/src/schema-form/array-field.tsx +110 -0
- package/src/schema-form/classify.ts +302 -0
- package/src/schema-form/context.ts +17 -0
- package/src/schema-form/default-value.ts +65 -0
- package/src/schema-form/enum-field.tsx +65 -0
- package/src/schema-form/field-group.tsx +71 -0
- package/src/schema-form/field-node.tsx +161 -0
- package/src/schema-form/index.ts +12 -0
- package/src/schema-form/media-field.tsx +281 -0
- package/src/schema-form/media.ts +69 -0
- package/src/schema-form/object-fields.tsx +74 -0
- package/src/schema-form/resolve.ts +114 -0
- package/src/schema-form/string-field.tsx +161 -0
- package/src/schema-form/styles.ts +20 -0
- package/src/schema-form/types.ts +71 -0
- package/src/schema-form/union-field.tsx +122 -0
- package/src/schema-form/union.ts +78 -0
- package/src/schema-form/unsupported-notice.tsx +39 -0
- package/src/schema-form/validate.ts +183 -0
- package/src/structured-output/StructuredOutput.tsx +81 -0
- package/src/testing.ts +235 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useAuth` — the API-key credential, held IN MEMORY by default (lost on reload →
|
|
3
|
+
* re-enter). An explicit "remember on this device" opt-in persists to
|
|
4
|
+
* sessionStorage. localStorage is NEVER written: the README documents the
|
|
5
|
+
* XSS trade-off and why localStorage persistence is not offered.
|
|
6
|
+
*/
|
|
7
|
+
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
|
8
|
+
import type { ReactNode } from 'react';
|
|
9
|
+
import { createElement } from 'react';
|
|
10
|
+
|
|
11
|
+
const SESSION_KEY = 'tai-studio.apiKey';
|
|
12
|
+
|
|
13
|
+
export interface AuthState {
|
|
14
|
+
readonly token: string | null;
|
|
15
|
+
readonly isAuthenticated: boolean;
|
|
16
|
+
/** Store the key; `remember` persists to sessionStorage for this device. */
|
|
17
|
+
readonly login: (token: string, remember: boolean) => void;
|
|
18
|
+
readonly logout: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const AuthContext = createContext<AuthState | null>(null);
|
|
22
|
+
|
|
23
|
+
function readSession(): string | null {
|
|
24
|
+
try {
|
|
25
|
+
return globalThis.sessionStorage.getItem(SESSION_KEY);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
32
|
+
// Seed from sessionStorage (a prior "remember" opt-in) if present.
|
|
33
|
+
const [token, setToken] = useState<string | null>(() => readSession());
|
|
34
|
+
|
|
35
|
+
const login = useCallback((next: string, remember: boolean) => {
|
|
36
|
+
setToken(next);
|
|
37
|
+
try {
|
|
38
|
+
if (remember) globalThis.sessionStorage.setItem(SESSION_KEY, next);
|
|
39
|
+
else globalThis.sessionStorage.removeItem(SESSION_KEY);
|
|
40
|
+
// localStorage is intentionally never touched.
|
|
41
|
+
} catch {
|
|
42
|
+
// Storage unavailable (private mode) — the in-memory token still works.
|
|
43
|
+
}
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
const logout = useCallback(() => {
|
|
47
|
+
setToken(null);
|
|
48
|
+
try {
|
|
49
|
+
globalThis.sessionStorage.removeItem(SESSION_KEY);
|
|
50
|
+
} catch {
|
|
51
|
+
/* ignore */
|
|
52
|
+
}
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const value = useMemo<AuthState>(
|
|
56
|
+
() => ({ token, isAuthenticated: token !== null, login, logout }),
|
|
57
|
+
[token, login, logout],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return createElement(AuthContext.Provider, { value }, children);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useAuth(): AuthState {
|
|
64
|
+
const state = useContext(AuthContext);
|
|
65
|
+
if (state === null) throw new Error('useAuth must be used within an <AuthProvider>');
|
|
66
|
+
return state;
|
|
67
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useBreakpoint` — the JS half of the responsive layer, reporting which of the
|
|
3
|
+
* four viewport bands is current and keeping the boundaries in lockstep with the
|
|
4
|
+
* `max-width` media queries in `components.css`. Layout that CSS can express
|
|
5
|
+
* stays in CSS; this hook is for the decisions CSS cannot make, where a surface
|
|
6
|
+
* must render DIFFERENT markup per band rather than restyle the same markup —
|
|
7
|
+
* mounting one pane of a master/detail split instead of two, say, rather than
|
|
8
|
+
* hiding the second.
|
|
9
|
+
*
|
|
10
|
+
* The queries are `max-width` on purpose: when nothing matches, the band
|
|
11
|
+
* resolves to `full`. That is also the answer in a DOM without `matchMedia`
|
|
12
|
+
* (jsdom), where a desktop default is the only safe assumption — a phone
|
|
13
|
+
* default would silently change what every feature's tests render.
|
|
14
|
+
*/
|
|
15
|
+
import { useMemo, useSyncExternalStore } from 'react';
|
|
16
|
+
|
|
17
|
+
export type Breakpoint = 'phone' | 'compact' | 'medium' | 'full';
|
|
18
|
+
|
|
19
|
+
export interface BreakpointState {
|
|
20
|
+
readonly band: Breakpoint;
|
|
21
|
+
/** True below 640 — the `phone` band, where a surface has one column to work in. */
|
|
22
|
+
readonly isPhone: boolean;
|
|
23
|
+
/** True below 1024 — too narrow for two panes side by side. */
|
|
24
|
+
readonly isSinglePane: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const PHONE_QUERY = '(max-width: 639px)';
|
|
28
|
+
const COMPACT_QUERY = '(max-width: 1023px)';
|
|
29
|
+
const MEDIUM_QUERY = '(max-width: 1279px)';
|
|
30
|
+
|
|
31
|
+
const BAND_QUERIES = [PHONE_QUERY, COMPACT_QUERY, MEDIUM_QUERY] as const;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `matchMedia` is feature-detected rather than guarded by a swallowed
|
|
35
|
+
* exception, so a real failure inside the media-query layer still throws.
|
|
36
|
+
*/
|
|
37
|
+
function hasMatchMedia(): boolean {
|
|
38
|
+
return typeof globalThis.matchMedia === 'function';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readBand(): Breakpoint {
|
|
42
|
+
if (!hasMatchMedia()) return 'full';
|
|
43
|
+
if (globalThis.matchMedia(PHONE_QUERY).matches) return 'phone';
|
|
44
|
+
if (globalThis.matchMedia(COMPACT_QUERY).matches) return 'compact';
|
|
45
|
+
if (globalThis.matchMedia(MEDIUM_QUERY).matches) return 'medium';
|
|
46
|
+
return 'full';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Subscribes to the three band boundaries. A `MediaQueryList` notifies only when
|
|
51
|
+
* its match state flips, so a resize inside a band never reaches React.
|
|
52
|
+
*/
|
|
53
|
+
function subscribe(onBandChange: () => void): () => void {
|
|
54
|
+
if (!hasMatchMedia()) {
|
|
55
|
+
return () => {
|
|
56
|
+
// No `matchMedia`: the band is fixed at `full` and can never change.
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const lists = BAND_QUERIES.map((query) => globalThis.matchMedia(query));
|
|
60
|
+
for (const list of lists) list.addEventListener('change', onBandChange);
|
|
61
|
+
return () => {
|
|
62
|
+
for (const list of lists) list.removeEventListener('change', onBandChange);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function useBreakpoint(): BreakpointState {
|
|
67
|
+
// The snapshot is a plain string, so React re-renders only when the BAND
|
|
68
|
+
// changes even though several boundaries can notify for one resize.
|
|
69
|
+
const band = useSyncExternalStore(subscribe, readBand, readBand);
|
|
70
|
+
return useMemo<BreakpointState>(
|
|
71
|
+
() => ({
|
|
72
|
+
band,
|
|
73
|
+
isPhone: band === 'phone',
|
|
74
|
+
isSinglePane: band === 'phone' || band === 'compact',
|
|
75
|
+
}),
|
|
76
|
+
[band],
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useCapabilities` — the caller's capability projection (`GET /api/auth/me`),
|
|
3
|
+
* fetched once per auth flip and held as SDK state. The UI is a projection of
|
|
4
|
+
* this: nav, pages, and plugin loading filter on what the projection says the
|
|
5
|
+
* caller can reach, and the server stays the sole authority (projection ⊆ gate).
|
|
6
|
+
*
|
|
7
|
+
* A PLAIN fetch state machine (`loading` / `ready` / `failed`), NOT TanStack
|
|
8
|
+
* Query, and for the same reason the login page fetches its methods with plain
|
|
9
|
+
* `fetch`: the shell wipes the query cache around auth flips, so a
|
|
10
|
+
* query-cached projection would be dropped underneath the components that gate
|
|
11
|
+
* on it. This provider owns the projection lifecycle instead.
|
|
12
|
+
*
|
|
13
|
+
* FAILED IS LOUD AND BLOCKING: on a `/me` failure the shell renders an
|
|
14
|
+
* `ErrorState` with `retry` in the nav region — never an optimistic full nav on
|
|
15
|
+
* error. The UI fails closed exactly as the server would.
|
|
16
|
+
*/
|
|
17
|
+
import {
|
|
18
|
+
createContext,
|
|
19
|
+
useCallback,
|
|
20
|
+
useContext,
|
|
21
|
+
useEffect,
|
|
22
|
+
useMemo,
|
|
23
|
+
useRef,
|
|
24
|
+
useState,
|
|
25
|
+
} from 'react';
|
|
26
|
+
import type { ReactNode } from 'react';
|
|
27
|
+
import { createElement } from 'react';
|
|
28
|
+
import type { MeProjection } from '@tai42/api-client';
|
|
29
|
+
|
|
30
|
+
import { useApi } from './useApi';
|
|
31
|
+
import { useAuth } from './useAuth';
|
|
32
|
+
import { useOnUnauthorized } from './useUnauthorized';
|
|
33
|
+
|
|
34
|
+
export type CapabilityState =
|
|
35
|
+
| { readonly status: 'loading' }
|
|
36
|
+
| { readonly status: 'ready'; readonly projection: MeProjection }
|
|
37
|
+
| { readonly status: 'failed'; readonly error: unknown };
|
|
38
|
+
|
|
39
|
+
export interface CapabilityContextValue {
|
|
40
|
+
readonly state: CapabilityState;
|
|
41
|
+
/** Re-fetch the projection (the nav-region `ErrorState`'s retry action). */
|
|
42
|
+
readonly retry: () => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const CapabilityContext = createContext<CapabilityContextValue | null>(null);
|
|
46
|
+
|
|
47
|
+
export function CapabilityProvider({ children }: { children: ReactNode }) {
|
|
48
|
+
const { isAuthenticated, token } = useAuth();
|
|
49
|
+
const api = useApi();
|
|
50
|
+
const onUnauthorized = useOnUnauthorized();
|
|
51
|
+
const [state, setState] = useState<CapabilityState>({ status: 'loading' });
|
|
52
|
+
const [attempt, setAttempt] = useState(0);
|
|
53
|
+
|
|
54
|
+
// Re-project when the ACTIVE CREDENTIAL changes, not only on the authenticated
|
|
55
|
+
// flip: a credential swap that stays authenticated (a fresh key pasted over a
|
|
56
|
+
// live session) must drop the prior identity's projection. Track the token via a
|
|
57
|
+
// ref and expose only a monotonic version to the fetch effect, so the raw secret
|
|
58
|
+
// never rides the effect's dependency array.
|
|
59
|
+
const tokenRef = useRef(token);
|
|
60
|
+
const [credentialVersion, setCredentialVersion] = useState(0);
|
|
61
|
+
if (tokenRef.current !== token) {
|
|
62
|
+
tokenRef.current = token;
|
|
63
|
+
setCredentialVersion((v) => v + 1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const retry = useCallback(() => {
|
|
67
|
+
setAttempt((n) => n + 1);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!isAuthenticated) {
|
|
72
|
+
// No credential → nothing to project. Reset to the neutral `loading` state
|
|
73
|
+
// so the next sign-in re-fetches cleanly; the shell renders the login page
|
|
74
|
+
// (not the capability-gated nav) while unauthenticated.
|
|
75
|
+
setState({ status: 'loading' });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
setState({ status: 'loading' });
|
|
80
|
+
api.getMe(controller.signal).then(
|
|
81
|
+
(projection) => {
|
|
82
|
+
if (controller.signal.aborted) return;
|
|
83
|
+
setState({ status: 'ready', projection });
|
|
84
|
+
},
|
|
85
|
+
(error: unknown) => {
|
|
86
|
+
if (controller.signal.aborted) return;
|
|
87
|
+
// A 401 means the stored credential is dead: route to login through the
|
|
88
|
+
// app's unauthorized handler (the same seam the api-client's 401 path
|
|
89
|
+
// drives) rather than settling into the retryable `failed` state against a
|
|
90
|
+
// known-bad key. Every other error stays a loud, retryable failure.
|
|
91
|
+
if (error instanceof Error && error.name === 'ApiUnauthorizedError') {
|
|
92
|
+
onUnauthorized();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
setState({ status: 'failed', error });
|
|
96
|
+
console.error(error);
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
return () => {
|
|
100
|
+
controller.abort();
|
|
101
|
+
};
|
|
102
|
+
}, [api, isAuthenticated, credentialVersion, attempt, onUnauthorized]);
|
|
103
|
+
|
|
104
|
+
const value = useMemo<CapabilityContextValue>(() => ({ state, retry }), [state, retry]);
|
|
105
|
+
|
|
106
|
+
return createElement(CapabilityContext.Provider, { value }, children);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function useCapabilities(): CapabilityContextValue {
|
|
110
|
+
const value = useContext(CapabilityContext);
|
|
111
|
+
if (value === null) {
|
|
112
|
+
throw new Error('useCapabilities must be used within a <CapabilityProvider>');
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Whether the projection is the synthetic TOTAL projection — the admin session
|
|
119
|
+
* (the condition-free ownerless `"*"` discriminator) and the gate-off local-dev
|
|
120
|
+
* projection alike. This is `projection.admin === true` and NOTHING else: a
|
|
121
|
+
* seeded editor/viewer carries `["*"]` scopes PLUS a jq fence, so keying on `"*"`
|
|
122
|
+
* scopes would hand every editor/viewer the unfiltered nav and a wall of 403s on
|
|
123
|
+
* the admin routes their fence denies. Keying on `admin` makes editor/viewer
|
|
124
|
+
* SCOPED sessions whose nav is filtered by their jq-exact `routes`, which is
|
|
125
|
+
* exactly right — and the gate-off synthetic projection (`admin: true`, empty
|
|
126
|
+
* `routes`) still reads as full, so an AC-disabled deployment shows everything.
|
|
127
|
+
*/
|
|
128
|
+
export function isFullProjection(projection: MeProjection): boolean {
|
|
129
|
+
return projection.admin;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Whether a concrete path is `prefix` itself or nested beneath it at a PATH-SEGMENT
|
|
134
|
+
* boundary — the client mirror of the server's segment-aware coverage rule
|
|
135
|
+
* (access_control/settings.py: `a == b or a.startswith(b + '/')`). Matching on a
|
|
136
|
+
* bare `startsWith` would let a sibling route like `/api/tools-schema` satisfy the
|
|
137
|
+
* `/api/tools` token; the boundary keeps them distinct.
|
|
138
|
+
*/
|
|
139
|
+
function pathUnderPrefix(path: string, prefix: string): boolean {
|
|
140
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Whether the projection reaches ANY route under one of `prefixes` — the
|
|
145
|
+
* declarative test behind nav filtering and plugin `requiredCapabilities`. A
|
|
146
|
+
* concrete `routes[].path` covers a prefix when it is that prefix or a
|
|
147
|
+
* segment-nested descendant of it; a dynamic `route_patterns[].pattern` covers a
|
|
148
|
+
* prefix only when it is a simple anchored open-wildcard pattern (`^<literal>.*$`)
|
|
149
|
+
* whose open literal overlaps the prefix at a segment boundary.
|
|
150
|
+
*
|
|
151
|
+
* Under-showing is safe (projection is UX; the server is the authority);
|
|
152
|
+
* over-showing is the bug. So any non-anchored or otherwise complex pattern
|
|
153
|
+
* (alternation, char class, bounded quantifier) is conservatively treated as NOT
|
|
154
|
+
* covering — its matched set is not a clean prefix, so we never infer reach from
|
|
155
|
+
* it.
|
|
156
|
+
*/
|
|
157
|
+
export function coversAnyRoute(projection: MeProjection, prefixes: readonly string[]): boolean {
|
|
158
|
+
return prefixes.some(
|
|
159
|
+
(prefix) =>
|
|
160
|
+
projection.routes.some((route) => pathUnderPrefix(route.path, prefix)) ||
|
|
161
|
+
projection.route_patterns.some((entry) => {
|
|
162
|
+
const open = patternOpenPrefix(entry.pattern);
|
|
163
|
+
// The open literal L means "every path starting with L is reachable". That
|
|
164
|
+
// set reaches something under `prefix` when the prefix itself lies in it
|
|
165
|
+
// (`prefix.startsWith(open)`) or when the literal already dives under the
|
|
166
|
+
// prefix at a segment boundary (`pathUnderPrefix(open, prefix)`).
|
|
167
|
+
return open !== null && (prefix.startsWith(open) || pathUnderPrefix(open, prefix));
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Whether the projection reaches a specific WRITE route — an EXACT `path` whose
|
|
174
|
+
* admitted `methods` include `method`. This is the method-aware companion to
|
|
175
|
+
* {@link coversAnyRoute}: a read/nav surface gates path-only (any method reaching a
|
|
176
|
+
* prefix means the surface is navigable), but an action that issues `method path`
|
|
177
|
+
* must gate on the method too, or a caller whose jq fence admits only GET on `path`
|
|
178
|
+
* would be shown a control that 403s on submit (projection ⊆ gate).
|
|
179
|
+
*
|
|
180
|
+
* Only concrete `routes` are consulted: dynamic `route_patterns` carry no method, so
|
|
181
|
+
* no write capability can be soundly inferred from them, and a synthetic TOTAL
|
|
182
|
+
* (admin) projection carries no concrete `routes` at all — callers short-circuit it
|
|
183
|
+
* with {@link isFullProjection} before reaching here. Under-showing is safe (the
|
|
184
|
+
* server stays the authority); over-showing an action the gate denies is the bug.
|
|
185
|
+
*/
|
|
186
|
+
export function coversRoute(projection: MeProjection, path: string, method: string): boolean {
|
|
187
|
+
return projection.routes.some((route) => route.path === path && route.methods.includes(method));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Whether a WRITE control may be shown/enabled for this caller — the fail-closed,
|
|
192
|
+
* method-aware gate the features fold together with any deployment-level read-only flag
|
|
193
|
+
* (the projection ⊆ gate invariant, in one place). It is `true` ONLY once the projection
|
|
194
|
+
* is `ready` AND either it is a full (admin / gate-off) projection or it reaches
|
|
195
|
+
* `method path`. While the projection is loading/failed it is `false` — a write control
|
|
196
|
+
* never enables before the gate is known (fail closed; not-ready ⇒ disabled/read-only).
|
|
197
|
+
*
|
|
198
|
+
* Only concrete `routes` carry methods, so a DYNAMIC (templated) write route — one the
|
|
199
|
+
* projection can represent only as a method-less `route_patterns` row — reads `false` for
|
|
200
|
+
* every non-admin caller and degrades that control to read-only. Under-showing is safe
|
|
201
|
+
* (the server stays the authority); over-showing a write the gate denies is the bug.
|
|
202
|
+
*/
|
|
203
|
+
export function coversWrite(state: CapabilityState, path: string, method: string): boolean {
|
|
204
|
+
return (
|
|
205
|
+
state.status === 'ready' &&
|
|
206
|
+
(isFullProjection(state.projection) || coversRoute(state.projection, path, method))
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Whether a WRITE control may be shown for the current caller — the single boundary
|
|
212
|
+
* primitive a feature calls to hide a control whose `method path` the caller's
|
|
213
|
+
* projection cannot reach. It reads the live capability state and folds it through
|
|
214
|
+
* {@link coversWrite}: `true` only once the projection is `ready` AND it is a full
|
|
215
|
+
* (admin / gate-off) projection or it reaches `method path`; while the projection is
|
|
216
|
+
* loading/failed it is `false` (fail closed — no write control before the gate is
|
|
217
|
+
* known). `method` defaults to `POST`, the common write verb; pass the exact verb for
|
|
218
|
+
* a PUT/PATCH/DELETE control.
|
|
219
|
+
*/
|
|
220
|
+
export function useCanWrite(path: string, method = 'POST'): boolean {
|
|
221
|
+
const { state } = useCapabilities();
|
|
222
|
+
return coversWrite(state, path, method);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const REGEX_METACHARACTERS = /[.*+?()[\]{}|\\^$]/;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The literal prefix of a simple anchored open-wildcard pattern (`^<literal>.*$`,
|
|
229
|
+
* the `$` optional), or `null` when the pattern is not exactly that shape. A
|
|
230
|
+
* returned literal `L` means "every path starting with `L` is reachable"; only
|
|
231
|
+
* that open-wildcard form yields a sound prefix, so a bare literal, an
|
|
232
|
+
* alternation, a char class, or any other construct declines (returns `null`).
|
|
233
|
+
*/
|
|
234
|
+
function patternOpenPrefix(pattern: string): string | null {
|
|
235
|
+
if (!pattern.startsWith('^')) return null;
|
|
236
|
+
let body = pattern.slice(1);
|
|
237
|
+
if (body.endsWith('$')) body = body.slice(0, -1);
|
|
238
|
+
if (!body.endsWith('.*')) return null;
|
|
239
|
+
body = body.slice(0, -2);
|
|
240
|
+
if (body.length === 0 || REGEX_METACHARACTERS.test(body)) return null;
|
|
241
|
+
return body;
|
|
242
|
+
}
|