@runtypelabs/persona 4.24.0 → 4.26.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/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-Db61aI9J.d.cts → types-BwKTZhg9.d.cts} +14 -0
- package/dist/animations/{types-Db61aI9J.d.ts → types-BwKTZhg9.d.ts} +14 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +14 -0
- package/dist/animations-extra.d.ts +14 -0
- package/dist/approval-ui-QAKWNFNM.js +1 -0
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +243 -8
- package/dist/approval-ui.d.ts +243 -8
- package/dist/approval-ui.js +2 -2
- package/dist/{artifacts-ui-IGB5YKLO.js → artifacts-ui-NJGMM27I.js} +1 -1
- package/dist/artifacts-ui.cjs +4 -4
- package/dist/artifacts-ui.d.cts +243 -8
- package/dist/artifacts-ui.d.ts +243 -8
- package/dist/artifacts-ui.js +5 -5
- package/dist/chunk-2ANTRKTV.js +1 -0
- package/dist/chunk-4HPTPUC6.js +1 -0
- package/dist/chunk-6V3MNEKB.js +1 -0
- package/dist/chunk-HGUCGM3K.js +9 -0
- package/dist/chunk-IO5VVUKP.js +3 -0
- package/dist/chunk-J4J3Y2P2.js +1 -0
- package/dist/chunk-K6HKIP5K.js +1 -0
- package/dist/chunk-K7NA7G7J.js +1 -0
- package/dist/chunk-MXUTS3LB.js +1 -0
- package/dist/chunk-PDPVORUF.js +1 -0
- package/dist/chunk-QPPJUSZH.js +1 -0
- package/dist/chunk-QRKGQPU7.js +1 -0
- package/dist/chunk-S6PBWE75.js +1 -0
- package/dist/chunk-T7QM7YMF.js +1 -0
- package/dist/chunk-TR2H475E.js +1 -0
- package/dist/chunk-UPO4GUFC.js +1 -0
- package/dist/chunk-ZC66NAIR.js +1 -0
- package/dist/client-history-YPEG45Q4.js +1 -0
- package/dist/client-history.js +1 -0
- package/dist/client-stream-42FOI3EC.js +4 -0
- package/dist/client-stream.js +5 -0
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions-E7HJYWO6.js +161 -0
- package/dist/{context-mentions-inline-I2RDEBAK.js → context-mentions-inline-QVESLW55.js} +1 -1
- package/dist/context-mentions.cjs +7 -7
- package/dist/context-mentions.d.cts +243 -8
- package/dist/context-mentions.d.ts +243 -8
- package/dist/context-mentions.js +8 -8
- package/dist/event-stream-view-FOIJHFQO.js +1 -0
- package/dist/event-stream-view.cjs +1 -1
- package/dist/event-stream-view.d.cts +246 -31
- package/dist/event-stream-view.d.ts +246 -31
- package/dist/event-stream-view.js +1 -1
- package/dist/forms-ui.d.cts +295 -667
- package/dist/forms-ui.d.ts +295 -667
- package/dist/history-shell-QDQ44PSE.js +3 -0
- package/dist/history-shell.js +3 -0
- package/dist/{history-view-BD6OXLNA.js → history-view-NCFBI5GR.js} +2 -2
- package/dist/history-view.cjs +2 -2
- package/dist/history-view.d.cts +35 -1
- package/dist/history-view.d.ts +35 -1
- package/dist/history-view.js +2 -2
- package/dist/index.cjs +80 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +301 -738
- package/dist/index.d.ts +301 -738
- package/dist/index.global.js +3 -215
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +81 -75
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +3 -67
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +22 -22
- package/dist/runtype-tts-entry-NFBVWR7N.js +1 -0
- package/dist/runtype-tts.js +1 -1
- package/dist/session-actions-ISSKSI66.js +1 -0
- package/dist/session-actions.js +1 -0
- package/dist/session-reconnect.d.cts +243 -8
- package/dist/session-reconnect.d.ts +243 -8
- package/dist/smart-dom-reader.cjs +13 -13
- package/dist/smart-dom-reader.d.cts +243 -8
- package/dist/smart-dom-reader.d.ts +243 -8
- package/dist/smart-dom-reader.js +11 -11
- package/dist/theme-editor-preview.cjs +116 -98
- package/dist/theme-editor-preview.d.cts +245 -8
- package/dist/theme-editor-preview.d.ts +245 -8
- package/dist/theme-editor-preview.js +49 -54
- package/dist/theme-editor.cjs +2 -2
- package/dist/theme-editor.d.cts +244 -23
- package/dist/theme-editor.d.ts +244 -23
- package/dist/theme-editor.js +2 -2
- package/dist/theme-reference.d.cts +14 -0
- package/dist/theme-reference.d.ts +14 -0
- package/dist/ui-extras-entry-WAYFKVNW.js +3 -0
- package/dist/ui-extras.js +72 -0
- package/dist/voice-runtime-UOQOFAFY.js +17 -0
- package/dist/voice-runtime.cjs +17 -1
- package/dist/voice-runtime.d.cts +11643 -192
- package/dist/voice-runtime.d.ts +11643 -192
- package/dist/voice-runtime.js +17 -1
- package/dist/voice-worklet-player.cjs +19 -4
- package/dist/voice-worklet-player.d.cts +9 -10
- package/dist/voice-worklet-player.d.ts +9 -10
- package/dist/voice-worklet-player.js +19 -4
- package/dist/webmcp-polyfill.js +1 -1
- package/dist/webmcp-runtime-V6ZWW3PI.js +6 -0
- package/dist/webmcp-runtime.cjs +2 -2
- package/dist/webmcp-runtime.d.cts +1 -33
- package/dist/webmcp-runtime.d.ts +1 -33
- package/dist/webmcp-runtime.js +2 -2
- package/dist/widget.css +1 -1
- package/package.json +11 -5
- package/src/approval-paused-tool-settle.test.ts +221 -0
- package/src/client-approve.test.ts +311 -0
- package/src/client-history-eager.ts +9 -0
- package/src/client-history-loader.ts +23 -0
- package/src/client-history.ts +579 -0
- package/src/client-resume.ts +177 -0
- package/src/client-stream-eager.ts +12 -0
- package/src/client-stream-loader.ts +25 -0
- package/src/client-stream.ts +1880 -0
- package/src/client.test.ts +31 -1
- package/src/client.ts +293 -2607
- package/src/components/approval-actions.test.ts +27 -0
- package/src/components/approval-actions.ts +27 -3
- package/src/components/message-bubble.test.ts +19 -0
- package/src/components/message-bubble.ts +2 -0
- package/src/components/registry.ts +6 -6
- package/src/components/tool-bubble.test.ts +9 -0
- package/src/components/tool-bubble.ts +1 -0
- package/src/history-shell-bundle.test.ts +54 -0
- package/src/history-shell-eager.ts +12 -0
- package/src/history-shell-loader.ts +27 -0
- package/src/history-shell.ts +2362 -0
- package/src/history-view-loader.ts +2 -0
- package/src/history-view.ts +3 -0
- package/src/index-core.ts +8 -0
- package/src/index-global.ts +90 -248
- package/src/index.ts +13 -0
- package/src/install.ts +6 -4
- package/src/internal/runtype-history-provider.test.ts +1 -0
- package/src/internal/runtype-history-provider.ts +28 -13
- package/src/plugins/registry.ts +8 -8
- package/src/session-actions-chunk.test.ts +55 -0
- package/src/session-actions-eager.ts +11 -0
- package/src/session-actions-loader.ts +37 -0
- package/src/session-actions.ts +909 -0
- package/src/session-history.test.ts +3 -1
- package/src/session.ts +875 -1530
- package/src/session.voice.test.ts +1241 -5
- package/src/styles/widget.css +58 -0
- package/src/theme-editor/state.ts +91 -91
- package/src/types.ts +269 -5
- package/src/ui-ask-user-sheet.ts +382 -0
- package/src/ui-extras-eager.ts +10 -0
- package/src/ui-extras-entry.ts +9 -0
- package/src/ui-extras-loader.ts +26 -0
- package/src/ui.artifact-rail-resize.test.ts +2 -1
- package/src/ui.ask-user-chunk.test.ts +93 -0
- package/src/ui.history-chunk.test.ts +2 -1
- package/src/ui.history-config-provider.test.ts +2 -1
- package/src/ui.history-confirm-fallback.test.ts +142 -0
- package/src/ui.history-controller.test.ts +2 -1
- package/src/ui.history-lifecycle.test.ts +2 -1
- package/src/ui.history-render-hooks.test.ts +2 -1
- package/src/ui.history-shell.test.ts +2 -1
- package/src/ui.rail-overlay.test.ts +2 -1
- package/src/ui.rail-resize.test.ts +2 -1
- package/src/ui.rail-trigger-pending.test.ts +2 -1
- package/src/ui.ts +399 -2770
- package/src/ui.voice-prewarm.test.ts +222 -0
- package/src/utils/attachment-manager.ts +67 -67
- package/src/utils/constants.ts +3 -0
- package/src/utils/context-mention-controller.ts +323 -323
- package/src/utils/context-mention-manager.ts +40 -40
- package/src/utils/context-mention-orchestrator.ts +28 -0
- package/src/utils/event-stream-buffer.ts +50 -50
- package/src/utils/event-stream-store.ts +43 -43
- package/src/utils/throughput-tracker.ts +34 -34
- package/src/utils/virtual-scroller.ts +67 -67
- package/src/utils/visitor-store.test.ts +50 -0
- package/src/utils/visitor-store.ts +23 -5
- package/src/voice/__fixtures__/voice-wire/README.md +1 -0
- package/src/voice/__fixtures__/voice-wire/client/cancel.json +3 -0
- package/src/voice/__fixtures__/voice-wire/client/context.json +4 -0
- package/src/voice/__fixtures__/voice-wire/client/delegation_result.json +6 -0
- package/src/voice/__fixtures__/voice-wire/client/delegation_update.json +6 -0
- package/src/voice/__fixtures__/voice-wire/client/ping.json +3 -0
- package/src/voice/__fixtures__/voice-wire/client/playback_progress.json +5 -0
- package/src/voice/__fixtures__/voice-wire/client/start.json +3 -0
- package/src/voice/__fixtures__/voice-wire/server/artifact.json +11 -0
- package/src/voice/__fixtures__/voice-wire/server/attached.json +4 -0
- package/src/voice/__fixtures__/voice-wire/server/audio_clear.json +4 -0
- package/src/voice/__fixtures__/voice-wire/server/audio_end.json +3 -0
- package/src/voice/__fixtures__/voice-wire/server/delegation_cancelled.json +5 -0
- package/src/voice/__fixtures__/voice-wire/server/delegation_completed.json +8 -0
- package/src/voice/__fixtures__/voice-wire/server/delegation_started.json +15 -0
- package/src/voice/__fixtures__/voice-wire/server/error.json +6 -0
- package/src/voice/__fixtures__/voice-wire/server/metrics.json +10 -0
- package/src/voice/__fixtures__/voice-wire/server/pong.json +3 -0
- package/src/voice/__fixtures__/voice-wire/server/session_config.json +16 -0
- package/src/voice/__fixtures__/voice-wire/server/transcript_final.json +7 -0
- package/src/voice/__fixtures__/voice-wire/server/transcript_interim.json +4 -0
- package/src/voice/__fixtures__/voice-wire/server/transcript_partial.json +6 -0
- package/src/voice/__fixtures__/voice-wire/server/transcript_update.json +10 -0
- package/src/voice/__fixtures__/voice-wire/server/warning.json +5 -0
- package/src/voice/audio-playback-manager.test.ts +57 -0
- package/src/voice/audio-playback-manager.ts +120 -98
- package/src/voice/browser-voice-provider.ts +37 -37
- package/src/voice/fallback-speech-engine.ts +22 -16
- package/src/voice/keyed-voice-transcript.ts +314 -0
- package/src/voice/mic-state-styles.ts +182 -0
- package/src/voice/read-aloud-controller.ts +46 -44
- package/src/voice/runtype-speech-engine.ts +35 -33
- package/src/voice/runtype-voice-provider.ts +856 -164
- package/src/voice/session-voice-wiring.ts +266 -0
- package/src/voice/voice-delegation.test.ts +71 -0
- package/src/voice/voice-delegation.ts +364 -0
- package/src/voice/voice-turn-tracker.ts +6 -6
- package/src/voice/voice.test.ts +1250 -7
- package/src/voice/worklet-playback-engine.test.ts +81 -0
- package/src/voice/worklet-playback-engine.ts +21 -3
- package/src/voice-runtime-loader.ts +4 -1
- package/src/voice-runtime.ts +7 -0
- package/src/webmcp-runtime-entry.ts +41 -39
- package/dist/approval-ui-3NMYNXBD.js +0 -1
- package/dist/chunk-5EIIHQLQ.js +0 -1
- package/dist/chunk-7CO4T5KB.js +0 -1
- package/dist/chunk-EFJBUH5F.js +0 -1
- package/dist/chunk-R2TMIMDI.js +0 -3
- package/dist/chunk-UTVLOTWB.js +0 -9
- package/dist/context-mentions-45MODWOY.js +0 -161
- package/dist/event-stream-view-GL3ADRY2.js +0 -1
- package/dist/runtype-tts-entry-UJAEF7NZ.js +0 -1
- package/dist/voice-runtime-EFR3R5ES.js +0 -1
- package/dist/webmcp-runtime-JI7T4QYF.js +0 -6
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visitor conversation-history REST for `AgentWidgetClient` (client token
|
|
3
|
+
* mode): list / get / finalize / delete conversations, reset the visitor, and
|
|
4
|
+
* the identity-proof binding and 401 recovery those calls share.
|
|
5
|
+
*
|
|
6
|
+
* Split out of `client.ts` so the IIFE/CDN bundle ships it as the lazy
|
|
7
|
+
* `client-history.js` sibling chunk (see `client-history-loader.ts`): it only
|
|
8
|
+
* runs once a page lists or opens history. The client's public methods keep
|
|
9
|
+
* their signatures and delegate here through a `ClientHistoryHost`.
|
|
10
|
+
*/
|
|
11
|
+
import type {
|
|
12
|
+
AgentWidgetConfig,
|
|
13
|
+
ClientSession,
|
|
14
|
+
WidgetHistoryInternals,
|
|
15
|
+
HistoryScope,
|
|
16
|
+
HistoryConversationSummary,
|
|
17
|
+
HistoryIdentityStatus,
|
|
18
|
+
HistoryConversationPage,
|
|
19
|
+
HistoryConversationDetail,
|
|
20
|
+
HistoryDisplayProjection
|
|
21
|
+
} from "./types";
|
|
22
|
+
import type { HistoryClientError } from "./client";
|
|
23
|
+
import { VERSION } from "./version";
|
|
24
|
+
|
|
25
|
+
/** Per-message / per-batch display-projection caps (contract facts #5, #15). */
|
|
26
|
+
const DISPLAY_PROJECTION_MESSAGE_CAP = 32768;
|
|
27
|
+
const DISPLAY_PROJECTION_BATCH_CAP = 49152;
|
|
28
|
+
|
|
29
|
+
/** Normalize current history metadata without leaking wire-only aliases. */
|
|
30
|
+
const normalizeHistorySummary = (raw: unknown): HistoryConversationSummary => {
|
|
31
|
+
const row = (raw ?? {}) as Record<string, unknown>;
|
|
32
|
+
const targetId = typeof row.targetId === "string" ? row.targetId : null;
|
|
33
|
+
return {
|
|
34
|
+
id: typeof row.id === "string" ? row.id : "",
|
|
35
|
+
title: typeof row.title === "string" ? row.title : "",
|
|
36
|
+
targetId,
|
|
37
|
+
preview: typeof row.preview === "string" ? row.preview : null,
|
|
38
|
+
messageCount: typeof row.messageCount === "number" ? row.messageCount : 0,
|
|
39
|
+
createdAt: typeof row.createdAt === "string" ? row.createdAt : "",
|
|
40
|
+
updatedAt: typeof row.updatedAt === "string" ? row.updatedAt : "",
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const PROOF_NOT_ADMITTED_MESSAGE =
|
|
45
|
+
"The identity proof was not admitted; account history is unavailable";
|
|
46
|
+
const PROOF_REJECTED_MESSAGE = "The identity proof was rejected";
|
|
47
|
+
const NO_VISITOR_CREDENTIAL_MESSAGE = "The request carried no visitor credential";
|
|
48
|
+
const IDENTITY_MISMATCH_MESSAGE = "The stored visitor belongs to a different signed-in user";
|
|
49
|
+
|
|
50
|
+
export type ClientHistoryHost = {
|
|
51
|
+
/** Core's class: errors must pass `instanceof` checks outside this chunk. */
|
|
52
|
+
readonly HistoryClientError: typeof HistoryClientError;
|
|
53
|
+
readonly config: AgentWidgetConfig;
|
|
54
|
+
readonly historyInternals: WidgetHistoryInternals;
|
|
55
|
+
readonly clientSession: ClientSession | null;
|
|
56
|
+
clientApiBase(): string;
|
|
57
|
+
setHistoryIdentityStatus(next: HistoryIdentityStatus): void;
|
|
58
|
+
browserOnlyStatus(): HistoryIdentityStatus;
|
|
59
|
+
derivedBrowserOnly(): HistoryIdentityStatus;
|
|
60
|
+
reinitWithProof(proof: string): Promise<ClientSession>;
|
|
61
|
+
isHistoryCapable(): boolean;
|
|
62
|
+
clearClientSession(): void;
|
|
63
|
+
initSession(): Promise<ClientSession>;
|
|
64
|
+
readVisitorToken(): Promise<string | null>;
|
|
65
|
+
isClientTokenMode(): boolean;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** `/v1/client/<path>` with a query string; credentials ride in headers. */
|
|
69
|
+
function historyUrl(
|
|
70
|
+
h: ClientHistoryHost,
|
|
71
|
+
path: string,
|
|
72
|
+
query: Record<string, string | number | undefined>
|
|
73
|
+
): string {
|
|
74
|
+
const params = new URLSearchParams();
|
|
75
|
+
for (const [key, value] of Object.entries(query)) {
|
|
76
|
+
if (value === undefined || value === '') continue;
|
|
77
|
+
params.set(key, String(value));
|
|
78
|
+
}
|
|
79
|
+
const search = params.toString();
|
|
80
|
+
return `${h.clientApiBase()}/v1/client/${path}${search ? `?${search}` : ''}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Explicit per-operation scope wins; otherwise config, then evidence. */
|
|
84
|
+
function resolveHistoryScope(
|
|
85
|
+
h: ClientHistoryHost, requested?: HistoryScope): HistoryScope {
|
|
86
|
+
if (requested) return requested;
|
|
87
|
+
const configured = h.config.features?.history?.scope;
|
|
88
|
+
if (configured) return configured;
|
|
89
|
+
return h.config.getIdentityProof ? 'verified-user' : 'browser';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolve the proof for one logical request. `null` is an intentional
|
|
94
|
+
* browser-scope fallback only while the visitor has never been bound.
|
|
95
|
+
*/
|
|
96
|
+
async function resolveIdentityProof(
|
|
97
|
+
h: ClientHistoryHost,
|
|
98
|
+
session: ClientSession,
|
|
99
|
+
track: boolean
|
|
100
|
+
): Promise<string | null> {
|
|
101
|
+
const provider = h.config.getIdentityProof;
|
|
102
|
+
if (!provider) {
|
|
103
|
+
if (track) h.setHistoryIdentityStatus(h.browserOnlyStatus());
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (track) h.setHistoryIdentityStatus({ state: 'verifying' });
|
|
107
|
+
let proof: string | null;
|
|
108
|
+
try {
|
|
109
|
+
proof = (await provider()) ?? null;
|
|
110
|
+
} catch {
|
|
111
|
+
if (track) h.setHistoryIdentityStatus({ state: 'identity_provider_failed' });
|
|
112
|
+
throw new h.HistoryClientError(
|
|
113
|
+
'identity_provider_failed',
|
|
114
|
+
'The identity proof provider failed'
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (proof) return proof;
|
|
118
|
+
if (session.visitor?.endUserId != null) {
|
|
119
|
+
// Never downgrade a bound visitor into its own browser scope.
|
|
120
|
+
if (track) {
|
|
121
|
+
h.setHistoryIdentityStatus({
|
|
122
|
+
state: 'authentication_required',
|
|
123
|
+
reason: 'proof_unavailable_after_binding',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
throw new h.HistoryClientError(
|
|
127
|
+
'authentication_required',
|
|
128
|
+
'This browser is bound to a signed-in user and needs a fresh identity proof'
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (track) {
|
|
132
|
+
h.setHistoryIdentityStatus({
|
|
133
|
+
state: 'browser_only',
|
|
134
|
+
reason: 'proof_unavailable_before_binding',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Bind the visitor before the first verified request; admitted proofs only. */
|
|
141
|
+
async function bindIdentity(
|
|
142
|
+
h: ClientHistoryHost,
|
|
143
|
+
session: ClientSession,
|
|
144
|
+
proof: string,
|
|
145
|
+
track: boolean
|
|
146
|
+
): Promise<ClientSession> {
|
|
147
|
+
if (session.visitor?.endUserId != null) return session;
|
|
148
|
+
const bound = await h.reinitWithProof(proof);
|
|
149
|
+
const admitted =
|
|
150
|
+
bound.visitor?.identityStatus === 'admitted' && bound.visitor?.endUserId != null;
|
|
151
|
+
if (!admitted) {
|
|
152
|
+
// "ignored", or a pre-acknowledgement server that left endUserId null.
|
|
153
|
+
if (track) {
|
|
154
|
+
h.setHistoryIdentityStatus({
|
|
155
|
+
state: 'configuration_error',
|
|
156
|
+
reason: 'proof_not_admitted',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
throw new h.HistoryClientError(
|
|
160
|
+
'proof_not_admitted',
|
|
161
|
+
PROOF_NOT_ADMITTED_MESSAGE
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return bound;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function readErrorCode(
|
|
168
|
+
h: ClientHistoryHost, response: Response): Promise<string> {
|
|
169
|
+
const body = (await response.json().catch(() => ({}))) as Record<string, unknown>;
|
|
170
|
+
if (typeof body.error === 'string') return body.error;
|
|
171
|
+
if (typeof body.message === 'string') return body.message;
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Selective one-shot 401 recovery. Returns the session to retry under;
|
|
177
|
+
* anything unrecoverable throws typed and is never retried.
|
|
178
|
+
*/
|
|
179
|
+
export async function recoverFromUnauthorized(
|
|
180
|
+
h: ClientHistoryHost,
|
|
181
|
+
reason: string,
|
|
182
|
+
proof: string | null,
|
|
183
|
+
track: boolean
|
|
184
|
+
): Promise<ClientSession> {
|
|
185
|
+
const text = reason.toLowerCase();
|
|
186
|
+
if (text.includes('invalid_identity_proof')) {
|
|
187
|
+
if (track) {
|
|
188
|
+
h.setHistoryIdentityStatus({
|
|
189
|
+
state: 'authentication_required',
|
|
190
|
+
reason: 'invalid_identity_proof',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
throw new h.HistoryClientError('invalid_identity_proof', PROOF_REJECTED_MESSAGE);
|
|
194
|
+
}
|
|
195
|
+
if (text.includes('visitor token required')) {
|
|
196
|
+
throw new h.HistoryClientError('visitor_token_missing', NO_VISITOR_CREDENTIAL_MESSAGE);
|
|
197
|
+
}
|
|
198
|
+
if (text.includes('visitor_identity_mismatch')) {
|
|
199
|
+
if (!proof) {
|
|
200
|
+
if (track) {
|
|
201
|
+
h.setHistoryIdentityStatus({
|
|
202
|
+
state: 'authentication_required',
|
|
203
|
+
reason: 'proof_unavailable_after_binding',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
throw new h.HistoryClientError('visitor_identity_mismatch', IDENTITY_MISMATCH_MESSAGE);
|
|
207
|
+
}
|
|
208
|
+
// Same proof: binds this visitor, or gives a different person a clean one.
|
|
209
|
+
return h.reinitWithProof(proof);
|
|
210
|
+
}
|
|
211
|
+
if (text.includes('expired') || text.includes('not found')) {
|
|
212
|
+
h.clearClientSession();
|
|
213
|
+
return h.initSession();
|
|
214
|
+
}
|
|
215
|
+
throw new h.HistoryClientError('unauthorized', reason || 'History request was not authorized');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function historyErrorFor(
|
|
219
|
+
h: ClientHistoryHost,
|
|
220
|
+
response: Response,
|
|
221
|
+
track: boolean
|
|
222
|
+
): Promise<HistoryClientError> {
|
|
223
|
+
const code = await readErrorCode(h, response);
|
|
224
|
+
if (response.status === 404) {
|
|
225
|
+
return new h.HistoryClientError('not_found', 'Conversation not found');
|
|
226
|
+
}
|
|
227
|
+
if (response.status === 429) {
|
|
228
|
+
const header = Number.parseInt(response.headers.get('Retry-After') ?? '', 10);
|
|
229
|
+
return new h.HistoryClientError('rate_limited', 'Too many history requests', {
|
|
230
|
+
...(Number.isFinite(header) ? { retryAfterSeconds: header } : {}),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (response.status === 503 && code.includes('identity_proof_not_admitted')) {
|
|
234
|
+
if (track) {
|
|
235
|
+
h.setHistoryIdentityStatus({
|
|
236
|
+
state: 'configuration_error',
|
|
237
|
+
reason: 'proof_not_admitted',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return new h.HistoryClientError(
|
|
241
|
+
'proof_not_admitted',
|
|
242
|
+
PROOF_NOT_ADMITTED_MESSAGE
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (response.status === 401) {
|
|
246
|
+
const text = code.toLowerCase();
|
|
247
|
+
if (text.includes('invalid_identity_proof')) {
|
|
248
|
+
if (track) {
|
|
249
|
+
h.setHistoryIdentityStatus({
|
|
250
|
+
state: 'authentication_required',
|
|
251
|
+
reason: 'invalid_identity_proof',
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return new h.HistoryClientError('invalid_identity_proof', PROOF_REJECTED_MESSAGE);
|
|
255
|
+
}
|
|
256
|
+
if (text.includes('visitor token required')) {
|
|
257
|
+
return new h.HistoryClientError('visitor_token_missing', NO_VISITOR_CREDENTIAL_MESSAGE);
|
|
258
|
+
}
|
|
259
|
+
if (text.includes('visitor_identity_mismatch')) {
|
|
260
|
+
return new h.HistoryClientError('visitor_identity_mismatch', IDENTITY_MISMATCH_MESSAGE);
|
|
261
|
+
}
|
|
262
|
+
return new h.HistoryClientError('unauthorized', code || 'History request was not authorized');
|
|
263
|
+
}
|
|
264
|
+
return new h.HistoryClientError(
|
|
265
|
+
'request_failed',
|
|
266
|
+
code || `History request failed (${response.status})`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Validate `X-History-Identity-Status` before any body is committed: the
|
|
272
|
+
* per-operation acknowledgement, not a prior init, is what proves scope.
|
|
273
|
+
*/
|
|
274
|
+
function commitIdentityAcknowledgement(
|
|
275
|
+
h: ClientHistoryHost,
|
|
276
|
+
header: string | null,
|
|
277
|
+
proofSent: boolean,
|
|
278
|
+
track: boolean
|
|
279
|
+
): void {
|
|
280
|
+
const value = header?.toLowerCase() ?? null;
|
|
281
|
+
if (proofSent) {
|
|
282
|
+
if (value === 'admitted') {
|
|
283
|
+
if (track) h.setHistoryIdentityStatus({ state: 'verified' });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (track) {
|
|
287
|
+
h.setHistoryIdentityStatus({
|
|
288
|
+
state: 'configuration_error',
|
|
289
|
+
reason: 'proof_not_admitted',
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (value === 'ignored') {
|
|
293
|
+
// The gate-off path must fail before I/O; a 2xx here is a broken server.
|
|
294
|
+
throw new h.HistoryClientError(
|
|
295
|
+
'identity_contract_violation',
|
|
296
|
+
'Server reported an ignored identity proof on a success response'
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
throw new h.HistoryClientError(
|
|
300
|
+
'proof_not_admitted',
|
|
301
|
+
'The response did not acknowledge the identity proof this request sent'
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (value === 'admitted') {
|
|
305
|
+
throw new h.HistoryClientError(
|
|
306
|
+
'identity_contract_violation',
|
|
307
|
+
'Server admitted an identity proof that was never sent'
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
// "not_provided", a missing header (rolling deploy), or an unknown value:
|
|
311
|
+
// no verified claim was made either way.
|
|
312
|
+
if (track) h.setHistoryIdentityStatus(h.browserOnlyStatus());
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Shared history transport: live session, `sessionId` query param,
|
|
317
|
+
* `X-Visitor-Token` header, one resolved proof, one-shot 401 recovery, and a
|
|
318
|
+
* credential-revision guard that discards a response the store outran.
|
|
319
|
+
*/
|
|
320
|
+
async function historyFetch<T>(
|
|
321
|
+
h: ClientHistoryHost,
|
|
322
|
+
path: string,
|
|
323
|
+
opts: {
|
|
324
|
+
method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
|
325
|
+
query?: Record<string, string | number | undefined>;
|
|
326
|
+
scope?: HistoryScope;
|
|
327
|
+
body?: unknown;
|
|
328
|
+
/** Reset tolerates a missing credential; every other route requires one. */
|
|
329
|
+
visitorTokenOptional?: boolean;
|
|
330
|
+
keepalive?: boolean;
|
|
331
|
+
/** Transport ops validate the acknowledgement but publish no status. */
|
|
332
|
+
trackIdentity?: boolean;
|
|
333
|
+
}
|
|
334
|
+
): Promise<T> {
|
|
335
|
+
assertHistoryUsable(h);
|
|
336
|
+
const track = opts.trackIdentity !== false;
|
|
337
|
+
let session = await h.initSession();
|
|
338
|
+
|
|
339
|
+
// Resolved once per logical request: a recovery retry reuses the same proof
|
|
340
|
+
// so one action cannot switch identities midway.
|
|
341
|
+
let proof: string | null = null;
|
|
342
|
+
if (resolveHistoryScope(h, opts.scope) === 'verified-user') {
|
|
343
|
+
proof = await resolveIdentityProof(h, session, track);
|
|
344
|
+
if (proof) session = await bindIdentity(h, session, proof, track);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const store = h.historyInternals.visitorStore;
|
|
348
|
+
let recovered = false;
|
|
349
|
+
for (;;) {
|
|
350
|
+
const capturedRevision = store?.revision() ?? 0;
|
|
351
|
+
const visitorToken = await h.readVisitorToken();
|
|
352
|
+
if (!visitorToken && !opts.visitorTokenOptional) {
|
|
353
|
+
throw new h.HistoryClientError(
|
|
354
|
+
'visitor_token_missing',
|
|
355
|
+
'No visitor credential is stored for this browser'
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
const response = await fetch(
|
|
359
|
+
historyUrl(h, path, { ...opts.query, sessionId: session.sessionId }),
|
|
360
|
+
{
|
|
361
|
+
method: opts.method,
|
|
362
|
+
headers: {
|
|
363
|
+
'X-Persona-Version': VERSION,
|
|
364
|
+
...(visitorToken ? { 'X-Visitor-Token': visitorToken } : {}),
|
|
365
|
+
...(proof ? { 'X-Identity-Proof': proof } : {}),
|
|
366
|
+
...(opts.body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
367
|
+
},
|
|
368
|
+
...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
|
|
369
|
+
...(opts.keepalive ? { keepalive: true } : {}),
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
if (response.status === 401 && !recovered) {
|
|
374
|
+
recovered = true;
|
|
375
|
+
session = await recoverFromUnauthorized(h,
|
|
376
|
+
await readErrorCode(h, response),
|
|
377
|
+
proof,
|
|
378
|
+
track
|
|
379
|
+
);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (!response.ok) {
|
|
383
|
+
throw await historyErrorFor(h, response, track);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const acknowledgement = response.headers.get('X-History-Identity-Status');
|
|
387
|
+
const data = (await response.json().catch(() => undefined)) as T;
|
|
388
|
+
if (store && store.revision() !== capturedRevision) {
|
|
389
|
+
// Another tab reset or replaced the credential: this body is stale.
|
|
390
|
+
throw new h.HistoryClientError(
|
|
391
|
+
'credential_changed',
|
|
392
|
+
'The visitor credential changed while the request was in flight'
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
commitIdentityAcknowledgement(h, acknowledgement, proof !== null, track);
|
|
396
|
+
return data;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function assertHistoryUsable(h: ClientHistoryHost): void {
|
|
401
|
+
if (!h.isClientTokenMode()) {
|
|
402
|
+
throw new Error('Conversation history is only available in client token mode');
|
|
403
|
+
}
|
|
404
|
+
if (!h.isHistoryCapable()) {
|
|
405
|
+
throw new h.HistoryClientError(
|
|
406
|
+
'history_disabled',
|
|
407
|
+
'Visitor history is disabled for this surface'
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** One page of the visitor's conversations, newest first. */
|
|
413
|
+
export async function listConversations(
|
|
414
|
+
h: ClientHistoryHost, opts?: {
|
|
415
|
+
cursor?: string;
|
|
416
|
+
limit?: number;
|
|
417
|
+
targetId?: string;
|
|
418
|
+
scope?: HistoryScope;
|
|
419
|
+
}): Promise<HistoryConversationPage> {
|
|
420
|
+
const page = await historyFetch<{
|
|
421
|
+
data?: unknown[];
|
|
422
|
+
nextCursor?: string | null;
|
|
423
|
+
}>(h, 'conversations', {
|
|
424
|
+
method: 'GET',
|
|
425
|
+
query: {
|
|
426
|
+
...(opts?.cursor ? { cursor: opts.cursor } : {}),
|
|
427
|
+
...(opts?.limit !== undefined ? { limit: opts.limit } : {}),
|
|
428
|
+
...(opts?.targetId ? { targetId: opts.targetId } : {}),
|
|
429
|
+
},
|
|
430
|
+
...(opts?.scope ? { scope: opts.scope } : {}),
|
|
431
|
+
});
|
|
432
|
+
return {
|
|
433
|
+
data: (Array.isArray(page?.data) ? page.data : []).map(normalizeHistorySummary),
|
|
434
|
+
nextCursor: typeof page?.nextCursor === 'string' ? page.nextCursor : null,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* One transcript page (newest first, oldest-first within the page). Messages
|
|
440
|
+
* stay on the wire shape: `utils/history-messages.ts` owns the mapping.
|
|
441
|
+
*/
|
|
442
|
+
export async function getConversation(
|
|
443
|
+
h: ClientHistoryHost,
|
|
444
|
+
conversationId: string,
|
|
445
|
+
opts?: { messageCursor?: string; scope?: HistoryScope }
|
|
446
|
+
): Promise<HistoryConversationDetail> {
|
|
447
|
+
const detail = await historyFetch<Record<string, unknown>>(
|
|
448
|
+
h, `conversations/${encodeURIComponent(conversationId)}`,
|
|
449
|
+
{
|
|
450
|
+
method: 'GET',
|
|
451
|
+
query: {
|
|
452
|
+
...(opts?.messageCursor ? { messageCursor: opts.messageCursor } : {}),
|
|
453
|
+
},
|
|
454
|
+
...(opts?.scope ? { scope: opts.scope } : {}),
|
|
455
|
+
}
|
|
456
|
+
);
|
|
457
|
+
return {
|
|
458
|
+
summary: normalizeHistorySummary(detail),
|
|
459
|
+
messages: Array.isArray(detail?.messages)
|
|
460
|
+
? (detail.messages as HistoryConversationDetail['messages'])
|
|
461
|
+
: [],
|
|
462
|
+
nextMessageCursor:
|
|
463
|
+
typeof detail?.nextMessageCursor === 'string' ? detail.nextMessageCursor : null,
|
|
464
|
+
conversationRevision:
|
|
465
|
+
typeof detail?.conversationRevision === 'string' ? detail.conversationRevision : null,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Finalize browser-derived visitor-visible projections for the ACTIVE
|
|
471
|
+
* conversation. Transport op, not a history read: current session + browser
|
|
472
|
+
* scope, never an identity proof.
|
|
473
|
+
*/
|
|
474
|
+
export async function finalizeDisplayProjections(
|
|
475
|
+
h: ClientHistoryHost,
|
|
476
|
+
conversationId: string,
|
|
477
|
+
messages: HistoryDisplayProjection[]
|
|
478
|
+
): Promise<{ conversationRevision: string | null }> {
|
|
479
|
+
assertHistoryUsable(h);
|
|
480
|
+
let total = 0;
|
|
481
|
+
for (const message of messages) {
|
|
482
|
+
const size = message.displayContent.length;
|
|
483
|
+
if (size > DISPLAY_PROJECTION_MESSAGE_CAP) {
|
|
484
|
+
throw new h.HistoryClientError(
|
|
485
|
+
'payload_too_large',
|
|
486
|
+
`displayContent exceeds the ${DISPLAY_PROJECTION_MESSAGE_CAP} character limit`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
total += size;
|
|
490
|
+
}
|
|
491
|
+
if (total > DISPLAY_PROJECTION_BATCH_CAP) {
|
|
492
|
+
throw new h.HistoryClientError(
|
|
493
|
+
'payload_too_large',
|
|
494
|
+
`Display projection batch exceeds the ${DISPLAY_PROJECTION_BATCH_CAP} character limit`
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
await h.initSession();
|
|
499
|
+
const store = h.historyInternals.visitorStore;
|
|
500
|
+
const capturedRevision = store?.revision() ?? 0;
|
|
501
|
+
const result = await historyFetch<{ conversationRevision?: string }>(
|
|
502
|
+
h, `conversations/${encodeURIComponent(conversationId)}/display-projections`,
|
|
503
|
+
{
|
|
504
|
+
method: 'PATCH',
|
|
505
|
+
body: { messages },
|
|
506
|
+
scope: 'browser',
|
|
507
|
+
keepalive: total <= DISPLAY_PROJECTION_BATCH_CAP,
|
|
508
|
+
trackIdentity: false,
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
const conversationRevision =
|
|
512
|
+
typeof result?.conversationRevision === 'string' ? result.conversationRevision : null;
|
|
513
|
+
// Install only while the same record and the same credential are still live.
|
|
514
|
+
if (
|
|
515
|
+
conversationRevision &&
|
|
516
|
+
h.clientSession?.conversationId === conversationId &&
|
|
517
|
+
(store?.revision() ?? 0) === capturedRevision
|
|
518
|
+
) {
|
|
519
|
+
h.historyInternals.setStoredConversationRevision?.(conversationRevision);
|
|
520
|
+
}
|
|
521
|
+
return { conversationRevision };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export async function deleteConversation(
|
|
525
|
+
h: ClientHistoryHost,
|
|
526
|
+
conversationId: string,
|
|
527
|
+
opts?: { scope?: HistoryScope }
|
|
528
|
+
): Promise<{ deleted: number }> {
|
|
529
|
+
const result = await historyFetch<{ deleted?: number }>(
|
|
530
|
+
h, `conversations/${encodeURIComponent(conversationId)}`,
|
|
531
|
+
{
|
|
532
|
+
method: 'DELETE',
|
|
533
|
+
...(opts?.scope ? { scope: opts.scope } : {}),
|
|
534
|
+
}
|
|
535
|
+
);
|
|
536
|
+
return { deleted: typeof result?.deleted === 'number' ? result.deleted : 0 };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Delete every conversation matching `targetId`. An omitted filter means the
|
|
541
|
+
* whole authorized visitor/client-token scope: headless callers own that
|
|
542
|
+
* choice, the UI always passes the active `ClientSession.targetId`.
|
|
543
|
+
*/
|
|
544
|
+
export async function deleteAllConversations(
|
|
545
|
+
h: ClientHistoryHost, opts?: {
|
|
546
|
+
targetId?: string;
|
|
547
|
+
scope?: HistoryScope;
|
|
548
|
+
}): Promise<{ deleted: number }> {
|
|
549
|
+
const result = await historyFetch<{ deleted?: number }>(h, 'conversations', {
|
|
550
|
+
method: 'DELETE',
|
|
551
|
+
query: {
|
|
552
|
+
...(opts?.targetId !== undefined ? { targetId: opts.targetId } : {}),
|
|
553
|
+
},
|
|
554
|
+
...(opts?.scope ? { scope: opts.scope } : {}),
|
|
555
|
+
});
|
|
556
|
+
return { deleted: typeof result?.deleted === 'number' ? result.deleted : 0 };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Revoke this browser's visitor credential. Always browser scope (the route
|
|
561
|
+
* accepts no proof) and the local clear is unconditional: a failed remote
|
|
562
|
+
* revocation still detaches this device.
|
|
563
|
+
*/
|
|
564
|
+
export async function resetVisitor(h: ClientHistoryHost): Promise<{ reset: true }> {
|
|
565
|
+
assertHistoryUsable(h);
|
|
566
|
+
h.setHistoryIdentityStatus({ state: 'resetting' });
|
|
567
|
+
try {
|
|
568
|
+
await historyFetch<{ reset?: boolean }>(h, 'visitor/reset', {
|
|
569
|
+
method: 'POST',
|
|
570
|
+
scope: 'browser',
|
|
571
|
+
visitorTokenOptional: true,
|
|
572
|
+
});
|
|
573
|
+
} finally {
|
|
574
|
+
await h.historyInternals.visitorStore?.clear();
|
|
575
|
+
// Post-reset resting state: this browser is a never-bound visitor again.
|
|
576
|
+
h.setHistoryIdentityStatus(h.derivedBrowserOnly());
|
|
577
|
+
}
|
|
578
|
+
return { reset: true };
|
|
579
|
+
}
|