@athenaintel/react 0.21.2 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/dist/chat/StatewireApprovalCard.cjs +70 -10
- package/dist/chat/StatewireApprovalCard.cjs.map +1 -1
- package/dist/chat/StatewireApprovalCard.d.ts +12 -1
- package/dist/chat/StatewireApprovalCard.js +73 -13
- package/dist/chat/StatewireApprovalCard.js.map +1 -1
- package/dist/chat/use-statewire-approval-request.cjs +22 -1
- package/dist/chat/use-statewire-approval-request.cjs.map +1 -1
- package/dist/chat/use-statewire-approval-request.d.ts +41 -0
- package/dist/chat/use-statewire-approval-request.js +24 -3
- package/dist/chat/use-statewire-approval-request.js.map +1 -1
- package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs +0 -1
- package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs.map +1 -1
- package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js +0 -1
- package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js.map +1 -1
- package/dist/collab/client.cjs +4 -9
- package/dist/collab/client.cjs.map +1 -1
- package/dist/collab/client.js +2 -7
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/index.cjs +23 -0
- package/dist/collab/index.cjs.map +1 -1
- package/dist/collab/index.d.ts +17 -2
- package/dist/collab/index.js +24 -1
- package/dist/collab/index.js.map +1 -1
- package/dist/collab/mint.cjs +18 -13
- package/dist/collab/mint.cjs.map +1 -1
- package/dist/collab/mint.d.ts +10 -0
- package/dist/collab/mint.js +18 -13
- package/dist/collab/mint.js.map +1 -1
- package/dist/collab/presence-client.cjs +211 -0
- package/dist/collab/presence-client.cjs.map +1 -0
- package/dist/collab/presence-client.d.ts +60 -0
- package/dist/collab/presence-client.js +194 -0
- package/dist/collab/presence-client.js.map +1 -0
- package/dist/collab/presence-mint.cjs +68 -0
- package/dist/collab/presence-mint.cjs.map +1 -0
- package/dist/collab/presence-mint.d.ts +87 -0
- package/dist/collab/presence-mint.js +68 -0
- package/dist/collab/presence-mint.js.map +1 -0
- package/dist/collab/presence-react.cjs +114 -0
- package/dist/collab/presence-react.cjs.map +1 -0
- package/dist/collab/presence-react.d.ts +47 -0
- package/dist/collab/presence-react.js +114 -0
- package/dist/collab/presence-react.js.map +1 -0
- package/dist/collab/presence-slots.cjs +220 -0
- package/dist/collab/presence-slots.cjs.map +1 -0
- package/dist/collab/presence-slots.d.ts +119 -0
- package/dist/collab/presence-slots.js +220 -0
- package/dist/collab/presence-slots.js.map +1 -0
- package/dist/collab/token-refresh.cjs +13 -0
- package/dist/collab/token-refresh.cjs.map +1 -0
- package/dist/collab/token-refresh.d.ts +20 -0
- package/dist/collab/token-refresh.js +13 -0
- package/dist/collab/token-refresh.js.map +1 -0
- package/dist/packages/agent-runtime/src/interrupts.cjs +40 -1
- package/dist/packages/agent-runtime/src/interrupts.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/interrupts.js +40 -1
- package/dist/packages/agent-runtime/src/interrupts.js.map +1 -1
- package/dist/packages/react/package.json.cjs +1 -1
- package/dist/packages/react/package.json.js +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { WebsocketProvider } from "@y/websocket";
|
|
2
|
+
import * as Y from "@y/y";
|
|
3
|
+
import { classifyClose } from "./close-policy.js";
|
|
4
|
+
import { mintWorkspacePresenceToken } from "./presence-mint.js";
|
|
5
|
+
import { mergePresenceStates } from "./presence-slots.js";
|
|
6
|
+
import { MIN_REFRESH_DELAY_MS, MAX_CONSECUTIVE_REMINTS, refreshDelayMs } from "./token-refresh.js";
|
|
7
|
+
const PRESENCE_SWEEP_INTERVAL_MS = 15e3;
|
|
8
|
+
const EMPTY_ROSTER = { humans: [], agents: [] };
|
|
9
|
+
function connectWorkspacePresence(args) {
|
|
10
|
+
const doc = new Y.Doc();
|
|
11
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
12
|
+
let provider = null;
|
|
13
|
+
let refreshTimer = null;
|
|
14
|
+
let destroyed = false;
|
|
15
|
+
let status = "connecting";
|
|
16
|
+
let accessType = null;
|
|
17
|
+
let error = null;
|
|
18
|
+
let version = 0;
|
|
19
|
+
let consecutiveRemints = 0;
|
|
20
|
+
let connectSeq = 0;
|
|
21
|
+
let roster = EMPTY_ROSTER;
|
|
22
|
+
let rosterFingerprint = JSON.stringify(EMPTY_ROSTER);
|
|
23
|
+
const notify = () => {
|
|
24
|
+
version += 1;
|
|
25
|
+
for (const listener of listeners) listener();
|
|
26
|
+
};
|
|
27
|
+
const applyRoster = (next) => {
|
|
28
|
+
const fingerprint = JSON.stringify(next);
|
|
29
|
+
if (fingerprint === rosterFingerprint) return false;
|
|
30
|
+
roster = next;
|
|
31
|
+
rosterFingerprint = fingerprint;
|
|
32
|
+
return true;
|
|
33
|
+
};
|
|
34
|
+
const recomputeRoster = () => {
|
|
35
|
+
if (destroyed) return;
|
|
36
|
+
const next = provider ? mergePresenceStates(provider.awareness.getStates()) : EMPTY_ROSTER;
|
|
37
|
+
if (applyRoster(next)) notify();
|
|
38
|
+
};
|
|
39
|
+
const setStatus = (next, err = null) => {
|
|
40
|
+
if (destroyed && next !== "closed") return;
|
|
41
|
+
status = next;
|
|
42
|
+
error = err;
|
|
43
|
+
if (next === "closed" || next === "error") applyRoster(EMPTY_ROSTER);
|
|
44
|
+
notify();
|
|
45
|
+
};
|
|
46
|
+
const clearRefreshTimer = () => {
|
|
47
|
+
if (refreshTimer !== null) {
|
|
48
|
+
clearTimeout(refreshTimer);
|
|
49
|
+
refreshTimer = null;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const teardownProvider = () => {
|
|
53
|
+
clearRefreshTimer();
|
|
54
|
+
if (provider) {
|
|
55
|
+
const stale = provider;
|
|
56
|
+
provider = null;
|
|
57
|
+
stale.destroy();
|
|
58
|
+
stale.awareness.destroy();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const scheduleRefresh = (grant) => {
|
|
62
|
+
clearRefreshTimer();
|
|
63
|
+
const delay = refreshDelayMs({ expiresAtMs: grant.expires_at_ms, now: Date.now() });
|
|
64
|
+
refreshTimer = setTimeout(() => {
|
|
65
|
+
void connect({ scheduledRefresh: true });
|
|
66
|
+
}, delay);
|
|
67
|
+
};
|
|
68
|
+
const connect = async (opts) => {
|
|
69
|
+
if (destroyed) return;
|
|
70
|
+
const attempt = ++connectSeq;
|
|
71
|
+
const scheduledRefresh = (opts == null ? void 0 : opts.scheduledRefresh) === true;
|
|
72
|
+
if (!scheduledRefresh) {
|
|
73
|
+
teardownProvider();
|
|
74
|
+
} else {
|
|
75
|
+
clearRefreshTimer();
|
|
76
|
+
}
|
|
77
|
+
let grant;
|
|
78
|
+
try {
|
|
79
|
+
grant = await mintWorkspacePresenceToken({ auth: args.auth, workspaceId: args.workspaceId });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (destroyed || attempt !== connectSeq) return;
|
|
82
|
+
if (scheduledRefresh) {
|
|
83
|
+
refreshTimer = setTimeout(() => {
|
|
84
|
+
void connect({ scheduledRefresh: true });
|
|
85
|
+
}, MIN_REFRESH_DELAY_MS);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
setStatus("error", err instanceof Error ? err : new Error(String(err)));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (destroyed || attempt !== connectSeq) return;
|
|
92
|
+
if (scheduledRefresh) {
|
|
93
|
+
teardownProvider();
|
|
94
|
+
}
|
|
95
|
+
accessType = grant.access_type;
|
|
96
|
+
const nextProvider = new WebsocketProvider(grant.ws_url, `${grant.org}/${grant.doc_id}`, doc, {
|
|
97
|
+
params: {
|
|
98
|
+
yauth: grant.token,
|
|
99
|
+
branch: grant.branch
|
|
100
|
+
},
|
|
101
|
+
// BroadcastChannel would share this room with same-origin tabs
|
|
102
|
+
// locally, bypassing the server's per-recipient filter. Presence must
|
|
103
|
+
// round-trip the server.
|
|
104
|
+
disableBc: true
|
|
105
|
+
});
|
|
106
|
+
provider = nextProvider;
|
|
107
|
+
nextProvider.awareness.off("update", nextProvider._awarenessUpdateHandler);
|
|
108
|
+
nextProvider.awareness.setLocalState(null);
|
|
109
|
+
nextProvider.on("status", (event) => {
|
|
110
|
+
if (provider !== nextProvider) return;
|
|
111
|
+
if (event.status === "connected") {
|
|
112
|
+
consecutiveRemints = 0;
|
|
113
|
+
}
|
|
114
|
+
setStatus(event.status);
|
|
115
|
+
});
|
|
116
|
+
nextProvider.on("connection-close", (event) => {
|
|
117
|
+
if (provider !== nextProvider) return;
|
|
118
|
+
const action = classifyClose(event == null ? void 0 : event.code);
|
|
119
|
+
if (action === "remint") {
|
|
120
|
+
consecutiveRemints += 1;
|
|
121
|
+
teardownProvider();
|
|
122
|
+
if (consecutiveRemints > MAX_CONSECUTIVE_REMINTS) {
|
|
123
|
+
setStatus(
|
|
124
|
+
"closed",
|
|
125
|
+
new Error("Access repeatedly revoked mid-session (4401); giving up.")
|
|
126
|
+
);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
applyRoster(EMPTY_ROSTER);
|
|
130
|
+
setStatus("connecting");
|
|
131
|
+
void connect();
|
|
132
|
+
} else if (action === "stop") {
|
|
133
|
+
teardownProvider();
|
|
134
|
+
setStatus(
|
|
135
|
+
"closed",
|
|
136
|
+
new Error(`Connection closed permanently (code ${(event == null ? void 0 : event.code) ?? "unknown"})`)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
const onAwareness = () => {
|
|
141
|
+
if (provider !== nextProvider) return;
|
|
142
|
+
recomputeRoster();
|
|
143
|
+
};
|
|
144
|
+
nextProvider.awareness.on("change", onAwareness);
|
|
145
|
+
nextProvider.awareness.on("update", onAwareness);
|
|
146
|
+
scheduleRefresh(grant);
|
|
147
|
+
};
|
|
148
|
+
void connect();
|
|
149
|
+
const sweepTimer = setInterval(recomputeRoster, PRESENCE_SWEEP_INTERVAL_MS);
|
|
150
|
+
return {
|
|
151
|
+
get status() {
|
|
152
|
+
return status;
|
|
153
|
+
},
|
|
154
|
+
get error() {
|
|
155
|
+
return error;
|
|
156
|
+
},
|
|
157
|
+
get accessType() {
|
|
158
|
+
return accessType;
|
|
159
|
+
},
|
|
160
|
+
get version() {
|
|
161
|
+
return version;
|
|
162
|
+
},
|
|
163
|
+
get humans() {
|
|
164
|
+
return roster.humans;
|
|
165
|
+
},
|
|
166
|
+
get agents() {
|
|
167
|
+
return roster.agents;
|
|
168
|
+
},
|
|
169
|
+
refreshNow: () => connect({ scheduledRefresh: true }),
|
|
170
|
+
onChange: (listener) => {
|
|
171
|
+
listeners.add(listener);
|
|
172
|
+
return () => {
|
|
173
|
+
listeners.delete(listener);
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
destroy: () => {
|
|
177
|
+
if (destroyed) return;
|
|
178
|
+
destroyed = true;
|
|
179
|
+
clearInterval(sweepTimer);
|
|
180
|
+
teardownProvider();
|
|
181
|
+
status = "closed";
|
|
182
|
+
applyRoster(EMPTY_ROSTER);
|
|
183
|
+
version += 1;
|
|
184
|
+
for (const listener of [...listeners]) listener();
|
|
185
|
+
listeners.clear();
|
|
186
|
+
doc.destroy();
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
export {
|
|
191
|
+
PRESENCE_SWEEP_INTERVAL_MS,
|
|
192
|
+
connectWorkspacePresence
|
|
193
|
+
};
|
|
194
|
+
//# sourceMappingURL=presence-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presence-client.js","sources":["../../src/collab/presence-client.ts"],"sourcesContent":["/**\n * Vanilla (framework-agnostic) observer of a workspace's presence feed.\n *\n * `connectWorkspacePresence` mints a read-only token for the workspace\n * awareness room (`<workspaceId>/awareness`), opens the Keryx WebSocket with\n * the v14 provider, and keeps a merged roster of the people and agent runs in\n * the room — the same per-recipient-filtered feed the Olympus People page\n * renders. Token lifecycle matches `connectGenericDoc`: re-mint before expiry,\n * one fresh mint on a 4401 kick, permanent stop on any other 44xx code,\n * provider backoff for everything else.\n *\n * The client is a **pure observer**. It never writes to the Y.Doc, it drops\n * the provider's default local awareness slot before the socket opens so the\n * token holder never announces itself, and it detaches y-websocket's echo of\n * remote awareness updates — the room's read-only grant carries no broadcast\n * permission, so anything sent would only be discarded server-side. React\n * apps use `useWorkspacePresence` from `@athenaintel/react/collab` instead of\n * calling this directly.\n */\n\nimport { WebsocketProvider } from '@y/websocket';\nimport * as Y from '@y/y';\nimport type { GenericDocStatus } from './client';\nimport { classifyClose } from './close-policy';\nimport type { CollabAuth } from './mint';\nimport { mintWorkspacePresenceToken, type WorkspacePresenceTokenResponse } from './presence-mint';\nimport {\n mergePresenceStates,\n type PresenceAgent,\n type PresenceHuman,\n type PresenceRoster,\n} from './presence-slots';\nimport { MAX_CONSECUTIVE_REMINTS, MIN_REFRESH_DELAY_MS, refreshDelayMs } from './token-refresh';\n\n/** Same vocabulary as the Generic Doc client; a presence feed has no sync phase of its own. */\nexport type WorkspacePresenceStatus = GenericDocStatus;\n\n/**\n * How often the roster is re-derived without an awareness event. y-protocols\n * drops a slot whose publisher went silent for its `outdatedTimeout` (30 s)\n * from a timer of its own, and this sweep is the belt to that brace: a stale\n * slot falls out of `humans` even if no event reaches us for it.\n */\nexport const PRESENCE_SWEEP_INTERVAL_MS = 15_000;\n\nexport interface WorkspacePresenceHandle {\n readonly status: WorkspacePresenceStatus;\n /** Terminal error, when status is 'error' or 'closed'. */\n readonly error: Error | null;\n /** Granted access from the last successful mint — always 'r' for this feed. */\n readonly accessType: 'r' | 'rw' | null;\n /** Monotonic change counter — bumps on every status or roster notification. */\n readonly version: number;\n /**\n * People in the room, one entry per person (tabs merged by attested id).\n * Identity-stable between notifications: the array only changes when the\n * derived roster does, so it is safe to key memoisation on.\n */\n readonly humans: PresenceHuman[];\n /** Agent runs in the room with something the token holder may see. */\n readonly agents: PresenceAgent[];\n /**\n * Force an early token refresh. Session-preserving: the live connection\n * stays up until the fresh token is minted; a failed mint retries on a\n * short timer instead of dropping the session.\n */\n refreshNow: () => Promise<void>;\n onChange: (listener: () => void) => () => void;\n destroy: () => void;\n}\n\nconst EMPTY_ROSTER: PresenceRoster = { humans: [], agents: [] };\n\nexport function connectWorkspacePresence(args: {\n workspaceId: string;\n auth: CollabAuth;\n}): WorkspacePresenceHandle {\n // The awareness room is a Keryx doc like any other; the provider needs a\n // Y.Doc to attach to. Nothing here ever writes to it.\n const doc = new Y.Doc();\n const listeners = new Set<() => void>();\n\n let provider: WebsocketProvider | null = null;\n let refreshTimer: ReturnType<typeof setTimeout> | null = null;\n let destroyed = false;\n let status: WorkspacePresenceStatus = 'connecting';\n let accessType: 'r' | 'rw' | null = null;\n let error: Error | null = null;\n let version = 0;\n let consecutiveRemints = 0;\n /**\n * Connection attempts can overlap — the initial mint, `refreshNow`, the\n * refresh timer and a 4401 kick all enter `connect()`. The newest attempt\n * owns the connection; an older one still waiting on its mint drops its\n * result when it lands, so an out-of-order grant can neither replace a newer\n * provider nor leave one connected but orphaned.\n */\n let connectSeq = 0;\n let roster: PresenceRoster = EMPTY_ROSTER;\n let rosterFingerprint = JSON.stringify(EMPTY_ROSTER);\n\n const notify = () => {\n version += 1;\n for (const listener of listeners) listener();\n };\n\n /** Replace the roster only when the derived value changed; returns whether it did. */\n const applyRoster = (next: PresenceRoster): boolean => {\n const fingerprint = JSON.stringify(next);\n if (fingerprint === rosterFingerprint) return false;\n roster = next;\n rosterFingerprint = fingerprint;\n return true;\n };\n\n const recomputeRoster = () => {\n if (destroyed) return;\n const next = provider ? mergePresenceStates(provider.awareness.getStates()) : EMPTY_ROSTER;\n if (applyRoster(next)) notify();\n };\n\n const setStatus = (next: WorkspacePresenceStatus, err: Error | null = null) => {\n if (destroyed && next !== 'closed') return;\n status = next;\n error = err;\n // A feed that stopped must not keep showing the last roster: silence\n // reading as \"these people are still here\" is the failure presence exists\n // to avoid.\n if (next === 'closed' || next === 'error') applyRoster(EMPTY_ROSTER);\n notify();\n };\n\n const clearRefreshTimer = () => {\n if (refreshTimer !== null) {\n clearTimeout(refreshTimer);\n refreshTimer = null;\n }\n };\n\n const teardownProvider = () => {\n clearRefreshTimer();\n if (provider) {\n const stale = provider;\n // Null first so the awareness listeners below see a superseded provider\n // and ignore the events its destruction emits.\n provider = null;\n stale.destroy();\n // The provider does not destroy the awareness it created; without this\n // every superseded instance keeps its outdated-state timer alive until\n // the doc is destroyed.\n stale.awareness.destroy();\n }\n };\n\n const scheduleRefresh = (grant: WorkspacePresenceTokenResponse) => {\n clearRefreshTimer();\n const delay = refreshDelayMs({ expiresAtMs: grant.expires_at_ms, now: Date.now() });\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, delay);\n };\n\n const connect = async (opts?: { scheduledRefresh?: boolean }): Promise<void> => {\n if (destroyed) return;\n const attempt = ++connectSeq;\n const scheduledRefresh = opts?.scheduledRefresh === true;\n // A scheduled refresh mints FIRST and keeps the live session up: the old\n // token has ~5 minutes of headroom, so a transient mint failure retries\n // on a short timer instead of dropping a healthy connection.\n if (!scheduledRefresh) {\n teardownProvider();\n } else {\n clearRefreshTimer();\n }\n let grant: WorkspacePresenceTokenResponse;\n try {\n grant = await mintWorkspacePresenceToken({ auth: args.auth, workspaceId: args.workspaceId });\n } catch (err) {\n // Superseded while minting: the newer attempt owns the outcome, and a\n // retry timer from this one would only stomp on its schedule.\n if (destroyed || attempt !== connectSeq) return;\n if (scheduledRefresh) {\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, MIN_REFRESH_DELAY_MS);\n return;\n }\n // A denied mint is authoritative (not an admin / roster not enabled /\n // sandbox credential refused); anything else (network) is worth telling\n // the caller about too — the handle stays usable via a later explicit\n // reconnect-by-recreate.\n setStatus('error', err instanceof Error ? err : new Error(String(err)));\n return;\n }\n if (destroyed || attempt !== connectSeq) return;\n if (scheduledRefresh) {\n teardownProvider();\n }\n accessType = grant.access_type;\n\n const nextProvider = new WebsocketProvider(grant.ws_url, `${grant.org}/${grant.doc_id}`, doc, {\n params: {\n yauth: grant.token,\n branch: grant.branch,\n },\n // BroadcastChannel would share this room with same-origin tabs\n // locally, bypassing the server's per-recipient filter. Presence must\n // round-trip the server.\n disableBc: true,\n });\n provider = nextProvider;\n\n // Pure observer. y-websocket re-broadcasts every awareness update it\n // sees (including remote ones) and announces a default `{}` local slot on\n // connect; drop both before the socket opens. Detach the echo handler\n // first so clearing the local slot does not itself get echoed.\n nextProvider.awareness.off('update', nextProvider._awarenessUpdateHandler);\n nextProvider.awareness.setLocalState(null);\n\n nextProvider.on('status', (event: { status: 'connecting' | 'connected' | 'disconnected' }) => {\n if (provider !== nextProvider) return;\n if (event.status === 'connected') {\n consecutiveRemints = 0;\n }\n setStatus(event.status);\n });\n\n nextProvider.on('connection-close', (event: CloseEvent | null) => {\n if (provider !== nextProvider) return;\n const action = classifyClose(event?.code);\n if (action === 'remint') {\n // Revoked mid-session: a fresh mint re-runs the server-side check. If\n // access is truly gone the mint 403s and we land in 'error'. Bounded:\n // repeated 4401s without an intervening successful connect mean the\n // server keeps rejecting freshly minted tokens — stop rather than\n // loop mint→connect→kick.\n consecutiveRemints += 1;\n teardownProvider();\n if (consecutiveRemints > MAX_CONSECUTIVE_REMINTS) {\n setStatus(\n 'closed',\n new Error('Access repeatedly revoked mid-session (4401); giving up.')\n );\n return;\n }\n // The socket is gone and the fresh grant is a network round-trip\n // away: until it connects there is no live feed, so neither\n // 'connected' nor the last roster may stand.\n applyRoster(EMPTY_ROSTER);\n setStatus('connecting');\n void connect();\n } else if (action === 'stop') {\n teardownProvider();\n setStatus(\n 'closed',\n new Error(`Connection closed permanently (code ${event?.code ?? 'unknown'})`)\n );\n }\n // 'retry': the provider's exponential backoff handles it.\n });\n\n const onAwareness = () => {\n if (provider !== nextProvider) return;\n recomputeRoster();\n };\n // 'change' fires when a slot's content changed; 'update' also on\n // keepalive renewals. Both re-derive; the fingerprint gate keeps a\n // renewal from notifying subscribers.\n nextProvider.awareness.on('change', onAwareness);\n nextProvider.awareness.on('update', onAwareness);\n\n scheduleRefresh(grant);\n };\n\n void connect();\n\n const sweepTimer = setInterval(recomputeRoster, PRESENCE_SWEEP_INTERVAL_MS);\n\n return {\n get status() {\n return status;\n },\n get error() {\n return error;\n },\n get accessType() {\n return accessType;\n },\n get version() {\n return version;\n },\n get humans() {\n return roster.humans;\n },\n get agents() {\n return roster.agents;\n },\n refreshNow: () => connect({ scheduledRefresh: true }),\n onChange: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n destroy: () => {\n if (destroyed) return;\n destroyed = true;\n clearInterval(sweepTimer);\n teardownProvider();\n status = 'closed';\n applyRoster(EMPTY_ROSTER);\n // Final notification so subscribers observe the closed, empty feed\n // before their listeners are dropped.\n version += 1;\n for (const listener of [...listeners]) listener();\n listeners.clear();\n doc.destroy();\n },\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const mint = require("./mint.cjs");
|
|
4
|
+
function workspaceUrl({
|
|
5
|
+
auth,
|
|
6
|
+
workspaceId,
|
|
7
|
+
path
|
|
8
|
+
}) {
|
|
9
|
+
const base = mint.getAthenaApiBaseUrl(auth.backendUrl);
|
|
10
|
+
return `${base}/api/v0/workspaces/${encodeURIComponent(workspaceId)}/${path}`;
|
|
11
|
+
}
|
|
12
|
+
async function mintWorkspacePresenceToken(args) {
|
|
13
|
+
const response = await fetch(
|
|
14
|
+
workspaceUrl({ auth: args.auth, workspaceId: args.workspaceId, path: "presence-token" }),
|
|
15
|
+
{
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
...mint.getCollabAuthHeaders(args.auth)
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({}),
|
|
22
|
+
signal: args.signal
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw await mint.collabErrorFromResponse(
|
|
27
|
+
response,
|
|
28
|
+
`Workspace presence token mint failed with status ${response.status}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return await response.json();
|
|
32
|
+
}
|
|
33
|
+
function arrayOrEmpty(value) {
|
|
34
|
+
return Array.isArray(value) ? value : [];
|
|
35
|
+
}
|
|
36
|
+
async function resolveWorkspacePresence(args) {
|
|
37
|
+
const response = await fetch(
|
|
38
|
+
workspaceUrl({ auth: args.auth, workspaceId: args.workspaceId, path: "presence/resolve" }),
|
|
39
|
+
{
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
...mint.getCollabAuthHeaders(args.auth)
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify(args.request),
|
|
46
|
+
signal: args.signal
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw await mint.collabErrorFromResponse(
|
|
51
|
+
response,
|
|
52
|
+
`Workspace presence resolve failed with status ${response.status}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const body = await response.json();
|
|
56
|
+
if (!body || typeof body !== "object") {
|
|
57
|
+
throw new mint.CollabTokenError(response.status, "Workspace presence resolve returned no body");
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
assets: arrayOrEmpty(body.assets),
|
|
61
|
+
sessions: arrayOrEmpty(body.sessions),
|
|
62
|
+
projects: arrayOrEmpty(body.projects),
|
|
63
|
+
members: arrayOrEmpty(body.members)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
exports.mintWorkspacePresenceToken = mintWorkspacePresenceToken;
|
|
67
|
+
exports.resolveWorkspacePresence = resolveWorkspacePresence;
|
|
68
|
+
//# sourceMappingURL=presence-mint.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presence-mint.cjs","sources":["../../src/collab/presence-mint.ts"],"sourcesContent":["/**\n * Public-API clients for the workspace presence feed.\n *\n * Two endpoints, both admin-only and both under the workspace:\n *\n * - `POST /api/v0/workspaces/{workspaceId}/presence-token` mints a\n * **read-only** Keryx token for the workspace awareness room\n * (`<workspaceId>/awareness`) — the same per-recipient-filtered presence\n * feed the Olympus People page renders. The holder can receive presence and\n * broadcast nothing; `connectWorkspacePresence` re-mints before expiry.\n * - `POST /api/v0/workspaces/{workspaceId}/presence/resolve` turns the opaque\n * ids carried in awareness contexts (asset ids, session thread ids, project\n * ids) into titles the caller may see. Only things the caller may open come\n * back; missing ids are simply absent.\n *\n * Wire shapes are snake_case, matching `CollabTokenResponse`.\n */\n\nimport {\n type CollabAuth,\n CollabTokenError,\n collabErrorFromResponse,\n getAthenaApiBaseUrl,\n getCollabAuthHeaders,\n} from './mint';\n\n/** Grant for the workspace awareness room. Always read-only (`access_type: 'r'`). */\nexport interface WorkspacePresenceTokenResponse {\n token: string;\n access_type: 'r' | 'rw';\n expires_at_ms: number;\n ws_url: string;\n org: string;\n doc_id: string;\n branch: string;\n}\n\nexport interface WorkspacePresenceResolveRequest {\n asset_ids?: string[];\n /** Thread ids, as published in `session` awareness contexts. */\n session_ids?: string[];\n project_ids?: string[];\n include_projects?: boolean;\n include_members?: boolean;\n}\n\nexport interface WorkspacePresenceResolvedAsset {\n id: string;\n title: string;\n asset_type: string;\n}\n\nexport interface WorkspacePresenceResolvedSession {\n thread_id: string;\n asset_id: string;\n title: string;\n}\n\nexport interface WorkspacePresenceResolvedProject {\n id: string;\n title: string;\n assets: WorkspacePresenceResolvedAsset[];\n}\n\nexport interface WorkspacePresenceResolvedMember {\n id: string;\n name: string;\n email: string;\n avatar_url: string | null;\n}\n\nexport interface WorkspacePresenceResolveResponse {\n assets: WorkspacePresenceResolvedAsset[];\n sessions: WorkspacePresenceResolvedSession[];\n projects: WorkspacePresenceResolvedProject[];\n members: WorkspacePresenceResolvedMember[];\n}\n\nfunction workspaceUrl({\n auth,\n workspaceId,\n path,\n}: {\n auth: CollabAuth;\n workspaceId: string;\n path: string;\n}): string {\n const base = getAthenaApiBaseUrl(auth.backendUrl);\n return `${base}/api/v0/workspaces/${encodeURIComponent(workspaceId)}/${path}`;\n}\n\n/**\n * Mint a read-only token for the workspace awareness room.\n *\n * Rejects with `CollabTokenError` on 401 (bad credential) and 403 (admin\n * required, roster not enabled for this workspace, or a sandbox credential\n * that may not read presence), carrying the server's `detail`.\n */\nexport async function mintWorkspacePresenceToken(args: {\n auth: CollabAuth;\n workspaceId: string;\n signal?: AbortSignal;\n}): Promise<WorkspacePresenceTokenResponse> {\n const response = await fetch(\n workspaceUrl({ auth: args.auth, workspaceId: args.workspaceId, path: 'presence-token' }),\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...getCollabAuthHeaders(args.auth),\n },\n body: JSON.stringify({}),\n signal: args.signal,\n }\n );\n if (!response.ok) {\n throw await collabErrorFromResponse(\n response,\n `Workspace presence token mint failed with status ${response.status}`\n );\n }\n return (await response.json()) as WorkspacePresenceTokenResponse;\n}\n\nfunction arrayOrEmpty<T>(value: T[] | undefined | null): T[] {\n return Array.isArray(value) ? value : [];\n}\n\n/**\n * Resolve awareness ids to titles. Every section of the response is always\n * present on the returned object (an omitted section reads as empty), so\n * callers can index without guarding. Failures reject with `CollabTokenError`\n * — the same status + detail shape as the mint.\n */\nexport async function resolveWorkspacePresence(args: {\n auth: CollabAuth;\n workspaceId: string;\n request: WorkspacePresenceResolveRequest;\n signal?: AbortSignal;\n}): Promise<WorkspacePresenceResolveResponse> {\n const response = await fetch(\n workspaceUrl({ auth: args.auth, workspaceId: args.workspaceId, path: 'presence/resolve' }),\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...getCollabAuthHeaders(args.auth),\n },\n body: JSON.stringify(args.request),\n signal: args.signal,\n }\n );\n if (!response.ok) {\n throw await collabErrorFromResponse(\n response,\n `Workspace presence resolve failed with status ${response.status}`\n );\n }\n const body = (await response.json()) as Partial<WorkspacePresenceResolveResponse> | null;\n if (!body || typeof body !== 'object') {\n throw new CollabTokenError(response.status, 'Workspace presence resolve returned no body');\n }\n return {\n assets: arrayOrEmpty(body.assets),\n sessions: arrayOrEmpty(body.sessions),\n projects: arrayOrEmpty(body.projects),\n members: arrayOrEmpty(body.members),\n };\n}\n"],"names":["getAthenaApiBaseUrl","getCollabAuthHeaders","collabErrorFromResponse","CollabTokenError"],"mappings":";;;AA8EA,SAAS,aAAa;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AACF,GAIW;AACT,QAAM,OAAOA,KAAAA,oBAAoB,KAAK,UAAU;AAChD,SAAO,GAAG,IAAI,sBAAsB,mBAAmB,WAAW,CAAC,IAAI,IAAI;AAC7E;AASA,eAAsB,2BAA2B,MAIL;AAC1C,QAAM,WAAW,MAAM;AAAA,IACrB,aAAa,EAAE,MAAM,KAAK,MAAM,aAAa,KAAK,aAAa,MAAM,kBAAkB;AAAA,IACvF;AAAA,MACE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,GAAGC,KAAAA,qBAAqB,KAAK,IAAI;AAAA,MAAA;AAAA,MAEnC,MAAM,KAAK,UAAU,EAAE;AAAA,MACvB,QAAQ,KAAK;AAAA,IAAA;AAAA,EACf;AAEF,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,MAAMC,KAAAA;AAAAA,MACV;AAAA,MACA,oDAAoD,SAAS,MAAM;AAAA,IAAA;AAAA,EAEvE;AACA,SAAQ,MAAM,SAAS,KAAA;AACzB;AAEA,SAAS,aAAgB,OAAoC;AAC3D,SAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAA;AACxC;AAQA,eAAsB,yBAAyB,MAKD;AAC5C,QAAM,WAAW,MAAM;AAAA,IACrB,aAAa,EAAE,MAAM,KAAK,MAAM,aAAa,KAAK,aAAa,MAAM,oBAAoB;AAAA,IACzF;AAAA,MACE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,GAAGD,KAAAA,qBAAqB,KAAK,IAAI;AAAA,MAAA;AAAA,MAEnC,MAAM,KAAK,UAAU,KAAK,OAAO;AAAA,MACjC,QAAQ,KAAK;AAAA,IAAA;AAAA,EACf;AAEF,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,MAAMC,KAAAA;AAAAA,MACV;AAAA,MACA,iDAAiD,SAAS,MAAM;AAAA,IAAA;AAAA,EAEpE;AACA,QAAM,OAAQ,MAAM,SAAS,KAAA;AAC7B,MAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;AACrC,UAAM,IAAIC,KAAAA,iBAAiB,SAAS,QAAQ,6CAA6C;AAAA,EAC3F;AACA,SAAO;AAAA,IACL,QAAQ,aAAa,KAAK,MAAM;AAAA,IAChC,UAAU,aAAa,KAAK,QAAQ;AAAA,IACpC,UAAU,aAAa,KAAK,QAAQ;AAAA,IACpC,SAAS,aAAa,KAAK,OAAO;AAAA,EAAA;AAEtC;;;"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public-API clients for the workspace presence feed.
|
|
3
|
+
*
|
|
4
|
+
* Two endpoints, both admin-only and both under the workspace:
|
|
5
|
+
*
|
|
6
|
+
* - `POST /api/v0/workspaces/{workspaceId}/presence-token` mints a
|
|
7
|
+
* **read-only** Keryx token for the workspace awareness room
|
|
8
|
+
* (`<workspaceId>/awareness`) — the same per-recipient-filtered presence
|
|
9
|
+
* feed the Olympus People page renders. The holder can receive presence and
|
|
10
|
+
* broadcast nothing; `connectWorkspacePresence` re-mints before expiry.
|
|
11
|
+
* - `POST /api/v0/workspaces/{workspaceId}/presence/resolve` turns the opaque
|
|
12
|
+
* ids carried in awareness contexts (asset ids, session thread ids, project
|
|
13
|
+
* ids) into titles the caller may see. Only things the caller may open come
|
|
14
|
+
* back; missing ids are simply absent.
|
|
15
|
+
*
|
|
16
|
+
* Wire shapes are snake_case, matching `CollabTokenResponse`.
|
|
17
|
+
*/
|
|
18
|
+
import { type CollabAuth } from './mint';
|
|
19
|
+
/** Grant for the workspace awareness room. Always read-only (`access_type: 'r'`). */
|
|
20
|
+
export interface WorkspacePresenceTokenResponse {
|
|
21
|
+
token: string;
|
|
22
|
+
access_type: 'r' | 'rw';
|
|
23
|
+
expires_at_ms: number;
|
|
24
|
+
ws_url: string;
|
|
25
|
+
org: string;
|
|
26
|
+
doc_id: string;
|
|
27
|
+
branch: string;
|
|
28
|
+
}
|
|
29
|
+
export interface WorkspacePresenceResolveRequest {
|
|
30
|
+
asset_ids?: string[];
|
|
31
|
+
/** Thread ids, as published in `session` awareness contexts. */
|
|
32
|
+
session_ids?: string[];
|
|
33
|
+
project_ids?: string[];
|
|
34
|
+
include_projects?: boolean;
|
|
35
|
+
include_members?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface WorkspacePresenceResolvedAsset {
|
|
38
|
+
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
asset_type: string;
|
|
41
|
+
}
|
|
42
|
+
export interface WorkspacePresenceResolvedSession {
|
|
43
|
+
thread_id: string;
|
|
44
|
+
asset_id: string;
|
|
45
|
+
title: string;
|
|
46
|
+
}
|
|
47
|
+
export interface WorkspacePresenceResolvedProject {
|
|
48
|
+
id: string;
|
|
49
|
+
title: string;
|
|
50
|
+
assets: WorkspacePresenceResolvedAsset[];
|
|
51
|
+
}
|
|
52
|
+
export interface WorkspacePresenceResolvedMember {
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
email: string;
|
|
56
|
+
avatar_url: string | null;
|
|
57
|
+
}
|
|
58
|
+
export interface WorkspacePresenceResolveResponse {
|
|
59
|
+
assets: WorkspacePresenceResolvedAsset[];
|
|
60
|
+
sessions: WorkspacePresenceResolvedSession[];
|
|
61
|
+
projects: WorkspacePresenceResolvedProject[];
|
|
62
|
+
members: WorkspacePresenceResolvedMember[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Mint a read-only token for the workspace awareness room.
|
|
66
|
+
*
|
|
67
|
+
* Rejects with `CollabTokenError` on 401 (bad credential) and 403 (admin
|
|
68
|
+
* required, roster not enabled for this workspace, or a sandbox credential
|
|
69
|
+
* that may not read presence), carrying the server's `detail`.
|
|
70
|
+
*/
|
|
71
|
+
export declare function mintWorkspacePresenceToken(args: {
|
|
72
|
+
auth: CollabAuth;
|
|
73
|
+
workspaceId: string;
|
|
74
|
+
signal?: AbortSignal;
|
|
75
|
+
}): Promise<WorkspacePresenceTokenResponse>;
|
|
76
|
+
/**
|
|
77
|
+
* Resolve awareness ids to titles. Every section of the response is always
|
|
78
|
+
* present on the returned object (an omitted section reads as empty), so
|
|
79
|
+
* callers can index without guarding. Failures reject with `CollabTokenError`
|
|
80
|
+
* — the same status + detail shape as the mint.
|
|
81
|
+
*/
|
|
82
|
+
export declare function resolveWorkspacePresence(args: {
|
|
83
|
+
auth: CollabAuth;
|
|
84
|
+
workspaceId: string;
|
|
85
|
+
request: WorkspacePresenceResolveRequest;
|
|
86
|
+
signal?: AbortSignal;
|
|
87
|
+
}): Promise<WorkspacePresenceResolveResponse>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { getCollabAuthHeaders, collabErrorFromResponse, CollabTokenError, getAthenaApiBaseUrl } from "./mint.js";
|
|
2
|
+
function workspaceUrl({
|
|
3
|
+
auth,
|
|
4
|
+
workspaceId,
|
|
5
|
+
path
|
|
6
|
+
}) {
|
|
7
|
+
const base = getAthenaApiBaseUrl(auth.backendUrl);
|
|
8
|
+
return `${base}/api/v0/workspaces/${encodeURIComponent(workspaceId)}/${path}`;
|
|
9
|
+
}
|
|
10
|
+
async function mintWorkspacePresenceToken(args) {
|
|
11
|
+
const response = await fetch(
|
|
12
|
+
workspaceUrl({ auth: args.auth, workspaceId: args.workspaceId, path: "presence-token" }),
|
|
13
|
+
{
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: {
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
...getCollabAuthHeaders(args.auth)
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify({}),
|
|
20
|
+
signal: args.signal
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw await collabErrorFromResponse(
|
|
25
|
+
response,
|
|
26
|
+
`Workspace presence token mint failed with status ${response.status}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return await response.json();
|
|
30
|
+
}
|
|
31
|
+
function arrayOrEmpty(value) {
|
|
32
|
+
return Array.isArray(value) ? value : [];
|
|
33
|
+
}
|
|
34
|
+
async function resolveWorkspacePresence(args) {
|
|
35
|
+
const response = await fetch(
|
|
36
|
+
workspaceUrl({ auth: args.auth, workspaceId: args.workspaceId, path: "presence/resolve" }),
|
|
37
|
+
{
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: {
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
...getCollabAuthHeaders(args.auth)
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify(args.request),
|
|
44
|
+
signal: args.signal
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw await collabErrorFromResponse(
|
|
49
|
+
response,
|
|
50
|
+
`Workspace presence resolve failed with status ${response.status}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const body = await response.json();
|
|
54
|
+
if (!body || typeof body !== "object") {
|
|
55
|
+
throw new CollabTokenError(response.status, "Workspace presence resolve returned no body");
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
assets: arrayOrEmpty(body.assets),
|
|
59
|
+
sessions: arrayOrEmpty(body.sessions),
|
|
60
|
+
projects: arrayOrEmpty(body.projects),
|
|
61
|
+
members: arrayOrEmpty(body.members)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
mintWorkspacePresenceToken,
|
|
66
|
+
resolveWorkspacePresence
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=presence-mint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presence-mint.js","sources":["../../src/collab/presence-mint.ts"],"sourcesContent":["/**\n * Public-API clients for the workspace presence feed.\n *\n * Two endpoints, both admin-only and both under the workspace:\n *\n * - `POST /api/v0/workspaces/{workspaceId}/presence-token` mints a\n * **read-only** Keryx token for the workspace awareness room\n * (`<workspaceId>/awareness`) — the same per-recipient-filtered presence\n * feed the Olympus People page renders. The holder can receive presence and\n * broadcast nothing; `connectWorkspacePresence` re-mints before expiry.\n * - `POST /api/v0/workspaces/{workspaceId}/presence/resolve` turns the opaque\n * ids carried in awareness contexts (asset ids, session thread ids, project\n * ids) into titles the caller may see. Only things the caller may open come\n * back; missing ids are simply absent.\n *\n * Wire shapes are snake_case, matching `CollabTokenResponse`.\n */\n\nimport {\n type CollabAuth,\n CollabTokenError,\n collabErrorFromResponse,\n getAthenaApiBaseUrl,\n getCollabAuthHeaders,\n} from './mint';\n\n/** Grant for the workspace awareness room. Always read-only (`access_type: 'r'`). */\nexport interface WorkspacePresenceTokenResponse {\n token: string;\n access_type: 'r' | 'rw';\n expires_at_ms: number;\n ws_url: string;\n org: string;\n doc_id: string;\n branch: string;\n}\n\nexport interface WorkspacePresenceResolveRequest {\n asset_ids?: string[];\n /** Thread ids, as published in `session` awareness contexts. */\n session_ids?: string[];\n project_ids?: string[];\n include_projects?: boolean;\n include_members?: boolean;\n}\n\nexport interface WorkspacePresenceResolvedAsset {\n id: string;\n title: string;\n asset_type: string;\n}\n\nexport interface WorkspacePresenceResolvedSession {\n thread_id: string;\n asset_id: string;\n title: string;\n}\n\nexport interface WorkspacePresenceResolvedProject {\n id: string;\n title: string;\n assets: WorkspacePresenceResolvedAsset[];\n}\n\nexport interface WorkspacePresenceResolvedMember {\n id: string;\n name: string;\n email: string;\n avatar_url: string | null;\n}\n\nexport interface WorkspacePresenceResolveResponse {\n assets: WorkspacePresenceResolvedAsset[];\n sessions: WorkspacePresenceResolvedSession[];\n projects: WorkspacePresenceResolvedProject[];\n members: WorkspacePresenceResolvedMember[];\n}\n\nfunction workspaceUrl({\n auth,\n workspaceId,\n path,\n}: {\n auth: CollabAuth;\n workspaceId: string;\n path: string;\n}): string {\n const base = getAthenaApiBaseUrl(auth.backendUrl);\n return `${base}/api/v0/workspaces/${encodeURIComponent(workspaceId)}/${path}`;\n}\n\n/**\n * Mint a read-only token for the workspace awareness room.\n *\n * Rejects with `CollabTokenError` on 401 (bad credential) and 403 (admin\n * required, roster not enabled for this workspace, or a sandbox credential\n * that may not read presence), carrying the server's `detail`.\n */\nexport async function mintWorkspacePresenceToken(args: {\n auth: CollabAuth;\n workspaceId: string;\n signal?: AbortSignal;\n}): Promise<WorkspacePresenceTokenResponse> {\n const response = await fetch(\n workspaceUrl({ auth: args.auth, workspaceId: args.workspaceId, path: 'presence-token' }),\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...getCollabAuthHeaders(args.auth),\n },\n body: JSON.stringify({}),\n signal: args.signal,\n }\n );\n if (!response.ok) {\n throw await collabErrorFromResponse(\n response,\n `Workspace presence token mint failed with status ${response.status}`\n );\n }\n return (await response.json()) as WorkspacePresenceTokenResponse;\n}\n\nfunction arrayOrEmpty<T>(value: T[] | undefined | null): T[] {\n return Array.isArray(value) ? value : [];\n}\n\n/**\n * Resolve awareness ids to titles. Every section of the response is always\n * present on the returned object (an omitted section reads as empty), so\n * callers can index without guarding. Failures reject with `CollabTokenError`\n * — the same status + detail shape as the mint.\n */\nexport async function resolveWorkspacePresence(args: {\n auth: CollabAuth;\n workspaceId: string;\n request: WorkspacePresenceResolveRequest;\n signal?: AbortSignal;\n}): Promise<WorkspacePresenceResolveResponse> {\n const response = await fetch(\n workspaceUrl({ auth: args.auth, workspaceId: args.workspaceId, path: 'presence/resolve' }),\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...getCollabAuthHeaders(args.auth),\n },\n body: JSON.stringify(args.request),\n signal: args.signal,\n }\n );\n if (!response.ok) {\n throw await collabErrorFromResponse(\n response,\n `Workspace presence resolve failed with status ${response.status}`\n );\n }\n const body = (await response.json()) as Partial<WorkspacePresenceResolveResponse> | null;\n if (!body || typeof body !== 'object') {\n throw new CollabTokenError(response.status, 'Workspace presence resolve returned no body');\n }\n return {\n assets: arrayOrEmpty(body.assets),\n sessions: arrayOrEmpty(body.sessions),\n projects: arrayOrEmpty(body.projects),\n members: arrayOrEmpty(body.members),\n };\n}\n"],"names":[],"mappings":";AA8EA,SAAS,aAAa;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AACF,GAIW;AACT,QAAM,OAAO,oBAAoB,KAAK,UAAU;AAChD,SAAO,GAAG,IAAI,sBAAsB,mBAAmB,WAAW,CAAC,IAAI,IAAI;AAC7E;AASA,eAAsB,2BAA2B,MAIL;AAC1C,QAAM,WAAW,MAAM;AAAA,IACrB,aAAa,EAAE,MAAM,KAAK,MAAM,aAAa,KAAK,aAAa,MAAM,kBAAkB;AAAA,IACvF;AAAA,MACE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,GAAG,qBAAqB,KAAK,IAAI;AAAA,MAAA;AAAA,MAEnC,MAAM,KAAK,UAAU,EAAE;AAAA,MACvB,QAAQ,KAAK;AAAA,IAAA;AAAA,EACf;AAEF,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,MAAM;AAAA,MACV;AAAA,MACA,oDAAoD,SAAS,MAAM;AAAA,IAAA;AAAA,EAEvE;AACA,SAAQ,MAAM,SAAS,KAAA;AACzB;AAEA,SAAS,aAAgB,OAAoC;AAC3D,SAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAA;AACxC;AAQA,eAAsB,yBAAyB,MAKD;AAC5C,QAAM,WAAW,MAAM;AAAA,IACrB,aAAa,EAAE,MAAM,KAAK,MAAM,aAAa,KAAK,aAAa,MAAM,oBAAoB;AAAA,IACzF;AAAA,MACE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,GAAG,qBAAqB,KAAK,IAAI;AAAA,MAAA;AAAA,MAEnC,MAAM,KAAK,UAAU,KAAK,OAAO;AAAA,MACjC,QAAQ,KAAK;AAAA,IAAA;AAAA,EACf;AAEF,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,MAAM;AAAA,MACV;AAAA,MACA,iDAAiD,SAAS,MAAM;AAAA,IAAA;AAAA,EAEpE;AACA,QAAM,OAAQ,MAAM,SAAS,KAAA;AAC7B,MAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;AACrC,UAAM,IAAI,iBAAiB,SAAS,QAAQ,6CAA6C;AAAA,EAC3F;AACA,SAAO;AAAA,IACL,QAAQ,aAAa,KAAK,MAAM;AAAA,IAChC,UAAU,aAAa,KAAK,QAAQ;AAAA,IACpC,UAAU,aAAa,KAAK,QAAQ;AAAA,IACpC,SAAS,aAAa,KAAK,OAAO;AAAA,EAAA;AAEtC;"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const react = require("react");
|
|
4
|
+
const AthenaContext = require("../provider/AthenaContext.cjs");
|
|
5
|
+
const presenceClient = require("./presence-client.cjs");
|
|
6
|
+
const presenceMint = require("./presence-mint.cjs");
|
|
7
|
+
const presenceSlots = require("./presence-slots.cjs");
|
|
8
|
+
const PRESENCE_DEFAULT_TICK_MS = 1e4;
|
|
9
|
+
const EMPTY_SNAPSHOT = "init||0";
|
|
10
|
+
const NO_HUMANS = [];
|
|
11
|
+
const NO_AGENTS = [];
|
|
12
|
+
function useWorkspacePresence(workspaceId, options) {
|
|
13
|
+
const config = AthenaContext.useAthenaConfig();
|
|
14
|
+
const enabled = (options == null ? void 0 : options.enabled) !== false && workspaceId !== null;
|
|
15
|
+
const tickMs = (options == null ? void 0 : options.tickMs) ?? PRESENCE_DEFAULT_TICK_MS;
|
|
16
|
+
const [entry, setEntry] = react.useState(null);
|
|
17
|
+
const handleRef = react.useRef(null);
|
|
18
|
+
react.useEffect(() => {
|
|
19
|
+
if (!enabled || workspaceId === null) {
|
|
20
|
+
setEntry(null);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const next = presenceClient.connectWorkspacePresence({
|
|
24
|
+
workspaceId,
|
|
25
|
+
auth: {
|
|
26
|
+
backendUrl: config.backendUrl,
|
|
27
|
+
apiKey: config.apiKey,
|
|
28
|
+
token: config.token
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
handleRef.current = next;
|
|
32
|
+
setEntry({ workspaceId, handle: next });
|
|
33
|
+
return () => {
|
|
34
|
+
handleRef.current = null;
|
|
35
|
+
next.destroy();
|
|
36
|
+
};
|
|
37
|
+
}, [enabled, workspaceId, config.backendUrl, config.apiKey, config.token]);
|
|
38
|
+
const handle = enabled && entry !== null && entry.workspaceId === workspaceId ? entry.handle : null;
|
|
39
|
+
const subscribe = react.useMemo(() => {
|
|
40
|
+
return (onStoreChange) => {
|
|
41
|
+
if (!handle) return () => {
|
|
42
|
+
};
|
|
43
|
+
return handle.onChange(onStoreChange);
|
|
44
|
+
};
|
|
45
|
+
}, [handle]);
|
|
46
|
+
react.useSyncExternalStore(
|
|
47
|
+
subscribe,
|
|
48
|
+
() => {
|
|
49
|
+
if (!handle) return EMPTY_SNAPSHOT;
|
|
50
|
+
return `${handle.status}|${handle.accessType ?? ""}|${handle.version}`;
|
|
51
|
+
},
|
|
52
|
+
() => EMPTY_SNAPSHOT
|
|
53
|
+
);
|
|
54
|
+
const [now, setNow] = react.useState(() => Date.now());
|
|
55
|
+
react.useEffect(() => {
|
|
56
|
+
if (!enabled) return;
|
|
57
|
+
setNow(Date.now());
|
|
58
|
+
const timer = setInterval(() => {
|
|
59
|
+
setNow(Date.now());
|
|
60
|
+
}, tickMs);
|
|
61
|
+
return () => {
|
|
62
|
+
clearInterval(timer);
|
|
63
|
+
};
|
|
64
|
+
}, [enabled, tickMs]);
|
|
65
|
+
const rosterHumans = (handle == null ? void 0 : handle.humans) ?? NO_HUMANS;
|
|
66
|
+
const humans = react.useMemo(
|
|
67
|
+
() => rosterHumans.map((human) => ({
|
|
68
|
+
...human,
|
|
69
|
+
state: presenceSlots.derivePresenceState({
|
|
70
|
+
lastIntentAt: human.lastIntentAt,
|
|
71
|
+
hidden: human.hidden,
|
|
72
|
+
manualAway: human.manualAway,
|
|
73
|
+
now
|
|
74
|
+
})
|
|
75
|
+
})),
|
|
76
|
+
[rosterHumans, now]
|
|
77
|
+
);
|
|
78
|
+
const refreshNow = react.useCallback(() => {
|
|
79
|
+
var _a;
|
|
80
|
+
return ((_a = handleRef.current) == null ? void 0 : _a.refreshNow()) ?? Promise.resolve();
|
|
81
|
+
}, []);
|
|
82
|
+
return {
|
|
83
|
+
status: handle ? handle.status : enabled ? "connecting" : "closed",
|
|
84
|
+
error: (handle == null ? void 0 : handle.error) ?? null,
|
|
85
|
+
accessType: (handle == null ? void 0 : handle.accessType) ?? null,
|
|
86
|
+
humans,
|
|
87
|
+
agents: (handle == null ? void 0 : handle.agents) ?? NO_AGENTS,
|
|
88
|
+
refreshNow
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function useWorkspacePresenceResolver(workspaceId) {
|
|
92
|
+
const config = AthenaContext.useAthenaConfig();
|
|
93
|
+
const { backendUrl, apiKey, token } = config;
|
|
94
|
+
return react.useCallback(
|
|
95
|
+
(request, signal) => {
|
|
96
|
+
if (workspaceId === null) {
|
|
97
|
+
return Promise.reject(
|
|
98
|
+
new Error("[AthenaSDK] useWorkspacePresenceResolver: no workspace selected")
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return presenceMint.resolveWorkspacePresence({
|
|
102
|
+
auth: { backendUrl, apiKey, token },
|
|
103
|
+
workspaceId,
|
|
104
|
+
request,
|
|
105
|
+
signal
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
[workspaceId, backendUrl, apiKey, token]
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
exports.PRESENCE_DEFAULT_TICK_MS = PRESENCE_DEFAULT_TICK_MS;
|
|
112
|
+
exports.useWorkspacePresence = useWorkspacePresence;
|
|
113
|
+
exports.useWorkspacePresenceResolver = useWorkspacePresenceResolver;
|
|
114
|
+
//# sourceMappingURL=presence-react.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presence-react.cjs","sources":["../../src/collab/presence-react.ts"],"sourcesContent":["/**\n * React bindings for the workspace presence feed.\n *\n * `useWorkspacePresence(workspaceId)` opens (and owns) a\n * `connectWorkspacePresence` handle, resolving auth from the surrounding\n * `AthenaProvider` (per-viewer bearer when present, API key otherwise), and\n * re-renders on status/roster changes via `useSyncExternalStore`. Each person\n * carries a `state` on the intent ladder, recomputed against a clock that\n * ticks every `tickMs` so `active` decays to `idle` and `away` between\n * awareness events.\n *\n * `useWorkspacePresenceResolver(workspaceId)` returns the batch id→title\n * resolver bound to the same auth. It is deliberately thin: callers own\n * caching (TanStack Query is the expected home for it).\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport {\n type WorkspacePresenceHandle,\n type WorkspacePresenceStatus,\n connectWorkspacePresence,\n} from './presence-client';\nimport {\n type WorkspacePresenceResolveRequest,\n type WorkspacePresenceResolveResponse,\n resolveWorkspacePresence,\n} from './presence-mint';\nimport {\n type PresenceAgent,\n type PresenceHuman,\n type PresenceState,\n derivePresenceState,\n} from './presence-slots';\n\n/** Default cadence for re-deriving each person's intent state. */\nexport const PRESENCE_DEFAULT_TICK_MS = 10_000;\n\nexport interface PresenceHumanWithState extends PresenceHuman {\n /** Where the person sits on the intent ladder as of the last tick. */\n state: PresenceState;\n}\n\nexport interface UseWorkspacePresenceOptions {\n /** `false` closes the feed without unmounting the caller. Default `true`. */\n enabled?: boolean;\n /** How often `state` is re-derived from `lastIntentAt`. Default 10 s. */\n tickMs?: number;\n}\n\nexport interface UseWorkspacePresenceResult {\n /** `'closed'` while disabled or without a workspace; `'connecting'` until the first mint resolves. */\n status: WorkspacePresenceStatus;\n error: Error | null;\n accessType: 'r' | 'rw' | null;\n humans: PresenceHumanWithState[];\n agents: PresenceAgent[];\n /** Force an early token refresh; resolves immediately when no feed is open. */\n refreshNow: () => Promise<void>;\n}\n\nconst EMPTY_SNAPSHOT = 'init||0';\nconst NO_HUMANS: PresenceHuman[] = [];\nconst NO_AGENTS: PresenceAgent[] = [];\n\nexport function useWorkspacePresence(\n workspaceId: string | null,\n options?: UseWorkspacePresenceOptions\n): UseWorkspacePresenceResult {\n const config = useAthenaConfig();\n const enabled = options?.enabled !== false && workspaceId !== null;\n const tickMs = options?.tickMs ?? PRESENCE_DEFAULT_TICK_MS;\n const [entry, setEntry] = useState<{\n workspaceId: string;\n handle: WorkspacePresenceHandle;\n } | null>(null);\n const handleRef = useRef<WorkspacePresenceHandle | null>(null);\n\n useEffect(() => {\n if (!enabled || workspaceId === null) {\n setEntry(null);\n return;\n }\n const next = connectWorkspacePresence({\n workspaceId,\n auth: {\n backendUrl: config.backendUrl,\n apiKey: config.apiKey,\n token: config.token,\n },\n });\n handleRef.current = next;\n setEntry({ workspaceId, handle: next });\n return () => {\n handleRef.current = null;\n next.destroy();\n };\n // Recreate when the target or credentials change; the handle re-mints on\n // its own schedule otherwise.\n }, [enabled, workspaceId, config.backendUrl, config.apiKey, config.token]);\n\n // React renders with the new arguments before the effect above runs, so a\n // handle opened for a previous workspace, or before the feed was disabled,\n // still sits in state for that one render. Expose it only while it is the\n // handle this render asked for; otherwise the closed / connecting empty\n // result wins, and a disabled feed never leaks a roster.\n const handle =\n enabled && entry !== null && entry.workspaceId === workspaceId ? entry.handle : null;\n\n const subscribe = useMemo(() => {\n return (onStoreChange: () => void) => {\n if (!handle) return () => {};\n return handle.onChange(onStoreChange);\n };\n }, [handle]);\n\n const snapshot = useSyncExternalStore(\n subscribe,\n () => {\n if (!handle) return EMPTY_SNAPSHOT;\n return `${handle.status}|${handle.accessType ?? ''}|${handle.version}`;\n },\n () => EMPTY_SNAPSHOT\n );\n void snapshot;\n\n // The ladder decays with wall-clock time, not with awareness events; a\n // person who stops typing must drift to idle even when nothing arrives.\n const [now, setNow] = useState(() => Date.now());\n useEffect(() => {\n if (!enabled) return;\n setNow(Date.now());\n const timer = setInterval(() => {\n setNow(Date.now());\n }, tickMs);\n return () => {\n clearInterval(timer);\n };\n }, [enabled, tickMs]);\n\n const rosterHumans = handle?.humans ?? NO_HUMANS;\n const humans = useMemo(\n () =>\n rosterHumans.map((human) => ({\n ...human,\n state: derivePresenceState({\n lastIntentAt: human.lastIntentAt,\n hidden: human.hidden,\n manualAway: human.manualAway,\n now,\n }),\n })),\n [rosterHumans, now]\n );\n\n const refreshNow = useCallback(() => handleRef.current?.refreshNow() ?? Promise.resolve(), []);\n\n return {\n status: handle ? handle.status : enabled ? 'connecting' : 'closed',\n error: handle?.error ?? null,\n accessType: handle?.accessType ?? null,\n humans,\n agents: handle?.agents ?? NO_AGENTS,\n refreshNow,\n };\n}\n\nexport type WorkspacePresenceResolver = (\n request: WorkspacePresenceResolveRequest,\n signal?: AbortSignal\n) => Promise<WorkspacePresenceResolveResponse>;\n\n/**\n * A stable resolver bound to the provider's auth and the given workspace.\n * Rejects when called without a workspace; no caching — callers own that.\n */\nexport function useWorkspacePresenceResolver(\n workspaceId: string | null\n): WorkspacePresenceResolver {\n const config = useAthenaConfig();\n const { backendUrl, apiKey, token } = config;\n return useCallback<WorkspacePresenceResolver>(\n (request, signal) => {\n if (workspaceId === null) {\n return Promise.reject(\n new Error('[AthenaSDK] useWorkspacePresenceResolver: no workspace selected')\n );\n }\n return resolveWorkspacePresence({\n auth: { backendUrl, apiKey, token },\n workspaceId,\n request,\n signal,\n });\n },\n [workspaceId, backendUrl, apiKey, token]\n );\n}\n"],"names":["useAthenaConfig","useState","useRef","useEffect","connectWorkspacePresence","useMemo","useSyncExternalStore","derivePresenceState","useCallback","resolveWorkspacePresence"],"mappings":"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"}
|