@xemahq/ui-kernel 0.9.1 → 0.10.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/README.md +2 -2
- package/dist/lib/biome-host/biome-builders.d.ts.map +1 -1
- package/dist/lib/biome-host/biome-builders.js +3 -1
- package/dist/lib/biome-host/biome-builders.js.map +1 -1
- package/dist/lib/biome-host/biome-registry.d.ts.map +1 -1
- package/dist/lib/biome-host/biome-registry.js.map +1 -1
- package/dist/lib/biome-host/biome-scope-hooks.d.ts +11 -0
- package/dist/lib/biome-host/biome-scope-hooks.d.ts.map +1 -0
- package/dist/lib/biome-host/biome-scope-hooks.js +25 -0
- package/dist/lib/biome-host/biome-scope-hooks.js.map +1 -0
- package/dist/lib/biome-host/biome-scope.d.ts +1 -10
- package/dist/lib/biome-host/biome-scope.d.ts.map +1 -1
- package/dist/lib/biome-host/biome-scope.js +1 -24
- package/dist/lib/biome-host/biome-scope.js.map +1 -1
- package/dist/lib/biome-host/biome-scoped-query.d.ts +1 -1
- package/dist/lib/biome-host/biome-scoped-query.d.ts.map +1 -1
- package/dist/lib/biome-host/biome-scoped-query.js +1 -1
- package/dist/lib/biome-host/biome-scoped-query.js.map +1 -1
- package/dist/lib/biome-host/create-biome-orval-config.js +2 -2
- package/dist/lib/biome-host/create-biome-orval-config.js.map +1 -1
- package/dist/lib/biome-host/errors.js.map +1 -1
- package/dist/lib/biome-host/index.d.ts +1 -2
- package/dist/lib/biome-host/index.d.ts.map +1 -1
- package/dist/lib/biome-host/index.js +1 -2
- package/dist/lib/biome-host/index.js.map +1 -1
- package/dist/lib/biome-host/session-contributions.d.ts +9 -4
- package/dist/lib/biome-host/session-contributions.d.ts.map +1 -1
- package/dist/lib/capabilities/capability-context.d.ts +9 -0
- package/dist/lib/capabilities/capability-context.d.ts.map +1 -0
- package/dist/lib/capabilities/capability-context.js +13 -0
- package/dist/lib/capabilities/capability-context.js.map +1 -0
- package/dist/lib/capabilities/capability-provider.d.ts +2 -8
- package/dist/lib/capabilities/capability-provider.d.ts.map +1 -1
- package/dist/lib/capabilities/capability-provider.js +1 -12
- package/dist/lib/capabilities/capability-provider.js.map +1 -1
- package/dist/lib/capabilities/index.d.ts +1 -0
- package/dist/lib/capabilities/index.d.ts.map +1 -1
- package/dist/lib/capabilities/index.js +1 -0
- package/dist/lib/capabilities/index.js.map +1 -1
- package/dist/lib/capabilities/use-capability.d.ts.map +1 -1
- package/dist/lib/capabilities/use-capability.js +1 -1
- package/dist/lib/capabilities/use-capability.js.map +1 -1
- package/dist/lib/system-bus/deeplink.js.map +1 -1
- package/dist/lib/system-bus/host-ports.d.ts +2 -1
- package/dist/lib/system-bus/host-ports.d.ts.map +1 -1
- package/dist/lib/system-bus/intent-registry.d.ts.map +1 -1
- package/dist/lib/system-bus/intent-registry.js.map +1 -1
- package/dist/offline/index.d.ts +2 -1
- package/dist/offline/index.d.ts.map +1 -1
- package/dist/offline/index.js +2 -1
- package/dist/offline/index.js.map +1 -1
- package/dist/offline/offline-status-badge.js +1 -1
- package/dist/offline/offline-status-badge.js.map +1 -1
- package/dist/offline/outbox.js.map +1 -1
- package/dist/offline/react-hooks.d.ts +7 -0
- package/dist/offline/react-hooks.d.ts.map +1 -0
- package/dist/offline/react-hooks.js +25 -0
- package/dist/offline/react-hooks.js.map +1 -0
- package/dist/offline/react.d.ts +0 -4
- package/dist/offline/react.d.ts.map +1 -1
- package/dist/offline/react.js +2 -24
- package/dist/offline/react.js.map +1 -1
- package/dist/offline/storage.d.ts.map +1 -1
- package/dist/offline/storage.js +8 -5
- package/dist/offline/storage.js.map +1 -1
- package/dist/registry/index.d.ts +1 -0
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -0
- package/dist/registry/index.js.map +1 -1
- package/dist/registry/lib/primitives/SessionEventCard.d.ts.map +1 -1
- package/dist/registry/lib/primitives/SessionEventCard.js +1 -1
- package/dist/registry/lib/primitives/SessionEventCard.js.map +1 -1
- package/dist/registry/lib/session-context-builder.d.ts +10 -2
- package/dist/registry/lib/session-context-builder.d.ts.map +1 -1
- package/dist/registry/lib/session-context-builder.js +14 -7
- package/dist/registry/lib/session-context-builder.js.map +1 -1
- package/dist/registry/lib/session-context-provider.d.ts +1 -3
- package/dist/registry/lib/session-context-provider.d.ts.map +1 -1
- package/dist/registry/lib/session-context-provider.js +2 -15
- package/dist/registry/lib/session-context-provider.js.map +1 -1
- package/dist/registry/lib/session-context.d.ts +5 -0
- package/dist/registry/lib/session-context.d.ts.map +1 -0
- package/dist/registry/lib/session-context.js +15 -0
- package/dist/registry/lib/session-context.js.map +1 -0
- package/dist/registry/lib/session-selectors.d.ts.map +1 -1
- package/dist/registry/lib/session-selectors.js +3 -1
- package/dist/registry/lib/session-selectors.js.map +1 -1
- package/dist/ui/branding/app-branding-context.d.ts +9 -0
- package/dist/ui/branding/app-branding-context.d.ts.map +1 -0
- package/dist/ui/branding/app-branding-context.js +10 -0
- package/dist/ui/branding/app-branding-context.js.map +1 -0
- package/dist/ui/branding/app-branding-provider.d.ts +0 -6
- package/dist/ui/branding/app-branding-provider.d.ts.map +1 -1
- package/dist/ui/branding/app-branding-provider.js +1 -8
- package/dist/ui/branding/app-branding-provider.js.map +1 -1
- package/dist/ui/capture/schema-field-input.js +1 -1
- package/dist/ui/capture/schema-field-input.js.map +1 -1
- package/dist/ui/capture/use-prefill.d.ts.map +1 -1
- package/dist/ui/capture/use-prefill.js +3 -1
- package/dist/ui/capture/use-prefill.js.map +1 -1
- package/dist/ui/capture/use-wizard.d.ts +1 -1
- package/dist/ui/capture/use-wizard.d.ts.map +1 -1
- package/dist/ui/capture/use-wizard.js +1 -1
- package/dist/ui/capture/use-wizard.js.map +1 -1
- package/dist/ui/capture/wizard.d.ts.map +1 -1
- package/dist/ui/capture/wizard.js.map +1 -1
- package/dist/ui/index.d.ts +5 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +5 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/primitives/alert-dialog.js +1 -1
- package/dist/ui/primitives/alert-dialog.js.map +1 -1
- package/dist/ui/primitives/badge-variants.d.ts +4 -0
- package/dist/ui/primitives/badge-variants.d.ts.map +1 -0
- package/dist/ui/primitives/badge-variants.js +15 -0
- package/dist/ui/primitives/badge-variants.js.map +1 -0
- package/dist/ui/primitives/badge.d.ts +3 -5
- package/dist/ui/primitives/badge.d.ts.map +1 -1
- package/dist/ui/primitives/badge.js +2 -15
- package/dist/ui/primitives/badge.js.map +1 -1
- package/dist/ui/primitives/button-variants.d.ts +5 -0
- package/dist/ui/primitives/button-variants.d.ts.map +1 -0
- package/dist/ui/primitives/button-variants.js +24 -0
- package/dist/ui/primitives/button-variants.js.map +1 -0
- package/dist/ui/primitives/button.d.ts +3 -6
- package/dist/ui/primitives/button.d.ts.map +1 -1
- package/dist/ui/primitives/button.js +2 -24
- package/dist/ui/primitives/button.js.map +1 -1
- package/dist/ui/primitives/form-stepper.d.ts +1 -13
- package/dist/ui/primitives/form-stepper.d.ts.map +1 -1
- package/dist/ui/primitives/form-stepper.js +1 -6
- package/dist/ui/primitives/form-stepper.js.map +1 -1
- package/dist/ui/primitives/form-stepper.types.d.ts +14 -0
- package/dist/ui/primitives/form-stepper.types.d.ts.map +1 -0
- package/dist/ui/primitives/form-stepper.types.js +7 -0
- package/dist/ui/primitives/form-stepper.types.js.map +1 -0
- package/dist/ui/primitives/preview-device-frame.js +1 -1
- package/dist/ui/primitives/preview-device-frame.js.map +1 -1
- package/dist/ui/primitives/tag-multi-select.d.ts.map +1 -1
- package/dist/ui/primitives/tag-multi-select.js +1 -1
- package/dist/ui/primitives/tag-multi-select.js.map +1 -1
- package/dist/vista/index.d.ts +4 -0
- package/dist/vista/index.d.ts.map +1 -1
- package/dist/vista/index.js +4 -0
- package/dist/vista/index.js.map +1 -1
- package/dist/vista/surfaces/FilesSurface.d.ts +1 -12
- package/dist/vista/surfaces/FilesSurface.d.ts.map +1 -1
- package/dist/vista/surfaces/FilesSurface.js +2 -46
- package/dist/vista/surfaces/FilesSurface.js.map +1 -1
- package/dist/vista/surfaces/FormSurface.d.ts.map +1 -1
- package/dist/vista/surfaces/FormSurface.js +39 -12
- package/dist/vista/surfaces/FormSurface.js.map +1 -1
- package/dist/vista/surfaces/elicitation-transport.d.ts +12 -3
- package/dist/vista/surfaces/elicitation-transport.d.ts.map +1 -1
- package/dist/vista/surfaces/elicitation-transport.js +96 -3
- package/dist/vista/surfaces/elicitation-transport.js.map +1 -1
- package/dist/vista/surfaces/file-surface-utils.d.ts +13 -0
- package/dist/vista/surfaces/file-surface-utils.d.ts.map +1 -0
- package/dist/vista/surfaces/file-surface-utils.js +46 -0
- package/dist/vista/surfaces/file-surface-utils.js.map +1 -0
- package/dist/vista/surfaces/index.d.ts +4 -3
- package/dist/vista/surfaces/index.d.ts.map +1 -1
- package/dist/vista/surfaces/index.js +4 -81
- package/dist/vista/surfaces/index.js.map +1 -1
- package/dist/vista/surfaces/register-surfaces.d.ts +2 -0
- package/dist/vista/surfaces/register-surfaces.d.ts.map +1 -0
- package/dist/vista/surfaces/register-surfaces.js +80 -0
- package/dist/vista/surfaces/register-surfaces.js.map +1 -0
- package/dist/vista/surfaces/text-surfaces.js +1 -1
- package/dist/vista/surfaces/text-surfaces.js.map +1 -1
- package/dist/vista/vista-code-renderer-context.d.ts +9 -0
- package/dist/vista/vista-code-renderer-context.d.ts.map +1 -0
- package/dist/vista/vista-code-renderer-context.js +6 -0
- package/dist/vista/vista-code-renderer-context.js.map +1 -0
- package/dist/vista/vista-code-renderer.d.ts +1 -6
- package/dist/vista/vista-code-renderer.d.ts.map +1 -1
- package/dist/vista/vista-code-renderer.js +1 -5
- package/dist/vista/vista-code-renderer.js.map +1 -1
- package/dist/vista/vista-file-context.d.ts +8 -0
- package/dist/vista/vista-file-context.d.ts.map +1 -0
- package/dist/vista/vista-file-context.js +6 -0
- package/dist/vista/vista-file-context.js.map +1 -0
- package/dist/vista/vista-file-source.d.ts +1 -6
- package/dist/vista/vista-file-source.d.ts.map +1 -1
- package/dist/vista/vista-file-source.js +2 -5
- package/dist/vista/vista-file-source.js.map +1 -1
- package/dist/vista/vista-form-context.d.ts +13 -0
- package/dist/vista/vista-form-context.d.ts.map +1 -0
- package/dist/vista/vista-form-context.js +6 -0
- package/dist/vista/vista-form-context.js.map +1 -0
- package/dist/vista/vista-form-host.d.ts +1 -10
- package/dist/vista/vista-form-host.d.ts.map +1 -1
- package/dist/vista/vista-form-host.js +1 -5
- package/dist/vista/vista-form-host.js.map +1 -1
- package/dist/vista/vista-open-context.d.ts +1 -3
- package/dist/vista/vista-open-context.d.ts.map +1 -1
- package/dist/vista/vista-open-context.js +1 -5
- package/dist/vista/vista-open-context.js.map +1 -1
- package/dist/vista/vista-open-state.d.ts +5 -0
- package/dist/vista/vista-open-state.d.ts.map +1 -0
- package/dist/vista/vista-open-state.js +6 -0
- package/dist/vista/vista-open-state.js.map +1 -0
- package/dist/vista/vista-payloads.d.ts +2 -3
- package/dist/vista/vista-payloads.d.ts.map +1 -1
- package/dist/vista/vista-payloads.js +18 -21
- package/dist/vista/vista-payloads.js.map +1 -1
- package/dist/vista/vista-tools.d.ts.map +1 -1
- package/dist/vista/vista-tools.js.map +1 -1
- package/package.json +3 -3
- package/src/lib/biome-host/biome-builders.ts +4 -1
- package/src/lib/biome-host/biome-registry.ts +0 -1
- package/src/lib/biome-host/biome-scope-hooks.ts +39 -0
- package/src/lib/biome-host/biome-scope.tsx +1 -68
- package/src/lib/biome-host/biome-scoped-query.ts +4 -1
- package/src/lib/biome-host/create-biome-orval-config.ts +2 -2
- package/src/lib/biome-host/errors.ts +1 -1
- package/src/lib/biome-host/index.ts +1 -2
- package/src/lib/biome-host/session-contributions.ts +21 -18
- package/src/lib/capabilities/capability-context.ts +25 -0
- package/src/lib/capabilities/capability-provider.tsx +5 -34
- package/src/lib/capabilities/index.ts +1 -0
- package/src/lib/capabilities/use-capability.ts +4 -1
- package/src/lib/system-bus/deeplink.ts +1 -1
- package/src/lib/system-bus/host-ports.ts +2 -1
- package/src/lib/system-bus/intent-registry.ts +1 -2
- package/src/offline/index.ts +4 -2
- package/src/offline/offline-status-badge.tsx +1 -1
- package/src/offline/outbox.test.ts +23 -16
- package/src/offline/outbox.ts +1 -1
- package/src/offline/react-hooks.ts +37 -0
- package/src/offline/react.tsx +1 -40
- package/src/offline/storage.ts +11 -6
- package/src/registry/index.ts +1 -0
- package/src/registry/lib/primitives/SessionEventCard.tsx +4 -2
- package/src/registry/lib/session-context-builder.ts +33 -17
- package/src/registry/lib/session-context-provider.tsx +8 -25
- package/src/registry/lib/session-context.ts +21 -0
- package/src/registry/lib/session-selectors.ts +3 -1
- package/src/ui/branding/app-branding-context.ts +20 -0
- package/src/ui/branding/app-branding-provider.tsx +5 -27
- package/src/ui/capture/schema-field-input.tsx +7 -1
- package/src/ui/capture/use-prefill.ts +4 -1
- package/src/ui/capture/use-wizard.ts +2 -2
- package/src/ui/capture/wizard.tsx +2 -1
- package/src/ui/index.ts +7 -2
- package/src/ui/primitives/alert-dialog.tsx +1 -1
- package/src/ui/primitives/badge-variants.ts +21 -0
- package/src/ui/primitives/badge.tsx +3 -19
- package/src/ui/primitives/button-variants.ts +31 -0
- package/src/ui/primitives/button.tsx +3 -28
- package/src/ui/primitives/form-stepper.tsx +1 -16
- package/src/ui/primitives/form-stepper.types.ts +15 -0
- package/src/ui/primitives/preview-device-frame.tsx +1 -1
- package/src/ui/primitives/tag-multi-select.tsx +3 -4
- package/src/vista/form-payload.spec.ts +71 -25
- package/src/vista/index.ts +4 -0
- package/src/vista/surfaces/FilesSurface.tsx +2 -68
- package/src/vista/surfaces/FormSurface.tsx +125 -26
- package/src/vista/surfaces/elicitation-transport.spec.ts +205 -23
- package/src/vista/surfaces/elicitation-transport.ts +192 -14
- package/src/vista/surfaces/file-surface-utils.ts +62 -0
- package/src/vista/surfaces/index.ts +24 -0
- package/src/vista/surfaces/{index.tsx → register-surfaces.tsx} +1 -21
- package/src/vista/surfaces/text-surfaces.tsx +1 -1
- package/src/vista/vista-code-renderer-context.ts +16 -0
- package/src/vista/vista-code-renderer.tsx +4 -16
- package/src/vista/vista-file-context.ts +14 -0
- package/src/vista/vista-file-source.tsx +5 -28
- package/src/vista/vista-form-context.ts +20 -0
- package/src/vista/vista-form-host.tsx +11 -36
- package/src/vista/vista-open-context.tsx +4 -14
- package/src/vista/vista-open-state.ts +11 -0
- package/src/vista/vista-payloads.ts +44 -46
- package/src/vista/vista-tools.ts +2 -2
- package/dist/lib/biome-host/response-envelope.d.ts +0 -3
- package/dist/lib/biome-host/response-envelope.d.ts.map +0 -1
- package/dist/lib/biome-host/response-envelope.js +0 -21
- package/dist/lib/biome-host/response-envelope.js.map +0 -1
- package/dist/lib/biome-host/session-profiles.d.ts +0 -18
- package/dist/lib/biome-host/session-profiles.d.ts.map +0 -1
- package/dist/lib/biome-host/session-profiles.js +0 -35
- package/dist/lib/biome-host/session-profiles.js.map +0 -1
- package/src/lib/biome-host/response-envelope.ts +0 -69
- package/src/lib/biome-host/session-profiles.ts +0 -99
|
@@ -13,25 +13,45 @@
|
|
|
13
13
|
* • the values live in this component's `useState`; the single reader is
|
|
14
14
|
* `submitElicitationFields`, which takes them as a parameter and returns an
|
|
15
15
|
* outcome — the values are not reachable from any return or any throw;
|
|
16
|
-
* • the
|
|
17
|
-
*
|
|
18
|
-
* the chat transport;
|
|
16
|
+
* • the payload has NO URL authority. The exact endpoints are derived from
|
|
17
|
+
* the trusted elicitation API base URL supplied by `VistaFormHost`;
|
|
19
18
|
* • secret fields are DECLARED (`spec.isSecret`) and render write-only; the
|
|
20
19
|
* surface holds no cache and no read path repopulates them.
|
|
21
20
|
*
|
|
22
21
|
* Nothing here is provider-aware. Whether the flow is a field form or an OAuth
|
|
23
22
|
* handoff is decided by the payload the owning service produced.
|
|
24
23
|
*/
|
|
25
|
-
import {
|
|
26
|
-
|
|
24
|
+
import {
|
|
25
|
+
AlertTriangle,
|
|
26
|
+
CheckCircle2,
|
|
27
|
+
ExternalLink,
|
|
28
|
+
Loader2,
|
|
29
|
+
ShieldCheck,
|
|
30
|
+
} from 'lucide-react';
|
|
31
|
+
import {
|
|
32
|
+
useCallback,
|
|
33
|
+
useEffect,
|
|
34
|
+
useState,
|
|
35
|
+
type ReactElement,
|
|
36
|
+
type ReactNode,
|
|
37
|
+
} from 'react';
|
|
27
38
|
|
|
28
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
readElicitationStatus,
|
|
41
|
+
resolveTrustedElicitationEndpoints,
|
|
42
|
+
resolveTrustedElicitationLaunchUrl,
|
|
43
|
+
submitElicitationFields,
|
|
44
|
+
VistaFormSubmitError,
|
|
45
|
+
} from './elicitation-transport';
|
|
29
46
|
import { SchemaForm } from '../../ui/capture/schema-form';
|
|
30
47
|
import { captureValuesToStringMap } from '../../ui/capture/submit-values';
|
|
31
48
|
import { useSchemaValidation } from '../../ui/capture/use-schema-validation';
|
|
32
49
|
import { cn } from '../../ui/cn';
|
|
33
50
|
import { Button } from '../../ui/primitives/button';
|
|
34
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
useVistaFormHost,
|
|
53
|
+
type VistaFormOutcome,
|
|
54
|
+
} from '../vista-form-context';
|
|
35
55
|
|
|
36
56
|
import type { CaptureValues } from '../../ui/capture/field-spec';
|
|
37
57
|
import type { VistaFormPayload } from '../vista-payloads';
|
|
@@ -51,6 +71,29 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
|
|
|
51
71
|
const [showFieldErrors, setShowFieldErrors] = useState(false);
|
|
52
72
|
const [outcome, setOutcome] = useState<VistaFormOutcome | null>(null);
|
|
53
73
|
const { errors, isValid } = useSchemaValidation(payload.fields, values);
|
|
74
|
+
let transportConfigurationError: string | null = null;
|
|
75
|
+
let launchUrl: string | undefined;
|
|
76
|
+
if (host !== null) {
|
|
77
|
+
try {
|
|
78
|
+
// Validate the trust anchor before retrieving a bearer token or accepting
|
|
79
|
+
// a submit action. The transport repeats this check at the fetch boundary.
|
|
80
|
+
resolveTrustedElicitationEndpoints(
|
|
81
|
+
host.elicitationApiBaseUrl,
|
|
82
|
+
payload.ref,
|
|
83
|
+
);
|
|
84
|
+
if (payload.launch !== undefined) {
|
|
85
|
+
launchUrl = resolveTrustedElicitationLaunchUrl(
|
|
86
|
+
host.elicitationApiBaseUrl,
|
|
87
|
+
payload.launch.path,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
transportConfigurationError =
|
|
92
|
+
error instanceof Error
|
|
93
|
+
? error.message
|
|
94
|
+
: 'The trusted elicitation service is not configured safely.';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
54
97
|
|
|
55
98
|
const settle = useCallback(
|
|
56
99
|
(next: VistaFormOutcome): void => {
|
|
@@ -85,14 +128,14 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
|
|
|
85
128
|
.getAccessToken()
|
|
86
129
|
.then((accessToken) =>
|
|
87
130
|
submitElicitationFields({
|
|
88
|
-
|
|
131
|
+
apiBaseUrl: host.elicitationApiBaseUrl,
|
|
89
132
|
ref: payload.ref,
|
|
90
133
|
accessToken,
|
|
91
134
|
fields: captureValuesToStringMap(payload.fields, values),
|
|
92
135
|
}),
|
|
93
136
|
)
|
|
94
137
|
.then(settle, failed);
|
|
95
|
-
}, [host, isValid, payload.fields, payload.ref,
|
|
138
|
+
}, [host, isValid, payload.fields, payload.ref, settle, failed, values]);
|
|
96
139
|
|
|
97
140
|
const handleCheckStatus = useCallback((): void => {
|
|
98
141
|
if (host === null) return;
|
|
@@ -101,17 +144,23 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
|
|
|
101
144
|
void host
|
|
102
145
|
.getAccessToken()
|
|
103
146
|
.then((accessToken) =>
|
|
104
|
-
readElicitationStatus({
|
|
147
|
+
readElicitationStatus({
|
|
148
|
+
apiBaseUrl: host.elicitationApiBaseUrl,
|
|
149
|
+
ref: payload.ref,
|
|
150
|
+
accessToken,
|
|
151
|
+
}),
|
|
105
152
|
)
|
|
106
153
|
.then((next) => {
|
|
107
154
|
if (next === null) {
|
|
108
|
-
setServiceError(
|
|
155
|
+
setServiceError(
|
|
156
|
+
'Not completed yet — finish the steps in the other window, then check again.',
|
|
157
|
+
);
|
|
109
158
|
setPhase(FormPhase.Editing);
|
|
110
159
|
return;
|
|
111
160
|
}
|
|
112
161
|
settle(next);
|
|
113
162
|
}, failed);
|
|
114
|
-
}, [host, payload.ref,
|
|
163
|
+
}, [host, payload.ref, settle, failed]);
|
|
115
164
|
|
|
116
165
|
// The consent/install window is a separate browsing context, so the only
|
|
117
166
|
// signal this tab gets is the user returning to it. Re-checking on focus is
|
|
@@ -137,6 +186,19 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
|
|
|
137
186
|
);
|
|
138
187
|
}
|
|
139
188
|
|
|
189
|
+
if (transportConfigurationError !== null) {
|
|
190
|
+
return (
|
|
191
|
+
<Shell heading={payload.heading} purpose={payload.purpose}>
|
|
192
|
+
<Notice
|
|
193
|
+
tone="error"
|
|
194
|
+
title="The secure service is not configured safely"
|
|
195
|
+
>
|
|
196
|
+
{transportConfigurationError}
|
|
197
|
+
</Notice>
|
|
198
|
+
</Shell>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
140
202
|
if (phase === FormPhase.Settled && outcome !== null) {
|
|
141
203
|
return (
|
|
142
204
|
<Shell heading={payload.heading} purpose={payload.purpose}>
|
|
@@ -146,7 +208,10 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
|
|
|
146
208
|
<p className="text-body-1 font-medium text-ink">Submitted.</p>
|
|
147
209
|
<p className="text-caption text-ink-3">
|
|
148
210
|
The value went straight to the service and was never shown to the
|
|
149
|
-
agent
|
|
211
|
+
agent
|
|
212
|
+
{outcome.connectionId === null
|
|
213
|
+
? '.'
|
|
214
|
+
: `. Connection ${outcome.connectionId} is ready.`}
|
|
150
215
|
</p>
|
|
151
216
|
</div>
|
|
152
217
|
</div>
|
|
@@ -166,7 +231,9 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
|
|
|
166
231
|
<div className="flex flex-wrap items-center gap-2">
|
|
167
232
|
<Button
|
|
168
233
|
onClick={() => {
|
|
169
|
-
|
|
234
|
+
// `launchUrl` was derived from the host trust anchor above; the
|
|
235
|
+
// payload contributes only a validated same-origin path.
|
|
236
|
+
globalThis.open(launchUrl, '_blank', 'noopener,noreferrer');
|
|
170
237
|
}}
|
|
171
238
|
className="gap-1.5"
|
|
172
239
|
disabled={working}
|
|
@@ -174,7 +241,11 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
|
|
|
174
241
|
<ExternalLink className="h-4 w-4" />
|
|
175
242
|
{payload.launch.label}
|
|
176
243
|
</Button>
|
|
177
|
-
<Button
|
|
244
|
+
<Button
|
|
245
|
+
variant="outline"
|
|
246
|
+
onClick={handleCheckStatus}
|
|
247
|
+
disabled={working}
|
|
248
|
+
>
|
|
178
249
|
{working && <Loader2 className="mr-1 h-4 w-4 animate-spin" />}
|
|
179
250
|
I've finished — check
|
|
180
251
|
</Button>
|
|
@@ -184,8 +255,8 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
|
|
|
184
255
|
<>
|
|
185
256
|
{payload.fields.length === 0 && (
|
|
186
257
|
<Notice tone="warn" title="No form fields were declared">
|
|
187
|
-
This connector declares no onboarding form. Submitting will ask
|
|
188
|
-
service, which is the authority on whether that is allowed.
|
|
258
|
+
This connector declares no onboarding form. Submitting will ask
|
|
259
|
+
the service, which is the authority on whether that is allowed.
|
|
189
260
|
</Notice>
|
|
190
261
|
)}
|
|
191
262
|
<SchemaForm
|
|
@@ -217,16 +288,25 @@ function Shell({
|
|
|
217
288
|
heading,
|
|
218
289
|
purpose,
|
|
219
290
|
children,
|
|
220
|
-
}: Readonly<{
|
|
291
|
+
}: Readonly<{
|
|
292
|
+
heading?: string;
|
|
293
|
+
purpose: string;
|
|
294
|
+
children: ReactNode;
|
|
295
|
+
}>): ReactElement {
|
|
221
296
|
return (
|
|
222
297
|
<div className="h-full overflow-auto">
|
|
223
298
|
<div className="mx-auto max-w-2xl space-y-4 p-6">
|
|
224
299
|
<div className="space-y-1">
|
|
225
|
-
<h2 className="text-subtitle font-semibold text-ink">
|
|
226
|
-
|
|
300
|
+
<h2 className="text-subtitle font-semibold text-ink">
|
|
301
|
+
{heading ?? 'Secure request'}
|
|
302
|
+
</h2>
|
|
303
|
+
{purpose.length > 0 && (
|
|
304
|
+
<p className="text-body-1 text-ink-2">{purpose}</p>
|
|
305
|
+
)}
|
|
227
306
|
<p className="flex items-center gap-1.5 text-caption text-ink-3">
|
|
228
307
|
<ShieldCheck className="h-3.5 w-3.5" />
|
|
229
|
-
Sent straight to the service with your own sign-in. The agent never
|
|
308
|
+
Sent straight to the service with your own sign-in. The agent never
|
|
309
|
+
sees what you type.
|
|
230
310
|
</p>
|
|
231
311
|
</div>
|
|
232
312
|
{children}
|
|
@@ -239,23 +319,42 @@ function Notice({
|
|
|
239
319
|
tone,
|
|
240
320
|
title,
|
|
241
321
|
children,
|
|
242
|
-
}: Readonly<{
|
|
322
|
+
}: Readonly<{
|
|
323
|
+
tone: 'error' | 'warn';
|
|
324
|
+
title: string;
|
|
325
|
+
children: ReactNode;
|
|
326
|
+
}>): ReactElement {
|
|
243
327
|
return (
|
|
244
328
|
<div
|
|
245
329
|
role="alert"
|
|
246
330
|
className={cn(
|
|
247
331
|
'flex items-start gap-2 rounded-md border p-3',
|
|
248
|
-
tone === 'error'
|
|
332
|
+
tone === 'error'
|
|
333
|
+
? 'border-destructive/40 bg-destructive/10'
|
|
334
|
+
: 'border-rule bg-paper-elev',
|
|
249
335
|
)}
|
|
250
336
|
>
|
|
251
337
|
<AlertTriangle
|
|
252
|
-
className={cn(
|
|
338
|
+
className={cn(
|
|
339
|
+
'mt-0.5 h-4 w-4 shrink-0',
|
|
340
|
+
tone === 'error' ? 'text-destructive' : 'text-ink-3',
|
|
341
|
+
)}
|
|
253
342
|
/>
|
|
254
343
|
<div className="space-y-0.5">
|
|
255
|
-
<p
|
|
344
|
+
<p
|
|
345
|
+
className={cn(
|
|
346
|
+
'text-body-1 font-medium',
|
|
347
|
+
tone === 'error' ? 'text-destructive' : 'text-ink',
|
|
348
|
+
)}
|
|
349
|
+
>
|
|
256
350
|
{title}
|
|
257
351
|
</p>
|
|
258
|
-
<p
|
|
352
|
+
<p
|
|
353
|
+
className={cn(
|
|
354
|
+
'text-body-1',
|
|
355
|
+
tone === 'error' ? 'text-destructive/90' : 'text-ink-3',
|
|
356
|
+
)}
|
|
357
|
+
>
|
|
259
358
|
{children}
|
|
260
359
|
</p>
|
|
261
360
|
</div>
|
|
@@ -12,11 +12,15 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
12
12
|
import {
|
|
13
13
|
extractServiceMessage,
|
|
14
14
|
readElicitationStatus,
|
|
15
|
+
resolveTrustedElicitationEndpoints,
|
|
16
|
+
resolveTrustedElicitationLaunchUrl,
|
|
15
17
|
submitElicitationFields,
|
|
16
18
|
VistaFormSubmitError,
|
|
19
|
+
VistaFormTransportConfigError,
|
|
17
20
|
} from './elicitation-transport';
|
|
18
21
|
|
|
19
22
|
const SERVICE_ORIGIN = 'https://connector-gateway.example.test';
|
|
23
|
+
const API_BASE_URL = `${SERVICE_ORIGIN}/credential-elicitations`;
|
|
20
24
|
const SUBMIT_URL = `${SERVICE_ORIGIN}/credential-elicitations/ref-1/submit`;
|
|
21
25
|
const STATUS_URL = `${SERVICE_ORIGIN}/credential-elicitations/ref-1`;
|
|
22
26
|
const SECRET = 'sk-live-DO-NOT-LEAK';
|
|
@@ -39,14 +43,128 @@ afterEach(() => {
|
|
|
39
43
|
vi.restoreAllMocks();
|
|
40
44
|
});
|
|
41
45
|
|
|
46
|
+
describe('trusted elicitation endpoint resolution', () => {
|
|
47
|
+
it('derives exact status and submit paths from the host trust anchor', () => {
|
|
48
|
+
expect(resolveTrustedElicitationEndpoints(API_BASE_URL, 'ref-1')).toEqual({
|
|
49
|
+
statusUrl: STATUS_URL,
|
|
50
|
+
submitUrl: SUBMIT_URL,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it.each([
|
|
55
|
+
['relative', '/credential-elicitations'],
|
|
56
|
+
['network-path', '//evil.example/credential-elicitations'],
|
|
57
|
+
['scheme confusion', 'javascript:https://trusted.example'],
|
|
58
|
+
[
|
|
59
|
+
'authority confusion',
|
|
60
|
+
'https://trusted.example@evil.example/credential-elicitations',
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
'query',
|
|
64
|
+
'https://trusted.example/credential-elicitations?next=https://evil.example',
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
'fragment',
|
|
68
|
+
'https://trusted.example/credential-elicitations#https://evil.example',
|
|
69
|
+
],
|
|
70
|
+
[
|
|
71
|
+
'path traversal',
|
|
72
|
+
'https://trusted.example/credential-elicitations/../evil',
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
'encoded path traversal',
|
|
76
|
+
'https://trusted.example/credential-elicitations/%2e%2e/evil',
|
|
77
|
+
],
|
|
78
|
+
])('rejects %s base URL input', (_label, apiBaseUrl) => {
|
|
79
|
+
expect(() =>
|
|
80
|
+
resolveTrustedElicitationEndpoints(apiBaseUrl, 'ref-1'),
|
|
81
|
+
).toThrow(VistaFormTransportConfigError);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('requires HTTPS for a non-local service', () => {
|
|
85
|
+
expect(() =>
|
|
86
|
+
resolveTrustedElicitationEndpoints(
|
|
87
|
+
'http://api.example.test/credential-elicitations',
|
|
88
|
+
'ref-1',
|
|
89
|
+
'http://host.example.test:8080',
|
|
90
|
+
),
|
|
91
|
+
).toThrow(/Insecure elicitation HTTP/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('permits explicit loopback and same-host HTTP development', () => {
|
|
95
|
+
expect(
|
|
96
|
+
resolveTrustedElicitationEndpoints(
|
|
97
|
+
'http://localhost:3010/credential-elicitations',
|
|
98
|
+
'ref-1',
|
|
99
|
+
'http://localhost:8080',
|
|
100
|
+
).statusUrl,
|
|
101
|
+
).toBe('http://localhost:3010/credential-elicitations/ref-1');
|
|
102
|
+
expect(
|
|
103
|
+
resolveTrustedElicitationEndpoints(
|
|
104
|
+
'http://192.168.1.20:3010/credential-elicitations',
|
|
105
|
+
'ref-1',
|
|
106
|
+
'http://192.168.1.20:8080',
|
|
107
|
+
).statusUrl,
|
|
108
|
+
).toBe('http://192.168.1.20:3010/credential-elicitations/ref-1');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it.each(['.', '..', '../steal', 'a/b', '%2e%2e', 'réf'])(
|
|
112
|
+
'rejects unsafe ref %s before URL construction',
|
|
113
|
+
(ref) => {
|
|
114
|
+
expect(() =>
|
|
115
|
+
resolveTrustedElicitationEndpoints(API_BASE_URL, ref),
|
|
116
|
+
).toThrow(VistaFormTransportConfigError);
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
it('keeps launch paths on the trusted origin and rejects authority/traversal', () => {
|
|
121
|
+
expect(
|
|
122
|
+
resolveTrustedElicitationLaunchUrl(
|
|
123
|
+
API_BASE_URL,
|
|
124
|
+
'/oauth/acme/authorize?flow=1',
|
|
125
|
+
),
|
|
126
|
+
).toBe(`${SERVICE_ORIGIN}/oauth/acme/authorize?flow=1`);
|
|
127
|
+
|
|
128
|
+
for (const path of [
|
|
129
|
+
'https://evil.example/steal',
|
|
130
|
+
'//evil.example/steal',
|
|
131
|
+
'/oauth/../steal',
|
|
132
|
+
'/oauth/%2e%2e/steal',
|
|
133
|
+
'/oauth\\..\\steal',
|
|
134
|
+
'/oauth/start#https://evil.example',
|
|
135
|
+
]) {
|
|
136
|
+
expect(() =>
|
|
137
|
+
resolveTrustedElicitationLaunchUrl(API_BASE_URL, path),
|
|
138
|
+
).toThrow(VistaFormTransportConfigError);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('does not call fetch when the trusted base or ref is invalid', async () => {
|
|
143
|
+
const fetchMock = vi.fn();
|
|
144
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
145
|
+
await expect(
|
|
146
|
+
submitElicitationFields({
|
|
147
|
+
apiBaseUrl:
|
|
148
|
+
'https://trusted.example@evil.example/credential-elicitations',
|
|
149
|
+
ref: '../steal',
|
|
150
|
+
accessToken: 'user-jwt',
|
|
151
|
+
fields: { apiKey: SECRET },
|
|
152
|
+
}),
|
|
153
|
+
).rejects.toBeInstanceOf(VistaFormTransportConfigError);
|
|
154
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
42
158
|
describe('submitElicitationFields', () => {
|
|
43
159
|
it('POSTs to the SERVICE origin directly — not a host-relative path', async () => {
|
|
44
160
|
const fetchMock = stubFetch(
|
|
45
|
-
jsonResponse(201, {
|
|
161
|
+
jsonResponse(201, {
|
|
162
|
+
data: { submitted: true, ref: 'ref-1', connectionId: 'conn-9' },
|
|
163
|
+
}),
|
|
46
164
|
);
|
|
47
165
|
|
|
48
166
|
await submitElicitationFields({
|
|
49
|
-
|
|
167
|
+
apiBaseUrl: API_BASE_URL,
|
|
50
168
|
ref: 'ref-1',
|
|
51
169
|
accessToken: 'user-jwt',
|
|
52
170
|
fields: { apiKey: SECRET },
|
|
@@ -57,8 +175,11 @@ describe('submitElicitationFields', () => {
|
|
|
57
175
|
expect(url).toBe(SUBMIT_URL);
|
|
58
176
|
expect(new URL(url).origin).toBe(SERVICE_ORIGIN);
|
|
59
177
|
expect(init.method).toBe('POST');
|
|
178
|
+
expect(init.redirect).toBe('error');
|
|
60
179
|
// The SUBJECT's own bearer — not a service token, not the agent's.
|
|
61
|
-
expect((init.headers as Record<string, string>).Authorization).toBe(
|
|
180
|
+
expect((init.headers as Record<string, string>).Authorization).toBe(
|
|
181
|
+
'Bearer user-jwt',
|
|
182
|
+
);
|
|
62
183
|
});
|
|
63
184
|
|
|
64
185
|
it('never hands a secret back to the caller, even when the service echoes it', async () => {
|
|
@@ -76,26 +197,36 @@ describe('submitElicitationFields', () => {
|
|
|
76
197
|
);
|
|
77
198
|
|
|
78
199
|
const outcome = await submitElicitationFields({
|
|
79
|
-
|
|
200
|
+
apiBaseUrl: API_BASE_URL,
|
|
80
201
|
ref: 'ref-1',
|
|
81
202
|
accessToken: 'user-jwt',
|
|
82
203
|
fields: { apiKey: SECRET },
|
|
83
204
|
});
|
|
84
205
|
|
|
85
206
|
// The outcome is a FRESH object with exactly three keys.
|
|
86
|
-
expect(Object.keys(outcome).sort()).toEqual([
|
|
87
|
-
|
|
207
|
+
expect(Object.keys(outcome).sort()).toEqual([
|
|
208
|
+
'connectionId',
|
|
209
|
+
'ref',
|
|
210
|
+
'submitted',
|
|
211
|
+
]);
|
|
212
|
+
expect(outcome).toEqual({
|
|
213
|
+
submitted: true,
|
|
214
|
+
ref: 'ref-1',
|
|
215
|
+
connectionId: 'conn-9',
|
|
216
|
+
});
|
|
88
217
|
expect(JSON.stringify(outcome)).not.toContain(SECRET);
|
|
89
218
|
expect(JSON.stringify(outcome)).not.toContain('apiKey');
|
|
90
219
|
});
|
|
91
220
|
|
|
92
221
|
it('round-trips a NON-secret field to the service body', async () => {
|
|
93
222
|
const fetchMock = stubFetch(
|
|
94
|
-
jsonResponse(201, {
|
|
223
|
+
jsonResponse(201, {
|
|
224
|
+
data: { submitted: true, ref: 'ref-1', connectionId: 'conn-9' },
|
|
225
|
+
}),
|
|
95
226
|
);
|
|
96
227
|
|
|
97
228
|
await submitElicitationFields({
|
|
98
|
-
|
|
229
|
+
apiBaseUrl: API_BASE_URL,
|
|
99
230
|
ref: 'ref-1',
|
|
100
231
|
accessToken: 'user-jwt',
|
|
101
232
|
fields: { baseUrl: 'https://acme.example.com', apiKey: SECRET },
|
|
@@ -108,10 +239,25 @@ describe('submitElicitationFields', () => {
|
|
|
108
239
|
});
|
|
109
240
|
|
|
110
241
|
it('accepts a flat (un-enveloped) response body', async () => {
|
|
111
|
-
stubFetch(
|
|
242
|
+
stubFetch(
|
|
243
|
+
jsonResponse(200, {
|
|
244
|
+
submitted: true,
|
|
245
|
+
ref: 'ref-1',
|
|
246
|
+
connectionId: 'conn-2',
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
112
249
|
await expect(
|
|
113
|
-
submitElicitationFields({
|
|
114
|
-
|
|
250
|
+
submitElicitationFields({
|
|
251
|
+
apiBaseUrl: API_BASE_URL,
|
|
252
|
+
ref: 'ref-1',
|
|
253
|
+
accessToken: 't',
|
|
254
|
+
fields: {},
|
|
255
|
+
}),
|
|
256
|
+
).resolves.toEqual({
|
|
257
|
+
submitted: true,
|
|
258
|
+
ref: 'ref-1',
|
|
259
|
+
connectionId: 'conn-2',
|
|
260
|
+
});
|
|
115
261
|
});
|
|
116
262
|
|
|
117
263
|
it("surfaces the backend's empty-schema 400 as a usable message", async () => {
|
|
@@ -126,7 +272,7 @@ describe('submitElicitationFields', () => {
|
|
|
126
272
|
|
|
127
273
|
// An empty field schema submits an empty map; the SERVICE is the authority.
|
|
128
274
|
const promise = submitElicitationFields({
|
|
129
|
-
|
|
275
|
+
apiBaseUrl: API_BASE_URL,
|
|
130
276
|
ref: 'ref-1',
|
|
131
277
|
accessToken: 'user-jwt',
|
|
132
278
|
fields: {},
|
|
@@ -139,35 +285,67 @@ describe('submitElicitationFields', () => {
|
|
|
139
285
|
it('refuses a 2xx that does not report the elicitation as submitted', async () => {
|
|
140
286
|
stubFetch(jsonResponse(200, { data: { ref: 'ref-1' } }));
|
|
141
287
|
await expect(
|
|
142
|
-
submitElicitationFields({
|
|
288
|
+
submitElicitationFields({
|
|
289
|
+
apiBaseUrl: API_BASE_URL,
|
|
290
|
+
ref: 'ref-1',
|
|
291
|
+
accessToken: 't',
|
|
292
|
+
fields: {},
|
|
293
|
+
}),
|
|
143
294
|
).rejects.toThrow(/did not report the elicitation as submitted/);
|
|
144
295
|
});
|
|
145
296
|
});
|
|
146
297
|
|
|
147
298
|
describe('readElicitationStatus', () => {
|
|
148
299
|
it('returns null while pending — an explicit "not yet", never a guess', async () => {
|
|
149
|
-
stubFetch(
|
|
300
|
+
stubFetch(
|
|
301
|
+
jsonResponse(200, {
|
|
302
|
+
data: { ref: 'ref-1', status: 'pending', connectionId: null },
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
150
305
|
await expect(
|
|
151
|
-
readElicitationStatus({
|
|
306
|
+
readElicitationStatus({
|
|
307
|
+
apiBaseUrl: API_BASE_URL,
|
|
308
|
+
ref: 'ref-1',
|
|
309
|
+
accessToken: 't',
|
|
310
|
+
}),
|
|
152
311
|
).resolves.toBeNull();
|
|
153
312
|
});
|
|
154
313
|
|
|
155
314
|
it('settles on the service-reported submitted status', async () => {
|
|
156
315
|
stubFetch(
|
|
157
|
-
jsonResponse(200, {
|
|
316
|
+
jsonResponse(200, {
|
|
317
|
+
data: { ref: 'ref-1', status: 'submitted', connectionId: 'conn-3' },
|
|
318
|
+
}),
|
|
158
319
|
);
|
|
159
320
|
await expect(
|
|
160
|
-
readElicitationStatus({
|
|
161
|
-
|
|
321
|
+
readElicitationStatus({
|
|
322
|
+
apiBaseUrl: API_BASE_URL,
|
|
323
|
+
ref: 'ref-1',
|
|
324
|
+
accessToken: 't',
|
|
325
|
+
}),
|
|
326
|
+
).resolves.toEqual({
|
|
327
|
+
submitted: true,
|
|
328
|
+
ref: 'ref-1',
|
|
329
|
+
connectionId: 'conn-3',
|
|
330
|
+
});
|
|
162
331
|
});
|
|
163
332
|
|
|
164
333
|
it('GETs the service origin with the subject bearer', async () => {
|
|
165
|
-
const fetchMock = stubFetch(
|
|
166
|
-
|
|
334
|
+
const fetchMock = stubFetch(
|
|
335
|
+
jsonResponse(200, { data: { ref: 'ref-1', status: 'pending' } }),
|
|
336
|
+
);
|
|
337
|
+
await readElicitationStatus({
|
|
338
|
+
apiBaseUrl: API_BASE_URL,
|
|
339
|
+
ref: 'ref-1',
|
|
340
|
+
accessToken: 'user-jwt',
|
|
341
|
+
});
|
|
167
342
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
168
343
|
expect(new URL(url).origin).toBe(SERVICE_ORIGIN);
|
|
169
344
|
expect(init.method).toBe('GET');
|
|
170
|
-
expect(
|
|
345
|
+
expect(init.redirect).toBe('error');
|
|
346
|
+
expect((init.headers as Record<string, string>).Authorization).toBe(
|
|
347
|
+
'Bearer user-jwt',
|
|
348
|
+
);
|
|
171
349
|
});
|
|
172
350
|
});
|
|
173
351
|
|
|
@@ -175,11 +353,15 @@ describe('extractServiceMessage', () => {
|
|
|
175
353
|
it('reads a string message, a message array, and a nested error message', () => {
|
|
176
354
|
expect(extractServiceMessage({ message: 'nope' }, 400)).toBe('nope');
|
|
177
355
|
expect(extractServiceMessage({ message: ['a', 'b'] }, 400)).toBe('a b');
|
|
178
|
-
expect(extractServiceMessage({ error: { message: 'deep' } }, 400)).toBe(
|
|
356
|
+
expect(extractServiceMessage({ error: { message: 'deep' } }, 400)).toBe(
|
|
357
|
+
'deep',
|
|
358
|
+
);
|
|
179
359
|
});
|
|
180
360
|
|
|
181
361
|
it('never yields an empty string', () => {
|
|
182
|
-
expect(extractServiceMessage(null, 503)).toBe(
|
|
362
|
+
expect(extractServiceMessage(null, 503)).toBe(
|
|
363
|
+
'The service rejected the submission (HTTP 503).',
|
|
364
|
+
);
|
|
183
365
|
expect(extractServiceMessage({ message: '' }, 500)).toBe(
|
|
184
366
|
'The service rejected the submission (HTTP 500).',
|
|
185
367
|
);
|