@fluxy-chat/sdk 0.1.0 → 0.2.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.
@@ -0,0 +1,131 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import { FluxyChatClient } from "./index";
5
+ import { decodeFluxyJwtPayload, jwtRefreshDelayMs } from "./jwt-utils";
6
+ import { trimTrailingSlashes } from "./url-utils";
7
+ import { FluxyRealtimeContext } from "./use-fluxy-chat";
8
+ async function resolveAuthToken(provider) {
9
+ if (typeof provider === "string") {
10
+ return { token: provider };
11
+ }
12
+ const result = await provider();
13
+ if (typeof result === "string") {
14
+ return { token: result };
15
+ }
16
+ return result;
17
+ }
18
+ async function fetchConnectSession(connectUrl, init) {
19
+ const res = await fetch(connectUrl, {
20
+ method: "POST",
21
+ headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) },
22
+ body: JSON.stringify({}),
23
+ ...init,
24
+ });
25
+ const data = (await res.json().catch(() => ({})));
26
+ if (!res.ok) {
27
+ throw new Error(data.error ?? `Connect failed (${res.status})`);
28
+ }
29
+ if (!data.memberJwt?.trim()) {
30
+ throw new Error("Connect response did not include memberJwt");
31
+ }
32
+ return {
33
+ token: data.memberJwt,
34
+ userId: data.memberUserId,
35
+ };
36
+ }
37
+ export function FluxyRealtimeProvider({ children, workerUrl, authTokenProvider, connectUrl, connectRequestInit, userId: userIdProp, refreshBufferMs = 5 * 60 * 1000, onSessionError, }) {
38
+ const [token, setToken] = React.useState(null);
39
+ const [userId, setUserId] = React.useState(userIdProp ?? "");
40
+ const [refreshKey, setRefreshKey] = React.useState(0);
41
+ const isRefreshingRef = React.useRef(false);
42
+ const providerRef = React.useRef(authTokenProvider);
43
+ const connectUrlRef = React.useRef(connectUrl);
44
+ const connectInitRef = React.useRef(connectRequestInit);
45
+ React.useEffect(() => {
46
+ providerRef.current = authTokenProvider;
47
+ });
48
+ React.useEffect(() => {
49
+ connectUrlRef.current = connectUrl;
50
+ });
51
+ React.useEffect(() => {
52
+ connectInitRef.current = connectRequestInit;
53
+ });
54
+ React.useEffect(() => {
55
+ if (userIdProp)
56
+ setUserId(userIdProp);
57
+ }, [userIdProp]);
58
+ const refreshSession = React.useCallback(() => {
59
+ if (isRefreshingRef.current)
60
+ return;
61
+ setRefreshKey((k) => k + 1);
62
+ }, []);
63
+ React.useEffect(() => {
64
+ if (!authTokenProvider && !connectUrlRef.current)
65
+ return;
66
+ let cancelled = false;
67
+ let timer = null;
68
+ const run = async () => {
69
+ isRefreshingRef.current = true;
70
+ try {
71
+ let session;
72
+ if (connectUrlRef.current) {
73
+ session = await fetchConnectSession(connectUrlRef.current, connectInitRef.current);
74
+ }
75
+ else if (providerRef.current) {
76
+ session = await resolveAuthToken(providerRef.current);
77
+ }
78
+ else {
79
+ return;
80
+ }
81
+ if (cancelled)
82
+ return;
83
+ setToken(session.token);
84
+ const claims = decodeFluxyJwtPayload(session.token);
85
+ const resolvedUserId = session.userId ?? claims.sub ?? userIdProp ?? "";
86
+ if (resolvedUserId)
87
+ setUserId(resolvedUserId);
88
+ if (claims.exp && typeof providerRef.current !== "string") {
89
+ const delay = jwtRefreshDelayMs(claims.exp, refreshBufferMs);
90
+ timer = setTimeout(() => {
91
+ if (!cancelled)
92
+ run();
93
+ }, delay);
94
+ }
95
+ }
96
+ catch (err) {
97
+ const error = err instanceof Error ? err : new Error(String(err));
98
+ onSessionError?.(error);
99
+ }
100
+ finally {
101
+ if (!cancelled)
102
+ isRefreshingRef.current = false;
103
+ }
104
+ };
105
+ void run();
106
+ return () => {
107
+ cancelled = true;
108
+ isRefreshingRef.current = false;
109
+ if (timer)
110
+ clearTimeout(timer);
111
+ };
112
+ }, [authTokenProvider, connectUrl, connectRequestInit, refreshBufferMs, refreshKey, userIdProp, onSessionError]);
113
+ const client = React.useMemo(() => {
114
+ if (!token?.trim() || !userId.trim())
115
+ return null;
116
+ return new FluxyChatClient({
117
+ baseUrl: trimTrailingSlashes(workerUrl),
118
+ userId,
119
+ token,
120
+ });
121
+ }, [workerUrl, userId, token]);
122
+ const value = React.useMemo(() => ({
123
+ client,
124
+ userId,
125
+ token,
126
+ workerUrl,
127
+ ready: Boolean(client),
128
+ refreshSession,
129
+ }), [client, userId, token, workerUrl, refreshSession]);
130
+ return _jsx(FluxyRealtimeContext.Provider, { value: value, children: children });
131
+ }
@@ -0,0 +1,4 @@
1
+ import type { FluxyRoomMember } from "./index";
2
+ export declare const FLUXY_MAX_MESSAGE_LENGTH = 4000;
3
+ export declare function normalizeRoomMember(raw: Record<string, unknown>): FluxyRoomMember | null;
4
+ export declare function normalizeRoomMembers(rows: unknown[]): FluxyRoomMember[];
@@ -0,0 +1,24 @@
1
+ export const FLUXY_MAX_MESSAGE_LENGTH = 4000;
2
+ export function normalizeRoomMember(raw) {
3
+ const userId = String(raw.userId ?? raw.user_id ?? "").trim();
4
+ if (!userId)
5
+ return null;
6
+ const role = String(raw.role ?? "member").trim() || "member";
7
+ const joined_at = typeof raw.joined_at === "string"
8
+ ? raw.joined_at
9
+ : typeof raw.joinedAt === "string"
10
+ ? raw.joinedAt
11
+ : undefined;
12
+ return { userId, role, joined_at };
13
+ }
14
+ export function normalizeRoomMembers(rows) {
15
+ const out = [];
16
+ for (const row of rows) {
17
+ if (!row || typeof row !== "object")
18
+ continue;
19
+ const member = normalizeRoomMember(row);
20
+ if (member)
21
+ out.push(member);
22
+ }
23
+ return out;
24
+ }
@@ -0,0 +1,2 @@
1
+ /** Strip trailing slashes without regex (avoids ReDoS on hostile URLs). */
2
+ export declare function trimTrailingSlashes(url: string): string;
@@ -0,0 +1,7 @@
1
+ /** Strip trailing slashes without regex (avoids ReDoS on hostile URLs). */
2
+ export function trimTrailingSlashes(url) {
3
+ let out = url;
4
+ while (out.endsWith("/"))
5
+ out = out.slice(0, -1);
6
+ return out;
7
+ }
@@ -0,0 +1,49 @@
1
+ import type { FluxyChatAttachment, FluxyChatClient, FluxyChatMessage } from "./index";
2
+ export interface UseChatOptions {
3
+ roomId: string;
4
+ /** Omit when wrapped in `FluxyRealtimeProvider`. */
5
+ client?: FluxyChatClient;
6
+ agentId?: string;
7
+ /** Initial REST page size (default 50). */
8
+ historyLimit?: number;
9
+ }
10
+ export declare function useChat({ roomId, client: clientProp, agentId, historyLimit }: UseChatOptions): {
11
+ messages: FluxyChatMessage[];
12
+ hasMore: boolean;
13
+ isLoadingMore: boolean;
14
+ loadMore: () => Promise<void>;
15
+ online: number;
16
+ typingUsers: Record<string, boolean>;
17
+ seenBy: Record<number, string[]>;
18
+ onlineUsers: string[];
19
+ connected: boolean;
20
+ connectionStatus: "connecting" | "connected" | "reconnecting" | "disconnected" | "polling" | "sse";
21
+ reconnectAttempt: number;
22
+ connectionError: Error | null;
23
+ agentTyping: boolean;
24
+ typingAgentId: string | null;
25
+ reactions: Record<number, Record<string, number>>;
26
+ sendMessage: (content: string, replyTo?: number | null, attachments?: FluxyChatAttachment[]) => void;
27
+ setTyping: (isTyping: boolean) => void;
28
+ editMessage: (messageId: number, content: string) => void;
29
+ sendReaction: (messageId: number, emoji: string, op?: "add" | "remove") => void;
30
+ sendReadReceipt: (messageId: number) => void;
31
+ deleteMessage: (messageId: number) => void;
32
+ invokeAgent: (content: string, options?: {
33
+ agentId?: string;
34
+ replyTo?: number | null;
35
+ }) => Promise<{
36
+ run: {
37
+ id: string;
38
+ status: string;
39
+ latencyMs?: number;
40
+ inputTokens?: number;
41
+ outputTokens?: number;
42
+ estimatedCost?: number;
43
+ iterations?: number;
44
+ toolCalls?: import("./index").FluxyChatToolCall[];
45
+ createdAt: string;
46
+ };
47
+ message: FluxyChatMessage;
48
+ }>;
49
+ };