@userflow-tech/react-native 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -14
- package/app.plugin.js +3 -0
- package/babel-plugin/index.d.ts +43 -0
- package/babel-plugin/index.d.ts.map +1 -0
- package/babel-plugin/index.js +854 -0
- package/babel-plugin/index.js.map +1 -0
- package/docs/deep-linking.md +2 -2
- package/docs/troubleshooting.md +2 -2
- package/expo-plugin/withUserflow.d.ts +17 -0
- package/expo-plugin/withUserflow.d.ts.map +1 -0
- package/expo-plugin/withUserflow.js +99 -0
- package/expo-plugin/withUserflow.js.map +1 -0
- package/lib/commonjs/anchors/AutoAnchorRegistry.js +1 -1
- package/lib/commonjs/anchors/autoAnchor.js +1 -1
- package/lib/commonjs/anchors/hintsCodec.js +1 -1
- package/lib/commonjs/pairing/diagnostics.js +2 -2
- package/lib/commonjs/pairing/diagnostics.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/anchors/AutoAnchorRegistry.js +1 -1
- package/lib/module/anchors/autoAnchor.js +1 -1
- package/lib/module/anchors/hintsCodec.js +1 -1
- package/lib/module/pairing/diagnostics.js +2 -2
- package/lib/module/pairing/diagnostics.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/commonjs/src/anchors/AutoAnchorRegistry.d.ts +1 -1
- package/lib/typescript/commonjs/src/anchors/AutoAnchorRegistry.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/anchors/autoAnchor.d.ts +1 -1
- package/lib/typescript/commonjs/src/anchors/hintsCodec.d.ts +1 -1
- package/lib/typescript/commonjs/src/version.d.ts +1 -1
- package/lib/typescript/module/src/anchors/AutoAnchorRegistry.d.ts +1 -1
- package/lib/typescript/module/src/anchors/AutoAnchorRegistry.d.ts.map +1 -1
- package/lib/typescript/module/src/anchors/autoAnchor.d.ts +1 -1
- package/lib/typescript/module/src/anchors/hintsCodec.d.ts +1 -1
- package/lib/typescript/module/src/version.d.ts +1 -1
- package/package.json +15 -7
- package/src/anchors/AutoAnchorRegistry.ts +1 -1
- package/src/anchors/autoAnchor.ts +1 -1
- package/src/anchors/hintsCodec.ts +1 -1
- package/src/pairing/diagnostics.ts +2 -2
- package/src/version.ts +1 -1
- package/vendor/core/LICENSE +76 -0
- package/vendor/core/README.md +9 -0
- package/vendor/core/dist/attributes.d.ts +16 -0
- package/vendor/core/dist/attributes.d.ts.map +1 -0
- package/vendor/core/dist/attributes.js +55 -0
- package/vendor/core/dist/attributes.js.map +1 -0
- package/vendor/core/dist/constants.d.ts +21 -0
- package/vendor/core/dist/constants.d.ts.map +1 -0
- package/vendor/core/dist/constants.js +21 -0
- package/vendor/core/dist/constants.js.map +1 -0
- package/vendor/core/dist/engine/ClientConditionEngine.d.ts +68 -0
- package/vendor/core/dist/engine/ClientConditionEngine.d.ts.map +1 -0
- package/vendor/core/dist/engine/ClientConditionEngine.js +197 -0
- package/vendor/core/dist/engine/ClientConditionEngine.js.map +1 -0
- package/vendor/core/dist/engine/FlowSessionEngine.d.ts +92 -0
- package/vendor/core/dist/engine/FlowSessionEngine.d.ts.map +1 -0
- package/vendor/core/dist/engine/FlowSessionEngine.js +310 -0
- package/vendor/core/dist/engine/FlowSessionEngine.js.map +1 -0
- package/vendor/core/dist/engine/PairingEngine.d.ts +92 -0
- package/vendor/core/dist/engine/PairingEngine.d.ts.map +1 -0
- package/vendor/core/dist/engine/PairingEngine.js +302 -0
- package/vendor/core/dist/engine/PairingEngine.js.map +1 -0
- package/vendor/core/dist/engine/UserflowCore.d.ts +126 -0
- package/vendor/core/dist/engine/UserflowCore.d.ts.map +1 -0
- package/vendor/core/dist/engine/UserflowCore.js +266 -0
- package/vendor/core/dist/engine/UserflowCore.js.map +1 -0
- package/vendor/core/dist/engine/handleActions.d.ts +19 -0
- package/vendor/core/dist/engine/handleActions.d.ts.map +1 -0
- package/vendor/core/dist/engine/handleActions.js +71 -0
- package/vendor/core/dist/engine/handleActions.js.map +1 -0
- package/vendor/core/dist/engine/interpolateSessionData.d.ts +7 -0
- package/vendor/core/dist/engine/interpolateSessionData.d.ts.map +1 -0
- package/vendor/core/dist/engine/interpolateSessionData.js +29 -0
- package/vendor/core/dist/engine/interpolateSessionData.js.map +1 -0
- package/vendor/core/dist/errors.d.ts +9 -0
- package/vendor/core/dist/errors.d.ts.map +1 -0
- package/vendor/core/dist/errors.js +15 -0
- package/vendor/core/dist/errors.js.map +1 -0
- package/vendor/core/dist/eventAttributes.d.ts +7 -0
- package/vendor/core/dist/eventAttributes.d.ts.map +1 -0
- package/vendor/core/dist/eventAttributes.js +31 -0
- package/vendor/core/dist/eventAttributes.js.map +1 -0
- package/vendor/core/dist/index.d.ts +30 -0
- package/vendor/core/dist/index.d.ts.map +1 -0
- package/vendor/core/dist/index.js +21 -0
- package/vendor/core/dist/index.js.map +1 -0
- package/vendor/core/dist/transport/backoff.d.ts +11 -0
- package/vendor/core/dist/transport/backoff.d.ts.map +1 -0
- package/vendor/core/dist/transport/backoff.js +17 -0
- package/vendor/core/dist/transport/backoff.js.map +1 -0
- package/vendor/core/dist/transport/pairing.d.ts +121 -0
- package/vendor/core/dist/transport/pairing.d.ts.map +1 -0
- package/vendor/core/dist/transport/pairing.js +551 -0
- package/vendor/core/dist/transport/pairing.js.map +1 -0
- package/vendor/core/dist/transport/phoenix.d.ts +223 -0
- package/vendor/core/dist/transport/phoenix.d.ts.map +1 -0
- package/vendor/core/dist/transport/phoenix.js +757 -0
- package/vendor/core/dist/transport/phoenix.js.map +1 -0
- package/vendor/core/dist/transport/rateLimiter.d.ts +17 -0
- package/vendor/core/dist/transport/rateLimiter.d.ts.map +1 -0
- package/vendor/core/dist/transport/rateLimiter.js +30 -0
- package/vendor/core/dist/transport/rateLimiter.js.map +1 -0
- package/vendor/core/dist/transport/scheduler.d.ts +12 -0
- package/vendor/core/dist/transport/scheduler.d.ts.map +1 -0
- package/vendor/core/dist/transport/scheduler.js +11 -0
- package/vendor/core/dist/transport/scheduler.js.map +1 -0
- package/vendor/core/dist/types.d.ts +35 -0
- package/vendor/core/dist/types.d.ts.map +1 -0
- package/vendor/core/dist/types.js +2 -0
- package/vendor/core/dist/types.js.map +1 -0
- package/vendor/core/dist/version.d.ts +3 -0
- package/vendor/core/dist/version.d.ts.map +1 -0
- package/vendor/core/dist/version.js +3 -0
- package/vendor/core/dist/version.js.map +1 -0
- package/vendor/core/package.json +20 -0
- package/vendor/protocol/LICENSE +76 -0
- package/vendor/protocol/README.md +8 -0
- package/vendor/protocol/dist/autoData.d.ts +232 -0
- package/vendor/protocol/dist/autoData.d.ts.map +1 -0
- package/vendor/protocol/dist/autoData.js +78 -0
- package/vendor/protocol/dist/autoData.js.map +1 -0
- package/vendor/protocol/dist/events.d.ts +16 -0
- package/vendor/protocol/dist/events.d.ts.map +1 -0
- package/vendor/protocol/dist/events.js +8 -0
- package/vendor/protocol/dist/events.js.map +1 -0
- package/vendor/protocol/dist/flows.d.ts +298 -0
- package/vendor/protocol/dist/flows.d.ts.map +1 -0
- package/vendor/protocol/dist/flows.js +154 -0
- package/vendor/protocol/dist/flows.js.map +1 -0
- package/vendor/protocol/dist/index.d.ts +24 -0
- package/vendor/protocol/dist/index.d.ts.map +1 -0
- package/vendor/protocol/dist/index.js +16 -0
- package/vendor/protocol/dist/index.js.map +1 -0
- package/vendor/protocol/dist/messages.d.ts +166 -0
- package/vendor/protocol/dist/messages.d.ts.map +1 -0
- package/vendor/protocol/dist/messages.js +57 -0
- package/vendor/protocol/dist/messages.js.map +1 -0
- package/vendor/protocol/dist/pairing.d.ts +56 -0
- package/vendor/protocol/dist/pairing.d.ts.map +1 -0
- package/vendor/protocol/dist/pairing.js +146 -0
- package/vendor/protocol/dist/pairing.js.map +1 -0
- package/vendor/protocol/package.json +17 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { type PairingJoinPayload, type PairingRelayEvent, type StartFlowWithTokenRelayPayload } from '@userflow-tech/protocol';
|
|
2
|
+
import type { ConnectionState, WebSocketCtor } from './phoenix.js';
|
|
3
|
+
import { type Scheduler } from './scheduler.js';
|
|
4
|
+
export type PairingConnectionEvent = {
|
|
5
|
+
type: 'state';
|
|
6
|
+
state: ConnectionState;
|
|
7
|
+
} | {
|
|
8
|
+
type: 'joined';
|
|
9
|
+
pairingId: string;
|
|
10
|
+
deviceId: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'token_expired';
|
|
13
|
+
} | {
|
|
14
|
+
type: 'device_already_paired';
|
|
15
|
+
} | {
|
|
16
|
+
type: 'join_error';
|
|
17
|
+
reason: string;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'relay';
|
|
20
|
+
event: PairingRelayEvent;
|
|
21
|
+
payload: unknown;
|
|
22
|
+
} | {
|
|
23
|
+
type: 'startFlowWithToken';
|
|
24
|
+
payload: StartFlowWithTokenRelayPayload;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'presence_state';
|
|
27
|
+
payload: unknown;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'presence_diff';
|
|
30
|
+
payload: unknown;
|
|
31
|
+
};
|
|
32
|
+
export type PairingConnectionListener = (event: PairingConnectionEvent) => void;
|
|
33
|
+
export interface PairingConnectionOptions {
|
|
34
|
+
/** Override US-pinned pairing host (tests / local stacks). */
|
|
35
|
+
pairingEndpoint?: string;
|
|
36
|
+
scheduler?: Scheduler;
|
|
37
|
+
reconnectLadderMs?: readonly number[];
|
|
38
|
+
heartbeatIntervalMs?: number;
|
|
39
|
+
heartbeatReplyTimeoutMs?: number;
|
|
40
|
+
now?: () => number;
|
|
41
|
+
socketCtor?: WebSocketCtor;
|
|
42
|
+
}
|
|
43
|
+
/** Builds the Phoenix websocket URL for `/socket/pairing`. */
|
|
44
|
+
export declare function buildPairingWsUrl(pairingEndpoint: string, pairingToken: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Device-side Phoenix connection to `/socket/pairing`.
|
|
47
|
+
* Authenticated by pairing JWT; joins `pairing:<pairing_id>` with an ephemeral device_id.
|
|
48
|
+
*/
|
|
49
|
+
export declare class PairingConnection {
|
|
50
|
+
private ws;
|
|
51
|
+
private ref;
|
|
52
|
+
private pending;
|
|
53
|
+
private channelJoinRef;
|
|
54
|
+
private channelTopic;
|
|
55
|
+
private channelJoined;
|
|
56
|
+
private state;
|
|
57
|
+
private shouldBeConnected;
|
|
58
|
+
private pairingToken;
|
|
59
|
+
private claims;
|
|
60
|
+
private deviceId;
|
|
61
|
+
private joinMeta;
|
|
62
|
+
private establishPromise;
|
|
63
|
+
private pendingConnect;
|
|
64
|
+
private reconnectHandle;
|
|
65
|
+
private heartbeatHandle;
|
|
66
|
+
private heartbeatReplyHandle;
|
|
67
|
+
private pendingHeartbeatRef;
|
|
68
|
+
private readonly listeners;
|
|
69
|
+
private readonly scheduler;
|
|
70
|
+
private readonly backoff;
|
|
71
|
+
private readonly heartbeatIntervalMs;
|
|
72
|
+
private readonly heartbeatReplyTimeoutMs;
|
|
73
|
+
private readonly pairingEndpoint;
|
|
74
|
+
private readonly now;
|
|
75
|
+
private readonly socketCtor;
|
|
76
|
+
constructor(options?: PairingConnectionOptions);
|
|
77
|
+
getState(): ConnectionState;
|
|
78
|
+
getPairingId(): string | null;
|
|
79
|
+
getDeviceId(): string | null;
|
|
80
|
+
onEvent(listener: PairingConnectionListener): () => void;
|
|
81
|
+
/**
|
|
82
|
+
* Connect with a pairing JWT and join `pairing:<pairing_id>`.
|
|
83
|
+
* Replaces any existing pairing connection (warm re-scan).
|
|
84
|
+
*/
|
|
85
|
+
connect(pairingToken: string, join?: Partial<PairingJoinPayload> & {
|
|
86
|
+
device_id?: string;
|
|
87
|
+
}): Promise<void>;
|
|
88
|
+
/** Push a relay event to the builder (e.g. `startFlowWithTokenResult`). */
|
|
89
|
+
push(event: PairingRelayEvent, payload: unknown): Promise<unknown>;
|
|
90
|
+
disconnect(options?: {
|
|
91
|
+
emitState?: boolean;
|
|
92
|
+
}): void;
|
|
93
|
+
ensureAlive(): void;
|
|
94
|
+
private reconnectNow;
|
|
95
|
+
private isTokenExpired;
|
|
96
|
+
private handleTokenExpired;
|
|
97
|
+
private establish;
|
|
98
|
+
private doEstablish;
|
|
99
|
+
private attemptEstablish;
|
|
100
|
+
private openSocket;
|
|
101
|
+
private joinChannel;
|
|
102
|
+
private onSocketDown;
|
|
103
|
+
private scheduleReconnect;
|
|
104
|
+
private cancelReconnect;
|
|
105
|
+
private startHeartbeat;
|
|
106
|
+
private stopHeartbeat;
|
|
107
|
+
private scheduleNextHeartbeat;
|
|
108
|
+
private clearHeartbeatTimer;
|
|
109
|
+
private clearHeartbeatReplyTimer;
|
|
110
|
+
private sendHeartbeat;
|
|
111
|
+
private handleMessage;
|
|
112
|
+
private request;
|
|
113
|
+
private nextRef;
|
|
114
|
+
private resetChannel;
|
|
115
|
+
private detachAndClose;
|
|
116
|
+
private abortPendingConnect;
|
|
117
|
+
private rejectPending;
|
|
118
|
+
private setState;
|
|
119
|
+
private emit;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=pairing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/transport/pairing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { decodePairingJwt, isPairingJwtExpired, isPairingRelayEvent, } from '@userflow-tech/protocol';
|
|
2
|
+
import { DEFAULT_PAIRING_ENDPOINT } from '../constants.js';
|
|
3
|
+
import { UserflowAuthError } from '../errors.js';
|
|
4
|
+
import { createBackoff } from './backoff.js';
|
|
5
|
+
import { defaultScheduler } from './scheduler.js';
|
|
6
|
+
const TIMEOUT_MS = 15000;
|
|
7
|
+
const HEARTBEAT_INTERVAL_MS = 30000;
|
|
8
|
+
const HEARTBEAT_REPLY_TIMEOUT_MS = 10000;
|
|
9
|
+
const WS_OPEN = 1;
|
|
10
|
+
const PHOENIX_TOPIC = 'phoenix';
|
|
11
|
+
function uuid() {
|
|
12
|
+
const webcrypto = globalThis.crypto;
|
|
13
|
+
if (webcrypto?.randomUUID)
|
|
14
|
+
return webcrypto.randomUUID();
|
|
15
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
|
|
16
|
+
const rand = (Math.random() * 16) | 0;
|
|
17
|
+
const value = char === 'x' ? rand : (rand & 0x3) | 0x8;
|
|
18
|
+
return value.toString(16);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function resolveSocketCtor(ctor) {
|
|
22
|
+
const provided = ctor ?? globalThis.WebSocket;
|
|
23
|
+
if (!provided) {
|
|
24
|
+
throw new UserflowAuthError('No WebSocket implementation is available');
|
|
25
|
+
}
|
|
26
|
+
return provided;
|
|
27
|
+
}
|
|
28
|
+
function joinError(response) {
|
|
29
|
+
if (response && typeof response === 'object') {
|
|
30
|
+
const err = response;
|
|
31
|
+
if (typeof err.reason === 'string')
|
|
32
|
+
return err.reason;
|
|
33
|
+
if (err.code && err.message)
|
|
34
|
+
return `${err.code}: ${err.message}`;
|
|
35
|
+
if (err.message)
|
|
36
|
+
return err.message;
|
|
37
|
+
}
|
|
38
|
+
return 'channel join rejected';
|
|
39
|
+
}
|
|
40
|
+
/** Builds the Phoenix websocket URL for `/socket/pairing`. */
|
|
41
|
+
export function buildPairingWsUrl(pairingEndpoint, pairingToken) {
|
|
42
|
+
const host = pairingEndpoint.replace(/\/$/, '');
|
|
43
|
+
const query = `vsn=2.0.0&pairing_token=${encodeURIComponent(pairingToken)}`;
|
|
44
|
+
const path = `/socket/pairing/websocket?${query}`;
|
|
45
|
+
if (host.startsWith('http://'))
|
|
46
|
+
return `ws://${host.slice('http://'.length)}${path}`;
|
|
47
|
+
if (host.startsWith('https://'))
|
|
48
|
+
return `wss://${host.slice('https://'.length)}${path}`;
|
|
49
|
+
if (host.startsWith('ws://') || host.startsWith('wss://'))
|
|
50
|
+
return `${host}${path}`;
|
|
51
|
+
return `wss://${host}${path}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Device-side Phoenix connection to `/socket/pairing`.
|
|
55
|
+
* Authenticated by pairing JWT; joins `pairing:<pairing_id>` with an ephemeral device_id.
|
|
56
|
+
*/
|
|
57
|
+
export class PairingConnection {
|
|
58
|
+
constructor(options = {}) {
|
|
59
|
+
this.ws = null;
|
|
60
|
+
this.ref = 0;
|
|
61
|
+
this.pending = new Map();
|
|
62
|
+
this.channelJoinRef = null;
|
|
63
|
+
this.channelTopic = null;
|
|
64
|
+
this.channelJoined = false;
|
|
65
|
+
this.state = 'disconnected';
|
|
66
|
+
this.shouldBeConnected = false;
|
|
67
|
+
this.pairingToken = null;
|
|
68
|
+
this.claims = null;
|
|
69
|
+
this.deviceId = null;
|
|
70
|
+
this.joinMeta = {};
|
|
71
|
+
this.establishPromise = null;
|
|
72
|
+
this.pendingConnect = null;
|
|
73
|
+
this.reconnectHandle = null;
|
|
74
|
+
this.heartbeatHandle = null;
|
|
75
|
+
this.heartbeatReplyHandle = null;
|
|
76
|
+
this.pendingHeartbeatRef = null;
|
|
77
|
+
this.listeners = new Set();
|
|
78
|
+
this.pairingEndpoint = (options.pairingEndpoint ?? DEFAULT_PAIRING_ENDPOINT).replace(/\/$/, '');
|
|
79
|
+
this.scheduler = options.scheduler ?? defaultScheduler;
|
|
80
|
+
this.backoff = createBackoff(options.reconnectLadderMs);
|
|
81
|
+
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS;
|
|
82
|
+
this.heartbeatReplyTimeoutMs =
|
|
83
|
+
options.heartbeatReplyTimeoutMs ??
|
|
84
|
+
Math.min(HEARTBEAT_REPLY_TIMEOUT_MS, this.heartbeatIntervalMs);
|
|
85
|
+
this.now = options.now ?? Date.now;
|
|
86
|
+
this.socketCtor = options.socketCtor ?? undefined;
|
|
87
|
+
}
|
|
88
|
+
getState() {
|
|
89
|
+
return this.state;
|
|
90
|
+
}
|
|
91
|
+
getPairingId() {
|
|
92
|
+
return this.claims?.pairing_id ?? null;
|
|
93
|
+
}
|
|
94
|
+
getDeviceId() {
|
|
95
|
+
return this.deviceId;
|
|
96
|
+
}
|
|
97
|
+
onEvent(listener) {
|
|
98
|
+
this.listeners.add(listener);
|
|
99
|
+
return () => {
|
|
100
|
+
this.listeners.delete(listener);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Connect with a pairing JWT and join `pairing:<pairing_id>`.
|
|
105
|
+
* Replaces any existing pairing connection (warm re-scan).
|
|
106
|
+
*/
|
|
107
|
+
async connect(pairingToken, join = {}) {
|
|
108
|
+
const claims = decodePairingJwt(pairingToken);
|
|
109
|
+
if (!claims) {
|
|
110
|
+
throw new UserflowAuthError('Invalid pairing token');
|
|
111
|
+
}
|
|
112
|
+
if (isPairingJwtExpired(claims, this.now())) {
|
|
113
|
+
this.emit({ type: 'token_expired' });
|
|
114
|
+
throw new UserflowAuthError('Pairing token expired');
|
|
115
|
+
}
|
|
116
|
+
this.disconnect({ emitState: false });
|
|
117
|
+
this.pairingToken = pairingToken;
|
|
118
|
+
this.claims = claims;
|
|
119
|
+
this.deviceId = join.device_id ?? uuid();
|
|
120
|
+
this.joinMeta = {
|
|
121
|
+
...(join.model !== undefined ? { model: join.model } : {}),
|
|
122
|
+
...(join.os !== undefined ? { os: join.os } : {}),
|
|
123
|
+
...(join.app_version !== undefined ? { app_version: join.app_version } : {}),
|
|
124
|
+
};
|
|
125
|
+
this.shouldBeConnected = true;
|
|
126
|
+
this.setState('connecting');
|
|
127
|
+
try {
|
|
128
|
+
await this.establish();
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const reason = error instanceof Error ? error.message : 'join failed';
|
|
132
|
+
if (reason.includes('device_already_paired')) {
|
|
133
|
+
this.emit({ type: 'device_already_paired' });
|
|
134
|
+
}
|
|
135
|
+
else if (reason.includes('expired') || this.isTokenExpired()) {
|
|
136
|
+
this.emit({ type: 'token_expired' });
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
this.emit({ type: 'join_error', reason });
|
|
140
|
+
}
|
|
141
|
+
this.shouldBeConnected = false;
|
|
142
|
+
this.setState('disconnected');
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Push a relay event to the builder (e.g. `startFlowWithTokenResult`). */
|
|
147
|
+
push(event, payload) {
|
|
148
|
+
if (!this.channelTopic || !this.channelJoined) {
|
|
149
|
+
return Promise.reject(new UserflowAuthError('Not joined to a pairing channel'));
|
|
150
|
+
}
|
|
151
|
+
return this.request(this.channelTopic, (joinRef, ref) => [
|
|
152
|
+
joinRef,
|
|
153
|
+
ref,
|
|
154
|
+
this.channelTopic,
|
|
155
|
+
event,
|
|
156
|
+
payload,
|
|
157
|
+
]);
|
|
158
|
+
}
|
|
159
|
+
disconnect(options = {}) {
|
|
160
|
+
const emitState = options.emitState !== false;
|
|
161
|
+
this.shouldBeConnected = false;
|
|
162
|
+
this.pairingToken = null;
|
|
163
|
+
this.claims = null;
|
|
164
|
+
this.deviceId = null;
|
|
165
|
+
this.joinMeta = {};
|
|
166
|
+
this.cancelReconnect();
|
|
167
|
+
this.stopHeartbeat();
|
|
168
|
+
this.abortPendingConnect(new UserflowAuthError('Connection closed'));
|
|
169
|
+
this.rejectPending(new UserflowAuthError('Connection closed'));
|
|
170
|
+
this.detachAndClose(this.ws);
|
|
171
|
+
this.resetChannel();
|
|
172
|
+
if (emitState)
|
|
173
|
+
this.setState('disconnected');
|
|
174
|
+
else
|
|
175
|
+
this.state = 'disconnected';
|
|
176
|
+
}
|
|
177
|
+
ensureAlive() {
|
|
178
|
+
if (!this.shouldBeConnected)
|
|
179
|
+
return;
|
|
180
|
+
if (this.isTokenExpired()) {
|
|
181
|
+
this.handleTokenExpired();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (this.state === 'connected' && this.ws) {
|
|
185
|
+
if (this.pendingHeartbeatRef !== null)
|
|
186
|
+
return;
|
|
187
|
+
this.clearHeartbeatTimer();
|
|
188
|
+
this.sendHeartbeat();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this.reconnectNow();
|
|
192
|
+
}
|
|
193
|
+
reconnectNow() {
|
|
194
|
+
if (!this.shouldBeConnected)
|
|
195
|
+
return;
|
|
196
|
+
if (this.state === 'connecting' || this.state === 'connected')
|
|
197
|
+
return;
|
|
198
|
+
if (this.isTokenExpired()) {
|
|
199
|
+
this.handleTokenExpired();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
this.cancelReconnect();
|
|
203
|
+
this.backoff.reset();
|
|
204
|
+
this.setState('reconnecting');
|
|
205
|
+
this.attemptEstablish();
|
|
206
|
+
}
|
|
207
|
+
isTokenExpired() {
|
|
208
|
+
return this.claims != null && isPairingJwtExpired(this.claims, this.now());
|
|
209
|
+
}
|
|
210
|
+
handleTokenExpired() {
|
|
211
|
+
this.shouldBeConnected = false;
|
|
212
|
+
this.cancelReconnect();
|
|
213
|
+
this.stopHeartbeat();
|
|
214
|
+
this.detachAndClose(this.ws);
|
|
215
|
+
this.resetChannel();
|
|
216
|
+
this.setState('disconnected');
|
|
217
|
+
this.emit({ type: 'token_expired' });
|
|
218
|
+
}
|
|
219
|
+
establish() {
|
|
220
|
+
if (this.establishPromise)
|
|
221
|
+
return this.establishPromise;
|
|
222
|
+
this.establishPromise = this.doEstablish().finally(() => {
|
|
223
|
+
this.establishPromise = null;
|
|
224
|
+
});
|
|
225
|
+
return this.establishPromise;
|
|
226
|
+
}
|
|
227
|
+
async doEstablish() {
|
|
228
|
+
if (this.isTokenExpired()) {
|
|
229
|
+
throw new UserflowAuthError('Pairing token expired');
|
|
230
|
+
}
|
|
231
|
+
this.setState(this.state === 'reconnecting' ? 'reconnecting' : 'connecting');
|
|
232
|
+
await this.openSocket();
|
|
233
|
+
await this.joinChannel();
|
|
234
|
+
this.setState('connected');
|
|
235
|
+
this.backoff.reset();
|
|
236
|
+
this.startHeartbeat();
|
|
237
|
+
if (this.claims && this.deviceId) {
|
|
238
|
+
this.emit({ type: 'joined', pairingId: this.claims.pairing_id, deviceId: this.deviceId });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
attemptEstablish() {
|
|
242
|
+
void this.establish().catch(() => {
|
|
243
|
+
if (!this.shouldBeConnected)
|
|
244
|
+
return;
|
|
245
|
+
if (this.isTokenExpired()) {
|
|
246
|
+
this.handleTokenExpired();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
this.setState('reconnecting');
|
|
250
|
+
this.scheduleReconnect();
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
openSocket() {
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
if (!this.pairingToken) {
|
|
256
|
+
reject(new UserflowAuthError('Missing pairing token'));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const SocketCtor = resolveSocketCtor(this.socketCtor);
|
|
260
|
+
const ws = new SocketCtor(buildPairingWsUrl(this.pairingEndpoint, this.pairingToken));
|
|
261
|
+
this.ws = ws;
|
|
262
|
+
let settled = false;
|
|
263
|
+
const timer = this.scheduler.setTimeout(() => {
|
|
264
|
+
if (settled)
|
|
265
|
+
return;
|
|
266
|
+
settled = true;
|
|
267
|
+
this.pendingConnect = null;
|
|
268
|
+
this.detachAndClose(ws);
|
|
269
|
+
reject(new UserflowAuthError('WebSocket connection timed out'));
|
|
270
|
+
}, TIMEOUT_MS);
|
|
271
|
+
const finish = () => {
|
|
272
|
+
settled = true;
|
|
273
|
+
this.pendingConnect = null;
|
|
274
|
+
this.scheduler.clearTimeout(timer);
|
|
275
|
+
};
|
|
276
|
+
this.pendingConnect = {
|
|
277
|
+
reject: (error) => {
|
|
278
|
+
if (settled)
|
|
279
|
+
return;
|
|
280
|
+
finish();
|
|
281
|
+
this.detachAndClose(ws);
|
|
282
|
+
reject(error);
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
ws.onopen = () => {
|
|
286
|
+
if (settled)
|
|
287
|
+
return;
|
|
288
|
+
finish();
|
|
289
|
+
resolve();
|
|
290
|
+
};
|
|
291
|
+
ws.onmessage = (event) => this.handleMessage(event);
|
|
292
|
+
ws.onerror = () => {
|
|
293
|
+
if (!settled) {
|
|
294
|
+
finish();
|
|
295
|
+
this.detachAndClose(ws);
|
|
296
|
+
reject(new UserflowAuthError('WebSocket connection failed'));
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
this.onSocketDown(ws);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
ws.onclose = () => {
|
|
303
|
+
if (!settled) {
|
|
304
|
+
finish();
|
|
305
|
+
this.detachAndClose(ws);
|
|
306
|
+
// Connect reject often means expired/invalid JWT (server closes).
|
|
307
|
+
if (this.isTokenExpired()) {
|
|
308
|
+
reject(new UserflowAuthError('Pairing token expired'));
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
reject(new UserflowAuthError('WebSocket closed before connect completed'));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
this.onSocketDown(ws);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
async joinChannel() {
|
|
321
|
+
if (!this.claims || !this.deviceId) {
|
|
322
|
+
throw new UserflowAuthError('Missing pairing session');
|
|
323
|
+
}
|
|
324
|
+
const topic = `pairing:${this.claims.pairing_id}`;
|
|
325
|
+
this.channelTopic = topic;
|
|
326
|
+
const payload = {
|
|
327
|
+
device_id: this.deviceId,
|
|
328
|
+
...this.joinMeta,
|
|
329
|
+
};
|
|
330
|
+
try {
|
|
331
|
+
await this.request(topic, (joinRef, ref) => {
|
|
332
|
+
this.channelJoinRef = joinRef;
|
|
333
|
+
return [joinRef, ref, topic, 'phx_join', payload];
|
|
334
|
+
});
|
|
335
|
+
this.channelJoined = true;
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
this.channelJoined = false;
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
onSocketDown(socket) {
|
|
343
|
+
if (this.ws !== socket)
|
|
344
|
+
return;
|
|
345
|
+
this.detachAndClose(socket);
|
|
346
|
+
this.channelJoined = false;
|
|
347
|
+
this.channelJoinRef = null;
|
|
348
|
+
this.stopHeartbeat();
|
|
349
|
+
this.rejectPending(new UserflowAuthError('Connection dropped'));
|
|
350
|
+
if (!this.shouldBeConnected) {
|
|
351
|
+
this.setState('disconnected');
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (this.isTokenExpired()) {
|
|
355
|
+
this.handleTokenExpired();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
this.setState('reconnecting');
|
|
359
|
+
this.scheduleReconnect();
|
|
360
|
+
}
|
|
361
|
+
scheduleReconnect() {
|
|
362
|
+
if (this.reconnectHandle !== null)
|
|
363
|
+
return;
|
|
364
|
+
const delay = this.backoff.nextDelay();
|
|
365
|
+
this.reconnectHandle = this.scheduler.setTimeout(() => {
|
|
366
|
+
this.reconnectHandle = null;
|
|
367
|
+
if (!this.shouldBeConnected)
|
|
368
|
+
return;
|
|
369
|
+
if (this.isTokenExpired()) {
|
|
370
|
+
this.handleTokenExpired();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
this.attemptEstablish();
|
|
374
|
+
}, delay);
|
|
375
|
+
}
|
|
376
|
+
cancelReconnect() {
|
|
377
|
+
if (this.reconnectHandle !== null) {
|
|
378
|
+
this.scheduler.clearTimeout(this.reconnectHandle);
|
|
379
|
+
this.reconnectHandle = null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
startHeartbeat() {
|
|
383
|
+
this.stopHeartbeat();
|
|
384
|
+
this.scheduleNextHeartbeat();
|
|
385
|
+
}
|
|
386
|
+
stopHeartbeat() {
|
|
387
|
+
this.clearHeartbeatTimer();
|
|
388
|
+
this.clearHeartbeatReplyTimer();
|
|
389
|
+
this.pendingHeartbeatRef = null;
|
|
390
|
+
}
|
|
391
|
+
scheduleNextHeartbeat() {
|
|
392
|
+
this.clearHeartbeatTimer();
|
|
393
|
+
this.heartbeatHandle = this.scheduler.setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs);
|
|
394
|
+
}
|
|
395
|
+
clearHeartbeatTimer() {
|
|
396
|
+
if (this.heartbeatHandle !== null) {
|
|
397
|
+
this.scheduler.clearTimeout(this.heartbeatHandle);
|
|
398
|
+
this.heartbeatHandle = null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
clearHeartbeatReplyTimer() {
|
|
402
|
+
if (this.heartbeatReplyHandle !== null) {
|
|
403
|
+
this.scheduler.clearTimeout(this.heartbeatReplyHandle);
|
|
404
|
+
this.heartbeatReplyHandle = null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
sendHeartbeat() {
|
|
408
|
+
this.heartbeatHandle = null;
|
|
409
|
+
if (this.state !== 'connected' || !this.ws)
|
|
410
|
+
return;
|
|
411
|
+
if (this.pendingHeartbeatRef !== null) {
|
|
412
|
+
this.onSocketDown(this.ws);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const ref = this.nextRef();
|
|
416
|
+
this.pendingHeartbeatRef = ref;
|
|
417
|
+
this.ws.send(JSON.stringify([null, ref, PHOENIX_TOPIC, 'heartbeat', {}]));
|
|
418
|
+
this.clearHeartbeatReplyTimer();
|
|
419
|
+
this.heartbeatReplyHandle = this.scheduler.setTimeout(() => {
|
|
420
|
+
this.heartbeatReplyHandle = null;
|
|
421
|
+
if (this.state !== 'connected' || !this.ws)
|
|
422
|
+
return;
|
|
423
|
+
if (this.pendingHeartbeatRef !== null) {
|
|
424
|
+
this.onSocketDown(this.ws);
|
|
425
|
+
}
|
|
426
|
+
}, this.heartbeatReplyTimeoutMs);
|
|
427
|
+
}
|
|
428
|
+
handleMessage(event) {
|
|
429
|
+
let frame;
|
|
430
|
+
try {
|
|
431
|
+
frame = JSON.parse(String(event.data));
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (!Array.isArray(frame) || frame.length < 5)
|
|
437
|
+
return;
|
|
438
|
+
const [, id, topic, eventName, payload] = frame;
|
|
439
|
+
if (eventName === 'presence_state') {
|
|
440
|
+
this.emit({ type: 'presence_state', payload });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (eventName === 'presence_diff') {
|
|
444
|
+
this.emit({ type: 'presence_diff', payload });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (eventName === 'phx_reply' && typeof id === 'string') {
|
|
448
|
+
if (topic === PHOENIX_TOPIC && id === this.pendingHeartbeatRef) {
|
|
449
|
+
this.pendingHeartbeatRef = null;
|
|
450
|
+
this.clearHeartbeatReplyTimer();
|
|
451
|
+
if (this.state === 'connected')
|
|
452
|
+
this.scheduleNextHeartbeat();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const wait = this.pending.get(id);
|
|
456
|
+
if (!wait || wait.topic !== topic)
|
|
457
|
+
return;
|
|
458
|
+
this.pending.delete(id);
|
|
459
|
+
this.scheduler.clearTimeout(wait.timer);
|
|
460
|
+
const reply = payload;
|
|
461
|
+
if (reply.status === 'ok') {
|
|
462
|
+
wait.ok(reply.response);
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
wait.fail(new UserflowAuthError(`${topic}: ${joinError(reply.response)}`));
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (this.channelTopic && topic === this.channelTopic && typeof eventName === 'string') {
|
|
470
|
+
if (eventName === 'startFlowWithToken') {
|
|
471
|
+
const body = (payload ?? {});
|
|
472
|
+
this.emit({ type: 'startFlowWithToken', payload: body });
|
|
473
|
+
this.emit({ type: 'relay', event: 'startFlowWithToken', payload });
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (isPairingRelayEvent(eventName)) {
|
|
477
|
+
this.emit({ type: 'relay', event: eventName, payload });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
request(topic, buildFrame) {
|
|
482
|
+
return new Promise((ok, fail) => {
|
|
483
|
+
if (!this.ws || this.ws.readyState !== WS_OPEN) {
|
|
484
|
+
fail(new UserflowAuthError('WebSocket is not open'));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const joinRef = this.channelJoinRef ?? this.nextRef();
|
|
488
|
+
const ref = this.nextRef();
|
|
489
|
+
const timer = this.scheduler.setTimeout(() => {
|
|
490
|
+
const wait = this.pending.get(ref);
|
|
491
|
+
if (!wait)
|
|
492
|
+
return;
|
|
493
|
+
this.pending.delete(ref);
|
|
494
|
+
wait.fail(new UserflowAuthError(`${topic}: reply timed out`));
|
|
495
|
+
}, TIMEOUT_MS);
|
|
496
|
+
this.pending.set(ref, { topic, timer, ok, fail });
|
|
497
|
+
this.ws.send(JSON.stringify(buildFrame(joinRef, ref)));
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
nextRef() {
|
|
501
|
+
this.ref += 1;
|
|
502
|
+
return String(this.ref);
|
|
503
|
+
}
|
|
504
|
+
resetChannel() {
|
|
505
|
+
this.channelTopic = null;
|
|
506
|
+
this.channelJoinRef = null;
|
|
507
|
+
this.channelJoined = false;
|
|
508
|
+
}
|
|
509
|
+
detachAndClose(ws) {
|
|
510
|
+
if (!ws)
|
|
511
|
+
return;
|
|
512
|
+
ws.onopen = null;
|
|
513
|
+
ws.onmessage = null;
|
|
514
|
+
ws.onerror = null;
|
|
515
|
+
ws.onclose = null;
|
|
516
|
+
try {
|
|
517
|
+
ws.close();
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
// ignore
|
|
521
|
+
}
|
|
522
|
+
if (this.ws === ws)
|
|
523
|
+
this.ws = null;
|
|
524
|
+
}
|
|
525
|
+
abortPendingConnect(error) {
|
|
526
|
+
this.pendingConnect?.reject(error);
|
|
527
|
+
this.pendingConnect = null;
|
|
528
|
+
}
|
|
529
|
+
rejectPending(error) {
|
|
530
|
+
for (const [, wait] of this.pending) {
|
|
531
|
+
this.scheduler.clearTimeout(wait.timer);
|
|
532
|
+
wait.fail(error);
|
|
533
|
+
}
|
|
534
|
+
this.pending.clear();
|
|
535
|
+
}
|
|
536
|
+
setState(state) {
|
|
537
|
+
this.state = state;
|
|
538
|
+
this.emit({ type: 'state', state });
|
|
539
|
+
}
|
|
540
|
+
emit(event) {
|
|
541
|
+
for (const listener of this.listeners) {
|
|
542
|
+
try {
|
|
543
|
+
listener(event);
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
// Never let a subscriber break the socket loop.
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
//# sourceMappingURL=pairing.js.map
|