@blario/react-native 0.0.0 → 0.1.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 +21 -0
- package/README.md +378 -0
- package/dist/BlarProvider.d.ts +74 -0
- package/dist/actions/batched.d.ts +1 -0
- package/dist/actions/click.d.ts +4 -0
- package/dist/actions/commit-watch.d.ts +15 -0
- package/dist/actions/events.d.ts +35 -0
- package/dist/actions/executor.d.ts +29 -0
- package/dist/actions/highlight.d.ts +4 -0
- package/dist/actions/interaction-guards.d.ts +9 -0
- package/dist/actions/landing.d.ts +11 -0
- package/dist/actions/navigation-driver.d.ts +22 -0
- package/dist/actions/parse.d.ts +4 -0
- package/dist/actions/result.d.ts +4 -0
- package/dist/actions/scrolling.d.ts +12 -0
- package/dist/actions/services.d.ts +20 -0
- package/dist/actions/settle.d.ts +13 -0
- package/dist/actions/switch.d.ts +3 -0
- package/dist/actions/target.d.ts +25 -0
- package/dist/actions/text-input.d.ts +9 -0
- package/dist/actions/types.d.ts +59 -0
- package/dist/agent/batch-commands.d.ts +17 -0
- package/dist/agent/compose.d.ts +23 -0
- package/dist/agent/controller.d.ts +28 -0
- package/dist/agent/frame-runner.d.ts +9 -0
- package/dist/agent/frames.d.ts +180 -0
- package/dist/agent/handlers/agent-command.d.ts +5 -0
- package/dist/agent/handlers/batch.d.ts +5 -0
- package/dist/agent/handlers/consent-screenshot.d.ts +4 -0
- package/dist/agent/handlers/context.d.ts +16 -0
- package/dist/agent/handlers/dom-state.d.ts +4 -0
- package/dist/agent/handlers/element-context.d.ts +2 -0
- package/dist/agent/handlers/screenshot.d.ts +3 -0
- package/dist/agent/handlers/text-dom.d.ts +5 -0
- package/dist/agent/stage.d.ts +13 -0
- package/dist/agent-mode/action-description.d.ts +3 -0
- package/dist/agent-mode/bubbles.d.ts +10 -0
- package/dist/agent-mode/colors.d.ts +12 -0
- package/dist/agent-mode/controller.d.ts +92 -0
- package/dist/agent-mode/cursor-follow.d.ts +12 -0
- package/dist/agent-mode/cursor.d.ts +37 -0
- package/dist/agent-mode/frames.d.ts +36 -0
- package/dist/agent-mode/haptics.d.ts +6 -0
- package/dist/agent-mode/keep-awake.d.ts +19 -0
- package/dist/agent-mode/layer-stacking.d.ts +7 -0
- package/dist/agent-mode/markdown.d.ts +19 -0
- package/dist/agent-mode/panel-geometry.d.ts +6 -0
- package/dist/agent-mode/resume.d.ts +13 -0
- package/dist/agent-mode/run-memory.d.ts +19 -0
- package/dist/agent-mode/spotlight-controller.d.ts +68 -0
- package/dist/agent-mode/spotlight.d.ts +6 -0
- package/dist/agent-mode/state.d.ts +9 -0
- package/dist/agent-mode/status.d.ts +11 -0
- package/dist/agent-mode/timers.d.ts +6 -0
- package/dist/agent-mode/ui/AgentDock.d.ts +37 -0
- package/dist/agent-mode/ui/AgentLayer.d.ts +19 -0
- package/dist/agent-mode/ui/AgentLayerHost.d.ts +8 -0
- package/dist/agent-mode/ui/AgentPanel.d.ts +19 -0
- package/dist/agent-mode/ui/AgentTap.d.ts +10 -0
- package/dist/agent-mode/ui/MarkdownText.d.ts +8 -0
- package/dist/agent-mode/ui/MessageBubbles.d.ts +9 -0
- package/dist/agent-mode/ui/PendingOverlay.d.ts +5 -0
- package/dist/agent-mode/ui/SpotlightLayer.d.ts +9 -0
- package/dist/agent-mode/ui/SpotlightOverlay.d.ts +7 -0
- package/dist/agent-mode/ui/StatusCard.d.ts +14 -0
- package/dist/agent-mode/ui/TargetPulse.d.ts +9 -0
- package/dist/agent-mode/ui/TouchShield.d.ts +5 -0
- package/dist/agent-mode/ui/WarningIcon.d.ts +6 -0
- package/dist/agent-mode/ui/full-window-overlay.d.ts +5 -0
- package/dist/agent-mode/ui/layer-host.d.ts +2 -0
- package/dist/backend/api-client.d.ts +38 -0
- package/dist/backend/api-types.d.ts +25 -0
- package/dist/backend/app-info.d.ts +14 -0
- package/dist/backend/backend-transport.d.ts +70 -0
- package/dist/backend/backoff.d.ts +9 -0
- package/dist/backend/base64.d.ts +1 -0
- package/dist/backend/errors.d.ts +17 -0
- package/dist/backend/foreground.d.ts +1 -0
- package/dist/backend/health.d.ts +17 -0
- package/dist/backend/identity-proof.d.ts +23 -0
- package/dist/backend/identity.d.ts +27 -0
- package/dist/backend/message-metadata.d.ts +19 -0
- package/dist/backend/presence.d.ts +20 -0
- package/dist/backend/response-cache.d.ts +9 -0
- package/dist/backend/session-change.d.ts +2 -0
- package/dist/backend/session-socket.d.ts +49 -0
- package/dist/backend/session.d.ts +39 -0
- package/dist/backend/socket-url.d.ts +9 -0
- package/dist/backend/storage.d.ts +13 -0
- package/dist/backend/tab-id.d.ts +2 -0
- package/dist/blar.d.ts +37 -0
- package/dist/chat/ChatLayer.d.ts +29 -0
- package/dist/chat/ChatSheet.d.ts +11 -0
- package/dist/chat/ChatView.d.ts +19 -0
- package/dist/chat/Launcher.d.ts +14 -0
- package/dist/chat/bridge.d.ts +22 -0
- package/dist/chat/chat-coordinator.d.ts +57 -0
- package/dist/chat/chat-state.d.ts +20 -0
- package/dist/chat/navigation.d.ts +5 -0
- package/dist/chat/options.d.ts +22 -0
- package/dist/chat/webview.d.ts +10 -0
- package/dist/chat/widget-bundle.generated.d.ts +2 -0
- package/dist/chat/widget-shell.d.ts +24 -0
- package/dist/confirmation/port.d.ts +12 -0
- package/dist/fiber/locate.d.ts +7 -0
- package/dist/fiber/narrow.d.ts +5 -0
- package/dist/fiber/reader.d.ts +10 -0
- package/dist/fiber/text.d.ts +4 -0
- package/dist/fiber/types.d.ts +21 -0
- package/dist/host/border-radius.d.ts +1 -0
- package/dist/host/instance.d.ts +9 -0
- package/dist/host/measure.d.ts +3 -0
- package/dist/host/rect.d.ts +22 -0
- package/dist/host/visible-area.d.ts +2 -0
- package/dist/index.d.ts +10 -0
- package/dist/nudge/NudgeBubble.d.ts +15 -0
- package/dist/nudge/nudge.d.ts +23 -0
- package/dist/optional/expo-haptics.d.ts +1 -0
- package/dist/optional/expo-keep-awake.d.ts +1 -0
- package/dist/optional/react-native-haptic-feedback.d.ts +1 -0
- package/dist/optional/react-native-keep-awake.d.ts +1 -0
- package/dist/overlay/AgentOverlay.d.ts +5 -0
- package/dist/screenshot/capture-backend.d.ts +6 -0
- package/dist/screenshot/screenshotter.d.ts +18 -0
- package/dist/screenshot/skia.d.ts +35 -0
- package/dist/screenshot/view-shot.d.ts +2 -0
- package/dist/snapshot/capture.d.ts +36 -0
- package/dist/snapshot/classify.d.ts +11 -0
- package/dist/snapshot/collect.d.ts +27 -0
- package/dist/snapshot/element-context.d.ts +9 -0
- package/dist/snapshot/icon.d.ts +2 -0
- package/dist/snapshot/kinds.d.ts +3 -0
- package/dist/snapshot/modals.d.ts +2 -0
- package/dist/snapshot/navigation.d.ts +14 -0
- package/dist/snapshot/ordering.d.ts +2 -0
- package/dist/snapshot/properties.d.ts +14 -0
- package/dist/snapshot/registry.d.ts +16 -0
- package/dist/snapshot/render.d.ts +10 -0
- package/dist/snapshot/route-summary.d.ts +3 -0
- package/dist/snapshot/scroll.d.ts +15 -0
- package/dist/snapshot/text-layout.d.ts +8 -0
- package/dist/snapshot/view.d.ts +30 -0
- package/dist/snapshot/visibility.d.ts +17 -0
- package/dist/snapshot/wrappers.d.ts +11 -0
- package/dist/telemetry/SdkErrorBoundary.d.ts +15 -0
- package/dist/telemetry/build-info.generated.d.ts +2 -0
- package/dist/telemetry/envelope.d.ts +53 -0
- package/dist/telemetry/reporter.d.ts +33 -0
- package/dist/telemetry/scrub.d.ts +1 -0
- package/dist/telemetry/sdk-reporter.d.ts +5 -0
- package/dist/transport/connection-options.d.ts +18 -0
- package/dist/transport/relay-transport.d.ts +17 -0
- package/dist/transport/transport.d.ts +7 -0
- package/dist/ui/insets.d.ts +8 -0
- package/dist/ui/strings.d.ts +33 -0
- package/dist/ui/useInsets.d.ts +2 -0
- package/dist/version.d.ts +1 -0
- package/package.json +84 -3
- package/src/BlarProvider.tsx +462 -0
- package/src/actions/batched.ts +16 -0
- package/src/actions/click.ts +54 -0
- package/src/actions/commit-watch.ts +86 -0
- package/src/actions/events.ts +58 -0
- package/src/actions/executor.ts +147 -0
- package/src/actions/highlight.ts +13 -0
- package/src/actions/interaction-guards.ts +44 -0
- package/src/actions/landing.ts +26 -0
- package/src/actions/navigation-driver.ts +131 -0
- package/src/actions/parse.ts +82 -0
- package/src/actions/result.ts +14 -0
- package/src/actions/scrolling.ts +112 -0
- package/src/actions/services.ts +24 -0
- package/src/actions/settle.ts +40 -0
- package/src/actions/switch.ts +31 -0
- package/src/actions/target.ts +77 -0
- package/src/actions/text-input.ts +82 -0
- package/src/actions/types.ts +52 -0
- package/src/agent/batch-commands.ts +33 -0
- package/src/agent/compose.ts +67 -0
- package/src/agent/controller.ts +106 -0
- package/src/agent/frame-runner.ts +27 -0
- package/src/agent/frames.ts +193 -0
- package/src/agent/handlers/agent-command.ts +74 -0
- package/src/agent/handlers/batch.ts +72 -0
- package/src/agent/handlers/consent-screenshot.ts +44 -0
- package/src/agent/handlers/context.ts +17 -0
- package/src/agent/handlers/dom-state.ts +19 -0
- package/src/agent/handlers/element-context.ts +9 -0
- package/src/agent/handlers/screenshot.ts +24 -0
- package/src/agent/handlers/text-dom.ts +17 -0
- package/src/agent/stage.ts +22 -0
- package/src/agent-mode/action-description.ts +25 -0
- package/src/agent-mode/bubbles.ts +31 -0
- package/src/agent-mode/colors.ts +40 -0
- package/src/agent-mode/controller.ts +459 -0
- package/src/agent-mode/cursor-follow.ts +43 -0
- package/src/agent-mode/cursor.ts +118 -0
- package/src/agent-mode/frames.ts +70 -0
- package/src/agent-mode/haptics.ts +49 -0
- package/src/agent-mode/keep-awake.ts +107 -0
- package/src/agent-mode/layer-stacking.ts +16 -0
- package/src/agent-mode/markdown.ts +84 -0
- package/src/agent-mode/panel-geometry.ts +27 -0
- package/src/agent-mode/resume.ts +16 -0
- package/src/agent-mode/run-memory.ts +65 -0
- package/src/agent-mode/spotlight-controller.ts +255 -0
- package/src/agent-mode/spotlight.ts +14 -0
- package/src/agent-mode/state.ts +36 -0
- package/src/agent-mode/status.ts +13 -0
- package/src/agent-mode/timers.ts +11 -0
- package/src/agent-mode/ui/AgentDock.tsx +285 -0
- package/src/agent-mode/ui/AgentLayer.tsx +123 -0
- package/src/agent-mode/ui/AgentLayerHost.tsx +37 -0
- package/src/agent-mode/ui/AgentPanel.tsx +187 -0
- package/src/agent-mode/ui/AgentTap.tsx +190 -0
- package/src/agent-mode/ui/MarkdownText.tsx +55 -0
- package/src/agent-mode/ui/MessageBubbles.tsx +58 -0
- package/src/agent-mode/ui/PendingOverlay.tsx +32 -0
- package/src/agent-mode/ui/SpotlightLayer.tsx +60 -0
- package/src/agent-mode/ui/SpotlightOverlay.tsx +35 -0
- package/src/agent-mode/ui/StatusCard.tsx +137 -0
- package/src/agent-mode/ui/TargetPulse.tsx +74 -0
- package/src/agent-mode/ui/TouchShield.tsx +22 -0
- package/src/agent-mode/ui/WarningIcon.tsx +32 -0
- package/src/agent-mode/ui/full-window-overlay.ts +16 -0
- package/src/agent-mode/ui/layer-host.ts +11 -0
- package/src/backend/api-client.ts +210 -0
- package/src/backend/api-types.ts +56 -0
- package/src/backend/app-info.ts +40 -0
- package/src/backend/backend-transport.ts +338 -0
- package/src/backend/backoff.ts +16 -0
- package/src/backend/base64.ts +25 -0
- package/src/backend/errors.ts +32 -0
- package/src/backend/foreground.ts +10 -0
- package/src/backend/health.ts +64 -0
- package/src/backend/identity-proof.ts +151 -0
- package/src/backend/identity.ts +31 -0
- package/src/backend/message-metadata.ts +28 -0
- package/src/backend/presence.ts +95 -0
- package/src/backend/response-cache.ts +45 -0
- package/src/backend/session-change.ts +14 -0
- package/src/backend/session-socket.ts +292 -0
- package/src/backend/session.ts +188 -0
- package/src/backend/socket-url.ts +25 -0
- package/src/backend/storage.ts +49 -0
- package/src/backend/tab-id.ts +24 -0
- package/src/blar.ts +100 -0
- package/src/chat/ChatLayer.tsx +109 -0
- package/src/chat/ChatSheet.tsx +82 -0
- package/src/chat/ChatView.tsx +127 -0
- package/src/chat/Launcher.tsx +72 -0
- package/src/chat/bridge.ts +56 -0
- package/src/chat/chat-coordinator.ts +189 -0
- package/src/chat/chat-state.ts +35 -0
- package/src/chat/navigation.ts +15 -0
- package/src/chat/options.ts +39 -0
- package/src/chat/webview.ts +23 -0
- package/src/chat/widget-bundle.generated.ts +2 -0
- package/src/chat/widget-shell.ts +170 -0
- package/src/confirmation/port.ts +15 -0
- package/src/fiber/locate.ts +41 -0
- package/src/fiber/narrow.ts +15 -0
- package/src/fiber/reader.ts +98 -0
- package/src/fiber/text.ts +52 -0
- package/src/fiber/types.ts +24 -0
- package/src/host/border-radius.ts +11 -0
- package/src/host/instance.ts +30 -0
- package/src/host/measure.ts +25 -0
- package/src/host/rect.ts +45 -0
- package/src/host/visible-area.ts +5 -0
- package/src/index.ts +10 -0
- package/src/nudge/NudgeBubble.tsx +117 -0
- package/src/nudge/nudge.ts +100 -0
- package/src/optional/expo-haptics.ts +11 -0
- package/src/optional/expo-keep-awake.ts +11 -0
- package/src/optional/react-native-haptic-feedback.ts +11 -0
- package/src/optional/react-native-keep-awake.ts +11 -0
- package/src/overlay/AgentOverlay.tsx +16 -0
- package/src/screenshot/capture-backend.ts +7 -0
- package/src/screenshot/screenshotter.ts +39 -0
- package/src/screenshot/skia.ts +107 -0
- package/src/screenshot/view-shot.ts +41 -0
- package/src/snapshot/capture.ts +163 -0
- package/src/snapshot/classify.ts +40 -0
- package/src/snapshot/collect.ts +136 -0
- package/src/snapshot/element-context.ts +66 -0
- package/src/snapshot/icon.ts +21 -0
- package/src/snapshot/kinds.ts +10 -0
- package/src/snapshot/modals.ts +21 -0
- package/src/snapshot/navigation.ts +47 -0
- package/src/snapshot/ordering.ts +26 -0
- package/src/snapshot/properties.ts +71 -0
- package/src/snapshot/registry.ts +33 -0
- package/src/snapshot/render.ts +140 -0
- package/src/snapshot/route-summary.ts +15 -0
- package/src/snapshot/scroll.ts +90 -0
- package/src/snapshot/text-layout.ts +59 -0
- package/src/snapshot/view.ts +87 -0
- package/src/snapshot/visibility.ts +63 -0
- package/src/snapshot/wrappers.ts +69 -0
- package/src/telemetry/SdkErrorBoundary.tsx +28 -0
- package/src/telemetry/build-info.generated.ts +2 -0
- package/src/telemetry/envelope.ts +83 -0
- package/src/telemetry/reporter.ts +170 -0
- package/src/telemetry/scrub.ts +10 -0
- package/src/telemetry/sdk-reporter.ts +37 -0
- package/src/transport/connection-options.ts +65 -0
- package/src/transport/relay-transport.ts +82 -0
- package/src/transport/transport.ts +8 -0
- package/src/ui/insets.ts +31 -0
- package/src/ui/strings.ts +102 -0
- package/src/ui/useInsets.ts +10 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import type { OutboundFrame } from '../agent/frames.ts';
|
|
2
|
+
import { type RunMemory, StoredRunMemory } from '../agent-mode/run-memory.ts';
|
|
3
|
+
import { isRecord } from '../fiber/narrow.ts';
|
|
4
|
+
import { reportSdkError, setReportIdentityProvider, setReportPublishableKey } from '../telemetry/sdk-reporter.ts';
|
|
5
|
+
import type { AgentTransport } from '../transport/transport.ts';
|
|
6
|
+
import { ApiClient } from './api-client.ts';
|
|
7
|
+
import { watchForeground } from './foreground.ts';
|
|
8
|
+
import { ClientHealthProbe } from './health.ts';
|
|
9
|
+
import type { BlarIdentity } from './identity.ts';
|
|
10
|
+
import { PresenceManager } from './presence.ts';
|
|
11
|
+
import { type SessionState, BackendSession } from './session.ts';
|
|
12
|
+
import { watchSessionChanges } from './session-change.ts';
|
|
13
|
+
import { SessionSocket, type SocketState } from './session-socket.ts';
|
|
14
|
+
import { createDefaultStorage, type KeyValueStorage, MemoryStorage } from './storage.ts';
|
|
15
|
+
import { getTabId } from './tab-id.ts';
|
|
16
|
+
|
|
17
|
+
export interface BackendTransportOptions {
|
|
18
|
+
publishableKey: string;
|
|
19
|
+
apiBaseUrl: string;
|
|
20
|
+
user: BlarIdentity | null;
|
|
21
|
+
storage?: KeyValueStorage;
|
|
22
|
+
onSocketState?: (state: SocketState) => void;
|
|
23
|
+
onConnecting?: (sessionId: number) => void;
|
|
24
|
+
onSessionChanged?: () => void;
|
|
25
|
+
log?: (message: string) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface WidgetProof {
|
|
29
|
+
identityToken?: string;
|
|
30
|
+
identitySignature?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type FrameListener = (frame: Record<string, unknown>) => void;
|
|
34
|
+
|
|
35
|
+
export class BackendTransport implements AgentTransport {
|
|
36
|
+
private readonly api: ApiClient;
|
|
37
|
+
|
|
38
|
+
private readonly session: BackendSession;
|
|
39
|
+
|
|
40
|
+
private readonly runMemory: StoredRunMemory;
|
|
41
|
+
|
|
42
|
+
private readonly health = new ClientHealthProbe();
|
|
43
|
+
|
|
44
|
+
private readonly presence: PresenceManager;
|
|
45
|
+
|
|
46
|
+
private readonly socket: SessionSocket;
|
|
47
|
+
|
|
48
|
+
private readonly tabId = getTabId();
|
|
49
|
+
|
|
50
|
+
private readonly frameListeners = new Set<FrameListener>();
|
|
51
|
+
|
|
52
|
+
private readonly identityListeners = new Set<() => void>();
|
|
53
|
+
|
|
54
|
+
private agentListener: ((raw: string) => void) | null = null;
|
|
55
|
+
|
|
56
|
+
private lastSeenSeq = 0;
|
|
57
|
+
|
|
58
|
+
private connectedKey: string | null = null;
|
|
59
|
+
|
|
60
|
+
private stopped = true;
|
|
61
|
+
|
|
62
|
+
private refreshing = false;
|
|
63
|
+
|
|
64
|
+
private initialUser: BlarIdentity | null;
|
|
65
|
+
|
|
66
|
+
private readonly log: (message: string) => void;
|
|
67
|
+
|
|
68
|
+
private readonly onConnecting: (sessionId: number) => void;
|
|
69
|
+
|
|
70
|
+
private booted: Promise<void> = Promise.resolve();
|
|
71
|
+
|
|
72
|
+
constructor(options: BackendTransportOptions) {
|
|
73
|
+
this.initialUser = options.user;
|
|
74
|
+
this.log = options.log ?? (() => undefined);
|
|
75
|
+
this.api = new ApiClient({ apiBaseUrl: options.apiBaseUrl, publishableKey: options.publishableKey });
|
|
76
|
+
const storage = options.storage ?? createDefaultStorage();
|
|
77
|
+
this.log(`storage ${storage instanceof MemoryStorage ? 'memory' : 'async-storage'}`);
|
|
78
|
+
this.session = new BackendSession(this.api, storage);
|
|
79
|
+
setReportPublishableKey(options.publishableKey);
|
|
80
|
+
setReportIdentityProvider(() => ({
|
|
81
|
+
sourceId: this.session.getState().sourceId,
|
|
82
|
+
sessionId: this.session.getState().sessionId,
|
|
83
|
+
userExternalId: this.getIdentity()?.externalId ?? null,
|
|
84
|
+
}));
|
|
85
|
+
this.runMemory = new StoredRunMemory(storage, `blar_agent_run_${options.publishableKey.slice(-8)}`, () => this.session.getState().sessionId);
|
|
86
|
+
this.onConnecting = options.onConnecting ?? (() => undefined);
|
|
87
|
+
watchSessionChanges((listener) => this.session.subscribe(listener), () => options.onSessionChanged?.());
|
|
88
|
+
this.presence = new PresenceManager(
|
|
89
|
+
this.api,
|
|
90
|
+
() => void this.refreshSession(),
|
|
91
|
+
(message) => this.log(message),
|
|
92
|
+
);
|
|
93
|
+
this.socket = new SessionSocket({
|
|
94
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
95
|
+
publishableKey: options.publishableKey,
|
|
96
|
+
tabId: this.tabId,
|
|
97
|
+
getSourceId: () => this.api.getSourceId(),
|
|
98
|
+
getProof: () => this.api.getIdentityProof(),
|
|
99
|
+
refreshProof: () => this.api.refreshIdentityProof(),
|
|
100
|
+
getLastSeenSeq: () => this.lastSeenSeq,
|
|
101
|
+
onMessage: (raw, frame) => this.receive(raw, frame),
|
|
102
|
+
onState: (state) => {
|
|
103
|
+
this.log(`socket ${state}`);
|
|
104
|
+
options.onSocketState?.(state);
|
|
105
|
+
},
|
|
106
|
+
onIdentityRejected: () => void this.session.logout().then(() => this.connect()),
|
|
107
|
+
watchForeground,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
onMessage(listener: (raw: string) => void): void {
|
|
112
|
+
this.agentListener = listener;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getRunMemory(): RunMemory {
|
|
116
|
+
return this.runMemory;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
onFrame(listener: FrameListener): () => void {
|
|
120
|
+
this.frameListeners.add(listener);
|
|
121
|
+
return () => this.frameListeners.delete(listener);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
onIdentityChange(listener: () => void): () => void {
|
|
125
|
+
this.identityListeners.add(listener);
|
|
126
|
+
return () => this.identityListeners.delete(listener);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
start(): void {
|
|
130
|
+
this.stopped = false;
|
|
131
|
+
this.health.start();
|
|
132
|
+
this.booted = this.boot();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
stop(): void {
|
|
136
|
+
this.stopped = true;
|
|
137
|
+
this.presence.stop();
|
|
138
|
+
this.health.stop();
|
|
139
|
+
this.socket.disconnect();
|
|
140
|
+
this.connectedKey = null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async setUser(identity: BlarIdentity): Promise<void> {
|
|
144
|
+
await this.booted;
|
|
145
|
+
await this.session.setUser(identity);
|
|
146
|
+
await this.connect();
|
|
147
|
+
this.identityListeners.forEach((listener) => listener());
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async logout(): Promise<void> {
|
|
151
|
+
await this.booted;
|
|
152
|
+
await this.session.logout();
|
|
153
|
+
await this.connect();
|
|
154
|
+
this.identityListeners.forEach((listener) => listener());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
isConnected(): boolean {
|
|
158
|
+
return this.socket.isOpen();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async refreshSession(): Promise<void> {
|
|
162
|
+
if (this.stopped || this.refreshing) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.refreshing = true;
|
|
166
|
+
try {
|
|
167
|
+
const previous = this.session.getState().sessionId;
|
|
168
|
+
const state = await this.session.refresh();
|
|
169
|
+
this.log(`refreshed session=${state.sessionId} (was ${previous})`);
|
|
170
|
+
if (state.sessionId !== null && state.sessionId !== previous) {
|
|
171
|
+
await this.connect();
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
this.log(`refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
175
|
+
reportSdkError('BackendTransport.refreshSession', error);
|
|
176
|
+
} finally {
|
|
177
|
+
this.refreshing = false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async switchSession(sessionId: number): Promise<void> {
|
|
182
|
+
this.session.adoptSession(sessionId);
|
|
183
|
+
await this.connect();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
leaveSession(): void {
|
|
187
|
+
this.session.leaveSession();
|
|
188
|
+
this.socket.disconnect();
|
|
189
|
+
this.connectedKey = null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async openNudge(): Promise<number | null> {
|
|
193
|
+
const sessionId = await this.api.openNudge(this.tabId);
|
|
194
|
+
this.log(`nudge open -> session=${sessionId}`);
|
|
195
|
+
return sessionId;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async dismissNudge(milestoneMessageId: number): Promise<void> {
|
|
199
|
+
await this.api.dismissNudge(milestoneMessageId);
|
|
200
|
+
this.log(`nudge dismissed milestone=${milestoneMessageId}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async snoozeNudge(milestoneMessageId: number): Promise<void> {
|
|
204
|
+
await this.api.snoozeNudge(milestoneMessageId);
|
|
205
|
+
this.log(`nudge snoozed milestone=${milestoneMessageId}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
getState(): SessionState {
|
|
209
|
+
return this.session.getState();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
getTabId(): string {
|
|
213
|
+
return this.tabId;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
getIdentity(): BlarIdentity | null {
|
|
217
|
+
return this.api.getIdentity();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async readWidgetProof(): Promise<WidgetProof | null> {
|
|
221
|
+
const proof = await this.api.refreshIdentityProof();
|
|
222
|
+
if (proof === null) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return proof.header === 'X-Identity-Token' ? { identityToken: proof.value } : { identitySignature: proof.value };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
noteInteraction(): void {
|
|
229
|
+
this.presence.noteInteraction();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
send(frame: OutboundFrame): void {
|
|
233
|
+
if (frame.type === 'pong') {
|
|
234
|
+
this.socket.send(JSON.stringify({ type: 'pong', client: this.health.take() }));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (frame.type === 'command_ack') {
|
|
238
|
+
this.socket.send(JSON.stringify(frame));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (frame.type === 'agent_cancel') {
|
|
242
|
+
this.log(`-> agent_cancel ${frame.reason}`);
|
|
243
|
+
this.socket.send(JSON.stringify(frame));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
this.log(`-> ${frame.type === 'sdk_response' ? frame.response_type : frame.type} ${frame.command_id}`);
|
|
247
|
+
if (frame.type === 'sdk_response' && frame.response_type === 'agent_command_response') {
|
|
248
|
+
this.log(` ${JSON.stringify(frame.data)}`);
|
|
249
|
+
}
|
|
250
|
+
if (frame.type === 'sdk_response' && frame.response_type === 'dom_state_response') {
|
|
251
|
+
this.log(`\n${frame.data.html_snapshot}`);
|
|
252
|
+
}
|
|
253
|
+
this.socket.sendOrCache(frame.command_id, JSON.stringify({ ...frame, document_id: this.tabId }));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private async boot(): Promise<void> {
|
|
257
|
+
try {
|
|
258
|
+
await Promise.all([this.session.boot(this.initialUser), this.runMemory.load()]);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
this.log(`boot failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
261
|
+
reportSdkError('BackendTransport.boot', error);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (this.stopped) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const state = this.session.getState();
|
|
268
|
+
this.log(`booted source=${state.sourceId} session=${state.sessionId} identified=${state.identified}`);
|
|
269
|
+
this.presence.start();
|
|
270
|
+
await this.connect().catch((error: unknown) => {
|
|
271
|
+
this.log(`connect failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
272
|
+
reportSdkError('BackendTransport.connect', error);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private async connect(): Promise<void> {
|
|
277
|
+
if (this.stopped) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const sessionId = await this.session.ensureSessionId();
|
|
281
|
+
if (this.stopped) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const sourceId = this.api.getSourceId();
|
|
285
|
+
if (sessionId === null || sourceId === null) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const key = `${sessionId}:${sourceId}`;
|
|
289
|
+
if (key === this.connectedKey && this.socket.isOpen()) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
this.connectedKey = key;
|
|
293
|
+
this.lastSeenSeq = 0;
|
|
294
|
+
this.log(`connecting session=${sessionId} source=${sourceId}`);
|
|
295
|
+
this.onConnecting(sessionId);
|
|
296
|
+
this.socket.connect(sessionId);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private receive(raw: string, frame: Record<string, unknown> | null): void {
|
|
300
|
+
if (frame === null) {
|
|
301
|
+
this.agentListener?.(raw);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (frame.type === 'ping') {
|
|
305
|
+
this.send({ type: 'pong' });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
this.trackSequence(frame);
|
|
309
|
+
if (typeof frame.event === 'string') {
|
|
310
|
+
this.log(`<- ${frame.event} ${this.describeCommand(frame)}`);
|
|
311
|
+
this.agentListener?.(raw);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
this.frameListeners.forEach((listener) => listener(frame));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private describeCommand(frame: Record<string, unknown>): string {
|
|
318
|
+
const data = isRecord(frame.data) ? frame.data : {};
|
|
319
|
+
const action = isRecord(data.action) ? JSON.stringify(data.action) : '';
|
|
320
|
+
const target = isRecord(data.target) && typeof data.target.agent_id === 'string' ? data.target.agent_id : '';
|
|
321
|
+
const risk = typeof data.risk_level === 'string' ? `risk=${data.risk_level}` : '';
|
|
322
|
+
return `${action} ${target} ${risk}`.trim();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private trackSequence(frame: Record<string, unknown>): void {
|
|
326
|
+
const message = isRecord(frame.message) ? frame.message : null;
|
|
327
|
+
if (frame.type === 'message' && message !== null && typeof message.seq === 'number') {
|
|
328
|
+
this.lastSeenSeq = Math.max(this.lastSeenSeq, message.seq);
|
|
329
|
+
}
|
|
330
|
+
if (frame.type === 'history' && Array.isArray(frame.messages)) {
|
|
331
|
+
for (const entry of frame.messages) {
|
|
332
|
+
if (isRecord(entry) && typeof entry.seq === 'number') {
|
|
333
|
+
this.lastSeenSeq = Math.max(this.lastSeenSeq, entry.seq);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface BackoffPolicy {
|
|
2
|
+
baseMs: number;
|
|
3
|
+
decay: number;
|
|
4
|
+
maxMs: number;
|
|
5
|
+
maxAttempts: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const SOCKET_BACKOFF: BackoffPolicy = { baseMs: 1000, decay: 1.5, maxMs: 30_000, maxAttempts: 10 };
|
|
9
|
+
|
|
10
|
+
export function backoffDelay(policy: BackoffPolicy, attempt: number): number {
|
|
11
|
+
return Math.min(policy.baseMs * Math.pow(policy.decay, attempt), policy.maxMs);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isExhausted(policy: BackoffPolicy, attempt: number): boolean {
|
|
15
|
+
return attempt >= policy.maxAttempts;
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
2
|
+
|
|
3
|
+
export function decodeBase64Url(input: string): string | null {
|
|
4
|
+
const normalized = input.replace(/-/g, '+').replace(/_/g, '/').replace(/=+$/, '');
|
|
5
|
+
let buffer = 0;
|
|
6
|
+
let bits = 0;
|
|
7
|
+
const bytes: number[] = [];
|
|
8
|
+
for (const char of normalized) {
|
|
9
|
+
const value = ALPHABET.indexOf(char);
|
|
10
|
+
if (value < 0) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
buffer = (buffer << 6) | value;
|
|
14
|
+
bits += 6;
|
|
15
|
+
if (bits >= 8) {
|
|
16
|
+
bits -= 8;
|
|
17
|
+
bytes.push((buffer >> bits) & 0xff);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return decodeURIComponent(bytes.map((byte) => `%${byte.toString(16).padStart(2, '0')}`).join(''));
|
|
22
|
+
} catch {
|
|
23
|
+
return String.fromCharCode(...bytes);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type ApiErrorKind = 'http' | 'network' | 'timeout';
|
|
2
|
+
|
|
3
|
+
export class ApiRequestError extends Error {
|
|
4
|
+
readonly kind: ApiErrorKind;
|
|
5
|
+
|
|
6
|
+
readonly status: number;
|
|
7
|
+
|
|
8
|
+
readonly code: string | null;
|
|
9
|
+
|
|
10
|
+
readonly endpoint: string;
|
|
11
|
+
|
|
12
|
+
constructor(options: { kind: ApiErrorKind; endpoint: string; status?: number; code?: string | null; message: string }) {
|
|
13
|
+
super(options.message);
|
|
14
|
+
this.name = 'ApiRequestError';
|
|
15
|
+
this.kind = options.kind;
|
|
16
|
+
this.endpoint = options.endpoint;
|
|
17
|
+
this.status = options.status ?? 0;
|
|
18
|
+
this.code = options.code ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static http(endpoint: string, status: number, statusText: string, code: string | null): ApiRequestError {
|
|
22
|
+
return new ApiRequestError({ kind: 'http', endpoint, status, code, message: `${endpoint} responded ${status} ${statusText}` });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static transport(endpoint: string, kind: 'network' | 'timeout'): ApiRequestError {
|
|
26
|
+
return new ApiRequestError({ kind, endpoint, message: `${endpoint} failed: ${kind}` });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isIdentityRejection(error: unknown): error is ApiRequestError {
|
|
31
|
+
return error instanceof ApiRequestError && error.kind === 'http' && (error.status === 401 || error.status === 403);
|
|
32
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AppState } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export function watchForeground(onActive: () => void): () => void {
|
|
4
|
+
const subscription = AppState.addEventListener('change', (state) => {
|
|
5
|
+
if (state === 'active') {
|
|
6
|
+
onActive();
|
|
7
|
+
}
|
|
8
|
+
});
|
|
9
|
+
return () => subscription.remove();
|
|
10
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { AppState, type AppStateStatus, type NativeEventSubscription } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { SDK_VERSION } from '../version.ts';
|
|
4
|
+
|
|
5
|
+
export interface ClientHealth {
|
|
6
|
+
visibility: 'visible' | 'hidden';
|
|
7
|
+
hidden_ms: number;
|
|
8
|
+
lag_ms: number;
|
|
9
|
+
sdk_version: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const PROBE_INTERVAL_MS = 1000;
|
|
13
|
+
|
|
14
|
+
export class ClientHealthProbe {
|
|
15
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
16
|
+
|
|
17
|
+
private subscription: NativeEventSubscription | null = null;
|
|
18
|
+
|
|
19
|
+
private expectedTickAt = 0;
|
|
20
|
+
|
|
21
|
+
private worstLagMs = 0;
|
|
22
|
+
|
|
23
|
+
private hiddenSince: number | null = null;
|
|
24
|
+
|
|
25
|
+
start(): void {
|
|
26
|
+
if (this.timer !== null) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
this.hiddenSince = AppState.currentState === 'active' ? null : Date.now();
|
|
30
|
+
this.subscription = AppState.addEventListener('change', (state) => this.onStateChange(state));
|
|
31
|
+
this.expectedTickAt = Date.now() + PROBE_INTERVAL_MS;
|
|
32
|
+
this.timer = setInterval(() => {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
this.worstLagMs = Math.max(this.worstLagMs, now - this.expectedTickAt);
|
|
35
|
+
this.expectedTickAt = now + PROBE_INTERVAL_MS;
|
|
36
|
+
}, PROBE_INTERVAL_MS);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
stop(): void {
|
|
40
|
+
if (this.timer !== null) {
|
|
41
|
+
clearInterval(this.timer);
|
|
42
|
+
this.timer = null;
|
|
43
|
+
}
|
|
44
|
+
this.subscription?.remove();
|
|
45
|
+
this.subscription = null;
|
|
46
|
+
this.worstLagMs = 0;
|
|
47
|
+
this.hiddenSince = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
take(): ClientHealth {
|
|
51
|
+
const lag = this.worstLagMs;
|
|
52
|
+
this.worstLagMs = 0;
|
|
53
|
+
return {
|
|
54
|
+
visibility: this.hiddenSince === null ? 'visible' : 'hidden',
|
|
55
|
+
hidden_ms: this.hiddenSince === null ? 0 : Date.now() - this.hiddenSince,
|
|
56
|
+
lag_ms: Math.max(0, Math.round(lag)),
|
|
57
|
+
sdk_version: SDK_VERSION,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private onStateChange(state: AppStateStatus): void {
|
|
62
|
+
this.hiddenSince = state === 'active' ? null : (this.hiddenSince ?? Date.now());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { isRecord } from '../fiber/narrow.ts';
|
|
2
|
+
import { decodeBase64Url } from './base64.ts';
|
|
3
|
+
import type { BlarIdentity, IdentityProof, IdentityRefusalReason } from './identity.ts';
|
|
4
|
+
|
|
5
|
+
const REFRESH_MARGIN_MS = 60_000;
|
|
6
|
+
|
|
7
|
+
type MintFailure = 'provider_failed' | 'subject_mismatch';
|
|
8
|
+
|
|
9
|
+
function tokenProof(value: string): IdentityProof {
|
|
10
|
+
if (typeof value !== 'string' || value !== value.trim() || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value)) {
|
|
11
|
+
throw new Error('invalid identity token');
|
|
12
|
+
}
|
|
13
|
+
return { header: 'X-Identity-Token', value, protocol: `blar-identity-token.${value}` };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function signatureProof(value: string): IdentityProof {
|
|
17
|
+
if (typeof value !== 'string' || value !== value.trim() || !/^[A-Fa-f0-9]+$/.test(value)) {
|
|
18
|
+
throw new Error('invalid identity signature');
|
|
19
|
+
}
|
|
20
|
+
return { header: 'X-Identity-Signature', value, protocol: `blar-identity-signature.${value}` };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function readJwtExpiry(token: string): number | null {
|
|
24
|
+
const payload = token.split('.')[1];
|
|
25
|
+
if (payload === undefined) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const decoded = decodeBase64Url(payload);
|
|
30
|
+
const claims: unknown = decoded === null ? null : JSON.parse(decoded);
|
|
31
|
+
return isRecord(claims) && typeof claims.exp === 'number' ? claims.exp * 1000 : null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class IdentityProofStore {
|
|
38
|
+
private identity: BlarIdentity | null = null;
|
|
39
|
+
|
|
40
|
+
private cached: IdentityProof | null = null;
|
|
41
|
+
|
|
42
|
+
private expiresAt: number | null = null;
|
|
43
|
+
|
|
44
|
+
private inFlight: Promise<IdentityProof | null> | null = null;
|
|
45
|
+
|
|
46
|
+
private mintFailure: MintFailure | null = null;
|
|
47
|
+
|
|
48
|
+
constructor(private readonly now: () => number = () => Date.now()) {}
|
|
49
|
+
|
|
50
|
+
setIdentity(identity: BlarIdentity | null): void {
|
|
51
|
+
this.identity = identity;
|
|
52
|
+
this.cached = null;
|
|
53
|
+
this.expiresAt = null;
|
|
54
|
+
this.inFlight = null;
|
|
55
|
+
this.mintFailure = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getIdentity(): BlarIdentity | null {
|
|
59
|
+
return this.identity;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
hasIdentity(): boolean {
|
|
63
|
+
return this.identity !== null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
canRefresh(): boolean {
|
|
67
|
+
const identity = this.identity;
|
|
68
|
+
return identity !== null && Boolean(identity.getProof ?? identity.getIdentityToken ?? identity.getIdentitySignature);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
refusalReason(): IdentityRefusalReason {
|
|
72
|
+
if (this.mintFailure !== null) {
|
|
73
|
+
return this.mintFailure;
|
|
74
|
+
}
|
|
75
|
+
if (this.cached === null && this.canRefresh()) {
|
|
76
|
+
return 'provider_failed';
|
|
77
|
+
}
|
|
78
|
+
if (!this.canRefresh() && this.expiresAt !== null && this.expiresAt <= this.now()) {
|
|
79
|
+
return 'static_token_expired';
|
|
80
|
+
}
|
|
81
|
+
return 'identify_refused';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get(options?: { refresh?: boolean }): Promise<IdentityProof | null> {
|
|
85
|
+
if (this.identity === null) {
|
|
86
|
+
return Promise.resolve(null);
|
|
87
|
+
}
|
|
88
|
+
const expiring = this.expiresAt !== null && this.expiresAt - this.now() <= REFRESH_MARGIN_MS;
|
|
89
|
+
if (this.cached !== null && options?.refresh !== true && !expiring) {
|
|
90
|
+
return Promise.resolve(this.cached);
|
|
91
|
+
}
|
|
92
|
+
if (this.cached !== null && !this.canRefresh()) {
|
|
93
|
+
return Promise.resolve(this.cached);
|
|
94
|
+
}
|
|
95
|
+
if (this.inFlight === null) {
|
|
96
|
+
this.inFlight = this.mint().finally(() => {
|
|
97
|
+
this.inFlight = null;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return this.inFlight;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async mint(): Promise<IdentityProof | null> {
|
|
104
|
+
const identity = this.identity;
|
|
105
|
+
if (identity === null) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
this.mintFailure = null;
|
|
109
|
+
try {
|
|
110
|
+
if (identity.getProof !== undefined) {
|
|
111
|
+
return this.fromProviderProof(identity, await identity.getProof());
|
|
112
|
+
}
|
|
113
|
+
const token = identity.getIdentityToken !== undefined ? await identity.getIdentityToken() : identity.identityToken;
|
|
114
|
+
if (token !== undefined && token !== '') {
|
|
115
|
+
return this.storeToken(token);
|
|
116
|
+
}
|
|
117
|
+
const signature = identity.getIdentitySignature !== undefined ? await identity.getIdentitySignature() : identity.identitySignature;
|
|
118
|
+
return this.storeSignature(signature);
|
|
119
|
+
} catch {
|
|
120
|
+
this.mintFailure = this.mintFailure ?? 'provider_failed';
|
|
121
|
+
this.cached = null;
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private fromProviderProof(
|
|
127
|
+
identity: BlarIdentity,
|
|
128
|
+
proof: { externalId?: string; identityToken?: string; identitySignature?: string },
|
|
129
|
+
): IdentityProof | null {
|
|
130
|
+
if (proof.externalId !== undefined && proof.externalId !== identity.externalId) {
|
|
131
|
+
this.mintFailure = 'subject_mismatch';
|
|
132
|
+
throw new Error('identity proof subject mismatch');
|
|
133
|
+
}
|
|
134
|
+
if (proof.identityToken !== undefined && proof.identityToken !== '') {
|
|
135
|
+
return this.storeToken(proof.identityToken);
|
|
136
|
+
}
|
|
137
|
+
return this.storeSignature(proof.identitySignature);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private storeToken(token: string): IdentityProof {
|
|
141
|
+
this.cached = tokenProof(token);
|
|
142
|
+
this.expiresAt = readJwtExpiry(token);
|
|
143
|
+
return this.cached;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private storeSignature(signature: string | undefined): IdentityProof | null {
|
|
147
|
+
this.cached = signature !== undefined && signature !== '' ? signatureProof(signature) : null;
|
|
148
|
+
this.expiresAt = null;
|
|
149
|
+
return this.cached;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface BlarIdentityProofInput {
|
|
2
|
+
externalId?: string;
|
|
3
|
+
identityToken?: string;
|
|
4
|
+
identitySignature?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type CustomFields = Record<string, string | number | boolean>;
|
|
8
|
+
|
|
9
|
+
export interface BlarIdentity {
|
|
10
|
+
externalId: string;
|
|
11
|
+
email?: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
phone?: string;
|
|
14
|
+
contactCompany?: string;
|
|
15
|
+
contactCompanyExternalId?: string;
|
|
16
|
+
country?: string;
|
|
17
|
+
identityToken?: string;
|
|
18
|
+
identitySignature?: string;
|
|
19
|
+
getProof?: () => Promise<BlarIdentityProofInput>;
|
|
20
|
+
getIdentityToken?: () => Promise<string>;
|
|
21
|
+
getIdentitySignature?: () => Promise<string>;
|
|
22
|
+
customFields?: CustomFields;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface IdentityProof {
|
|
26
|
+
header: 'X-Identity-Token' | 'X-Identity-Signature';
|
|
27
|
+
value: string;
|
|
28
|
+
protocol: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type IdentityRefusalReason = 'provider_failed' | 'static_token_expired' | 'identify_refused' | 'subject_mismatch';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TextDomResponseData } from '../agent/frames.ts';
|
|
2
|
+
import type { Size } from '../host/rect.ts';
|
|
3
|
+
import type { AppInfo } from './app-info.ts';
|
|
4
|
+
|
|
5
|
+
export interface MessageMetadata extends AppInfo {
|
|
6
|
+
url: string;
|
|
7
|
+
tab_id: string;
|
|
8
|
+
debug_context: { viewport: Size };
|
|
9
|
+
text_dom?: TextDomResponseData;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MessageMetadataInput {
|
|
13
|
+
screen: string;
|
|
14
|
+
tabId: string;
|
|
15
|
+
viewport: Size;
|
|
16
|
+
appInfo: AppInfo;
|
|
17
|
+
textDom: TextDomResponseData | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildMessageMetadata(input: MessageMetadataInput): MessageMetadata {
|
|
21
|
+
return {
|
|
22
|
+
...input.appInfo,
|
|
23
|
+
url: input.screen,
|
|
24
|
+
tab_id: input.tabId,
|
|
25
|
+
debug_context: { viewport: input.viewport },
|
|
26
|
+
...(input.textDom === null ? {} : { text_dom: input.textDom }),
|
|
27
|
+
};
|
|
28
|
+
}
|