@athenaintel/react 0.21.2 → 0.22.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 +67 -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/dist/styles.css +1 -1
- package/package.json +7 -7
package/dist/collab/index.js
CHANGED
|
@@ -1,17 +1,40 @@
|
|
|
1
1
|
import { WS_CLOSE_AUTH_REVOKED, WS_CLOSE_DOC_DELETED, classifyClose } from "./close-policy.js";
|
|
2
2
|
import { connectGenericDoc } from "./client.js";
|
|
3
3
|
import { CollabTokenError, getAthenaApiBaseUrl, mintCollabToken } from "./mint.js";
|
|
4
|
+
import { PRESENCE_SWEEP_INTERVAL_MS, connectWorkspacePresence } from "./presence-client.js";
|
|
5
|
+
import { mintWorkspacePresenceToken, resolveWorkspacePresence } from "./presence-mint.js";
|
|
6
|
+
import { PRESENCE_DEFAULT_TICK_MS, useWorkspacePresence, useWorkspacePresenceResolver } from "./presence-react.js";
|
|
7
|
+
import { MAX_PRESENCE_CONTEXTS_PER_SLOT, MAX_PRESENCE_STRING_CHARS, PRESENCE_ACTIVE_WINDOW_MS, PRESENCE_CONTEXT_TYPE, PRESENCE_IDLE_WINDOW_MS, derivePresenceState, mergePresenceStates } from "./presence-slots.js";
|
|
4
8
|
import { useGenericDoc } from "./react.js";
|
|
5
9
|
import { defineDocShape } from "./shape.js";
|
|
10
|
+
import { MAX_CONSECUTIVE_REMINTS, MIN_REFRESH_DELAY_MS, REFRESH_HEADROOM_MS, refreshDelayMs } from "./token-refresh.js";
|
|
6
11
|
export {
|
|
7
12
|
CollabTokenError,
|
|
13
|
+
MAX_CONSECUTIVE_REMINTS,
|
|
14
|
+
MAX_PRESENCE_CONTEXTS_PER_SLOT,
|
|
15
|
+
MAX_PRESENCE_STRING_CHARS,
|
|
16
|
+
MIN_REFRESH_DELAY_MS,
|
|
17
|
+
PRESENCE_ACTIVE_WINDOW_MS,
|
|
18
|
+
PRESENCE_CONTEXT_TYPE,
|
|
19
|
+
PRESENCE_DEFAULT_TICK_MS,
|
|
20
|
+
PRESENCE_IDLE_WINDOW_MS,
|
|
21
|
+
PRESENCE_SWEEP_INTERVAL_MS,
|
|
22
|
+
REFRESH_HEADROOM_MS,
|
|
8
23
|
WS_CLOSE_AUTH_REVOKED,
|
|
9
24
|
WS_CLOSE_DOC_DELETED,
|
|
10
25
|
classifyClose,
|
|
11
26
|
connectGenericDoc,
|
|
27
|
+
connectWorkspacePresence,
|
|
12
28
|
defineDocShape,
|
|
29
|
+
derivePresenceState,
|
|
13
30
|
getAthenaApiBaseUrl,
|
|
31
|
+
mergePresenceStates,
|
|
14
32
|
mintCollabToken,
|
|
15
|
-
|
|
33
|
+
mintWorkspacePresenceToken,
|
|
34
|
+
refreshDelayMs,
|
|
35
|
+
resolveWorkspacePresence,
|
|
36
|
+
useGenericDoc,
|
|
37
|
+
useWorkspacePresence,
|
|
38
|
+
useWorkspacePresenceResolver
|
|
16
39
|
};
|
|
17
40
|
//# sourceMappingURL=index.js.map
|
package/dist/collab/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
|
package/dist/collab/mint.cjs
CHANGED
|
@@ -11,7 +11,7 @@ class CollabTokenError extends Error {
|
|
|
11
11
|
this.status = status;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
-
function
|
|
14
|
+
function getCollabAuthHeaders(auth) {
|
|
15
15
|
if (auth.token) {
|
|
16
16
|
return { Authorization: `Bearer ${auth.token}` };
|
|
17
17
|
}
|
|
@@ -20,6 +20,17 @@ function getAuthHeaders(auth) {
|
|
|
20
20
|
}
|
|
21
21
|
return {};
|
|
22
22
|
}
|
|
23
|
+
async function collabErrorFromResponse(response, fallback) {
|
|
24
|
+
let detail = "";
|
|
25
|
+
try {
|
|
26
|
+
const body = await response.json();
|
|
27
|
+
if (body && typeof body === "object" && "detail" in body) {
|
|
28
|
+
detail = String(body.detail);
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
return new CollabTokenError(response.status, detail || fallback);
|
|
33
|
+
}
|
|
23
34
|
function getAthenaApiBaseUrl(backendUrl) {
|
|
24
35
|
return backendUrl.replace(/\/api\/assistant-ui\/?$/, "").replace(/\/api\/v0\/?$/, "").replace(/\/$/, "");
|
|
25
36
|
}
|
|
@@ -31,29 +42,23 @@ async function mintCollabToken(args) {
|
|
|
31
42
|
method: "POST",
|
|
32
43
|
headers: {
|
|
33
44
|
"Content-Type": "application/json",
|
|
34
|
-
...
|
|
45
|
+
...getCollabAuthHeaders(args.auth)
|
|
35
46
|
},
|
|
36
47
|
body: JSON.stringify({ access: args.access }),
|
|
37
48
|
signal: args.signal
|
|
38
49
|
}
|
|
39
50
|
);
|
|
40
51
|
if (!response.ok) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (body && typeof body === "object" && "detail" in body) {
|
|
45
|
-
detail = String(body.detail);
|
|
46
|
-
}
|
|
47
|
-
} catch {
|
|
48
|
-
}
|
|
49
|
-
throw new CollabTokenError(
|
|
50
|
-
response.status,
|
|
51
|
-
detail || `Collab token mint failed with status ${response.status}`
|
|
52
|
+
throw await collabErrorFromResponse(
|
|
53
|
+
response,
|
|
54
|
+
`Collab token mint failed with status ${response.status}`
|
|
52
55
|
);
|
|
53
56
|
}
|
|
54
57
|
return await response.json();
|
|
55
58
|
}
|
|
56
59
|
exports.CollabTokenError = CollabTokenError;
|
|
60
|
+
exports.collabErrorFromResponse = collabErrorFromResponse;
|
|
57
61
|
exports.getAthenaApiBaseUrl = getAthenaApiBaseUrl;
|
|
62
|
+
exports.getCollabAuthHeaders = getCollabAuthHeaders;
|
|
58
63
|
exports.mintCollabToken = mintCollabToken;
|
|
59
64
|
//# sourceMappingURL=mint.cjs.map
|
package/dist/collab/mint.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mint.cjs","sources":["../../src/collab/mint.ts"],"sourcesContent":["/**\n * Collab-token mint client for Generic Doc assets.\n *\n * Calls `POST /api/v0/assets/{assetId}/collab-token` — the only public surface\n * that issues Keryx capability tokens, allowlisted server-side to the\n * `generic_doc` asset type and admin-only during the initial rollout. Tokens\n * are room-bound and short-lived; `connectGenericDoc` re-mints automatically\n * before expiry.\n */\n\nexport interface CollabAuth {\n /** Athena API base or any Athena backend URL (e.g. the AthenaProvider `backendUrl`). */\n backendUrl: string;\n /** Personal or sandbox API key. Used when no bearer token is provided. */\n apiKey?: string;\n /** Per-viewer bearer token. Takes precedence over the API key. */\n token?: string | null;\n}\n\nexport interface CollabTokenResponse {\n token: string;\n access_type: 'r' | 'rw';\n expires_at_ms: number;\n ws_url: string;\n rest_url: string;\n org: string;\n doc_id: string;\n branch: string;\n}\n\nexport class CollabTokenError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message);\n this.name = 'CollabTokenError';\n this.status = status;\n }\n}\n\
|
|
1
|
+
{"version":3,"file":"mint.cjs","sources":["../../src/collab/mint.ts"],"sourcesContent":["/**\n * Collab-token mint client for Generic Doc assets.\n *\n * Calls `POST /api/v0/assets/{assetId}/collab-token` — the only public surface\n * that issues Keryx capability tokens, allowlisted server-side to the\n * `generic_doc` asset type and admin-only during the initial rollout. Tokens\n * are room-bound and short-lived; `connectGenericDoc` re-mints automatically\n * before expiry.\n */\n\nexport interface CollabAuth {\n /** Athena API base or any Athena backend URL (e.g. the AthenaProvider `backendUrl`). */\n backendUrl: string;\n /** Personal or sandbox API key. Used when no bearer token is provided. */\n apiKey?: string;\n /** Per-viewer bearer token. Takes precedence over the API key. */\n token?: string | null;\n}\n\nexport interface CollabTokenResponse {\n token: string;\n access_type: 'r' | 'rw';\n expires_at_ms: number;\n ws_url: string;\n rest_url: string;\n org: string;\n doc_id: string;\n branch: string;\n}\n\nexport class CollabTokenError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message);\n this.name = 'CollabTokenError';\n this.status = status;\n }\n}\n\n/**\n * Credential headers for a public-API call. The per-viewer bearer token wins\n * over the API key so a viewer never acts with the app's own identity.\n */\nexport function getCollabAuthHeaders(auth: CollabAuth): Record<string, string> {\n if (auth.token) {\n return { Authorization: `Bearer ${auth.token}` };\n }\n if (auth.apiKey) {\n return { 'X-API-KEY': auth.apiKey };\n }\n return {};\n}\n\n/**\n * Turn a non-2xx public-API response into a `CollabTokenError`, carrying the\n * FastAPI `detail` string when the body has one and the bare status otherwise.\n */\nexport async function collabErrorFromResponse(\n response: Response,\n fallback: string\n): Promise<CollabTokenError> {\n let detail = '';\n try {\n const body: unknown = await response.json();\n if (body && typeof body === 'object' && 'detail' in body) {\n detail = String((body as { detail: unknown }).detail);\n }\n } catch {\n // non-JSON error body; status alone tells the story\n }\n return new CollabTokenError(response.status, detail || fallback);\n}\n\n/**\n * Trim a nested Athena endpoint back to the API origin. Accepts the\n * AthenaProvider `backendUrl` (`…/api/assistant-ui`), the sandbox\n * `ATHENA_API_URL` convention (`…/api/v0`), or a bare origin.\n */\nexport function getAthenaApiBaseUrl(backendUrl: string): string {\n return backendUrl\n .replace(/\\/api\\/assistant-ui\\/?$/, '')\n .replace(/\\/api\\/v0\\/?$/, '')\n .replace(/\\/$/, '');\n}\n\nexport async function mintCollabToken(args: {\n auth: CollabAuth;\n assetId: string;\n access: 'view' | 'edit';\n signal?: AbortSignal;\n}): Promise<CollabTokenResponse> {\n const base = getAthenaApiBaseUrl(args.auth.backendUrl);\n const response = await fetch(\n `${base}/api/v0/assets/${encodeURIComponent(args.assetId)}/collab-token`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...getCollabAuthHeaders(args.auth),\n },\n body: JSON.stringify({ access: args.access }),\n signal: args.signal,\n }\n );\n if (!response.ok) {\n throw await collabErrorFromResponse(\n response,\n `Collab token mint failed with status ${response.status}`\n );\n }\n return (await response.json()) as CollabTokenResponse;\n}\n"],"names":[],"mappings":";;;;;AA8BO,MAAM,yBAAyB,MAAM;AAAA,EAG1C,YAAY,QAAgB,SAAiB;AAC3C,UAAM,OAAO;AAHN;AAIP,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;AAMO,SAAS,qBAAqB,MAA0C;AAC7E,MAAI,KAAK,OAAO;AACd,WAAO,EAAE,eAAe,UAAU,KAAK,KAAK,GAAA;AAAA,EAC9C;AACA,MAAI,KAAK,QAAQ;AACf,WAAO,EAAE,aAAa,KAAK,OAAA;AAAA,EAC7B;AACA,SAAO,CAAA;AACT;AAMA,eAAsB,wBACpB,UACA,UAC2B;AAC3B,MAAI,SAAS;AACb,MAAI;AACF,UAAM,OAAgB,MAAM,SAAS,KAAA;AACrC,QAAI,QAAQ,OAAO,SAAS,YAAY,YAAY,MAAM;AACxD,eAAS,OAAQ,KAA6B,MAAM;AAAA,IACtD;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,IAAI,iBAAiB,SAAS,QAAQ,UAAU,QAAQ;AACjE;AAOO,SAAS,oBAAoB,YAA4B;AAC9D,SAAO,WACJ,QAAQ,2BAA2B,EAAE,EACrC,QAAQ,iBAAiB,EAAE,EAC3B,QAAQ,OAAO,EAAE;AACtB;AAEA,eAAsB,gBAAgB,MAKL;AAC/B,QAAM,OAAO,oBAAoB,KAAK,KAAK,UAAU;AACrD,QAAM,WAAW,MAAM;AAAA,IACrB,GAAG,IAAI,kBAAkB,mBAAmB,KAAK,OAAO,CAAC;AAAA,IACzD;AAAA,MACE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,GAAG,qBAAqB,KAAK,IAAI;AAAA,MAAA;AAAA,MAEnC,MAAM,KAAK,UAAU,EAAE,QAAQ,KAAK,QAAQ;AAAA,MAC5C,QAAQ,KAAK;AAAA,IAAA;AAAA,EACf;AAEF,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,MAAM;AAAA,MACV;AAAA,MACA,wCAAwC,SAAS,MAAM;AAAA,IAAA;AAAA,EAE3D;AACA,SAAQ,MAAM,SAAS,KAAA;AACzB;;;;;;"}
|
package/dist/collab/mint.d.ts
CHANGED
|
@@ -29,6 +29,16 @@ export declare class CollabTokenError extends Error {
|
|
|
29
29
|
readonly status: number;
|
|
30
30
|
constructor(status: number, message: string);
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Credential headers for a public-API call. The per-viewer bearer token wins
|
|
34
|
+
* over the API key so a viewer never acts with the app's own identity.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getCollabAuthHeaders(auth: CollabAuth): Record<string, string>;
|
|
37
|
+
/**
|
|
38
|
+
* Turn a non-2xx public-API response into a `CollabTokenError`, carrying the
|
|
39
|
+
* FastAPI `detail` string when the body has one and the bare status otherwise.
|
|
40
|
+
*/
|
|
41
|
+
export declare function collabErrorFromResponse(response: Response, fallback: string): Promise<CollabTokenError>;
|
|
32
42
|
/**
|
|
33
43
|
* Trim a nested Athena endpoint back to the API origin. Accepts the
|
|
34
44
|
* AthenaProvider `backendUrl` (`…/api/assistant-ui`), the sandbox
|
package/dist/collab/mint.js
CHANGED
|
@@ -9,7 +9,7 @@ class CollabTokenError extends Error {
|
|
|
9
9
|
this.status = status;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
-
function
|
|
12
|
+
function getCollabAuthHeaders(auth) {
|
|
13
13
|
if (auth.token) {
|
|
14
14
|
return { Authorization: `Bearer ${auth.token}` };
|
|
15
15
|
}
|
|
@@ -18,6 +18,17 @@ function getAuthHeaders(auth) {
|
|
|
18
18
|
}
|
|
19
19
|
return {};
|
|
20
20
|
}
|
|
21
|
+
async function collabErrorFromResponse(response, fallback) {
|
|
22
|
+
let detail = "";
|
|
23
|
+
try {
|
|
24
|
+
const body = await response.json();
|
|
25
|
+
if (body && typeof body === "object" && "detail" in body) {
|
|
26
|
+
detail = String(body.detail);
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
return new CollabTokenError(response.status, detail || fallback);
|
|
31
|
+
}
|
|
21
32
|
function getAthenaApiBaseUrl(backendUrl) {
|
|
22
33
|
return backendUrl.replace(/\/api\/assistant-ui\/?$/, "").replace(/\/api\/v0\/?$/, "").replace(/\/$/, "");
|
|
23
34
|
}
|
|
@@ -29,31 +40,25 @@ async function mintCollabToken(args) {
|
|
|
29
40
|
method: "POST",
|
|
30
41
|
headers: {
|
|
31
42
|
"Content-Type": "application/json",
|
|
32
|
-
...
|
|
43
|
+
...getCollabAuthHeaders(args.auth)
|
|
33
44
|
},
|
|
34
45
|
body: JSON.stringify({ access: args.access }),
|
|
35
46
|
signal: args.signal
|
|
36
47
|
}
|
|
37
48
|
);
|
|
38
49
|
if (!response.ok) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (body && typeof body === "object" && "detail" in body) {
|
|
43
|
-
detail = String(body.detail);
|
|
44
|
-
}
|
|
45
|
-
} catch {
|
|
46
|
-
}
|
|
47
|
-
throw new CollabTokenError(
|
|
48
|
-
response.status,
|
|
49
|
-
detail || `Collab token mint failed with status ${response.status}`
|
|
50
|
+
throw await collabErrorFromResponse(
|
|
51
|
+
response,
|
|
52
|
+
`Collab token mint failed with status ${response.status}`
|
|
50
53
|
);
|
|
51
54
|
}
|
|
52
55
|
return await response.json();
|
|
53
56
|
}
|
|
54
57
|
export {
|
|
55
58
|
CollabTokenError,
|
|
59
|
+
collabErrorFromResponse,
|
|
56
60
|
getAthenaApiBaseUrl,
|
|
61
|
+
getCollabAuthHeaders,
|
|
57
62
|
mintCollabToken
|
|
58
63
|
};
|
|
59
64
|
//# sourceMappingURL=mint.js.map
|
package/dist/collab/mint.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mint.js","sources":["../../src/collab/mint.ts"],"sourcesContent":["/**\n * Collab-token mint client for Generic Doc assets.\n *\n * Calls `POST /api/v0/assets/{assetId}/collab-token` — the only public surface\n * that issues Keryx capability tokens, allowlisted server-side to the\n * `generic_doc` asset type and admin-only during the initial rollout. Tokens\n * are room-bound and short-lived; `connectGenericDoc` re-mints automatically\n * before expiry.\n */\n\nexport interface CollabAuth {\n /** Athena API base or any Athena backend URL (e.g. the AthenaProvider `backendUrl`). */\n backendUrl: string;\n /** Personal or sandbox API key. Used when no bearer token is provided. */\n apiKey?: string;\n /** Per-viewer bearer token. Takes precedence over the API key. */\n token?: string | null;\n}\n\nexport interface CollabTokenResponse {\n token: string;\n access_type: 'r' | 'rw';\n expires_at_ms: number;\n ws_url: string;\n rest_url: string;\n org: string;\n doc_id: string;\n branch: string;\n}\n\nexport class CollabTokenError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message);\n this.name = 'CollabTokenError';\n this.status = status;\n }\n}\n\
|
|
1
|
+
{"version":3,"file":"mint.js","sources":["../../src/collab/mint.ts"],"sourcesContent":["/**\n * Collab-token mint client for Generic Doc assets.\n *\n * Calls `POST /api/v0/assets/{assetId}/collab-token` — the only public surface\n * that issues Keryx capability tokens, allowlisted server-side to the\n * `generic_doc` asset type and admin-only during the initial rollout. Tokens\n * are room-bound and short-lived; `connectGenericDoc` re-mints automatically\n * before expiry.\n */\n\nexport interface CollabAuth {\n /** Athena API base or any Athena backend URL (e.g. the AthenaProvider `backendUrl`). */\n backendUrl: string;\n /** Personal or sandbox API key. Used when no bearer token is provided. */\n apiKey?: string;\n /** Per-viewer bearer token. Takes precedence over the API key. */\n token?: string | null;\n}\n\nexport interface CollabTokenResponse {\n token: string;\n access_type: 'r' | 'rw';\n expires_at_ms: number;\n ws_url: string;\n rest_url: string;\n org: string;\n doc_id: string;\n branch: string;\n}\n\nexport class CollabTokenError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message);\n this.name = 'CollabTokenError';\n this.status = status;\n }\n}\n\n/**\n * Credential headers for a public-API call. The per-viewer bearer token wins\n * over the API key so a viewer never acts with the app's own identity.\n */\nexport function getCollabAuthHeaders(auth: CollabAuth): Record<string, string> {\n if (auth.token) {\n return { Authorization: `Bearer ${auth.token}` };\n }\n if (auth.apiKey) {\n return { 'X-API-KEY': auth.apiKey };\n }\n return {};\n}\n\n/**\n * Turn a non-2xx public-API response into a `CollabTokenError`, carrying the\n * FastAPI `detail` string when the body has one and the bare status otherwise.\n */\nexport async function collabErrorFromResponse(\n response: Response,\n fallback: string\n): Promise<CollabTokenError> {\n let detail = '';\n try {\n const body: unknown = await response.json();\n if (body && typeof body === 'object' && 'detail' in body) {\n detail = String((body as { detail: unknown }).detail);\n }\n } catch {\n // non-JSON error body; status alone tells the story\n }\n return new CollabTokenError(response.status, detail || fallback);\n}\n\n/**\n * Trim a nested Athena endpoint back to the API origin. Accepts the\n * AthenaProvider `backendUrl` (`…/api/assistant-ui`), the sandbox\n * `ATHENA_API_URL` convention (`…/api/v0`), or a bare origin.\n */\nexport function getAthenaApiBaseUrl(backendUrl: string): string {\n return backendUrl\n .replace(/\\/api\\/assistant-ui\\/?$/, '')\n .replace(/\\/api\\/v0\\/?$/, '')\n .replace(/\\/$/, '');\n}\n\nexport async function mintCollabToken(args: {\n auth: CollabAuth;\n assetId: string;\n access: 'view' | 'edit';\n signal?: AbortSignal;\n}): Promise<CollabTokenResponse> {\n const base = getAthenaApiBaseUrl(args.auth.backendUrl);\n const response = await fetch(\n `${base}/api/v0/assets/${encodeURIComponent(args.assetId)}/collab-token`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...getCollabAuthHeaders(args.auth),\n },\n body: JSON.stringify({ access: args.access }),\n signal: args.signal,\n }\n );\n if (!response.ok) {\n throw await collabErrorFromResponse(\n response,\n `Collab token mint failed with status ${response.status}`\n );\n }\n return (await response.json()) as CollabTokenResponse;\n}\n"],"names":[],"mappings":";;;AA8BO,MAAM,yBAAyB,MAAM;AAAA,EAG1C,YAAY,QAAgB,SAAiB;AAC3C,UAAM,OAAO;AAHN;AAIP,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;AAMO,SAAS,qBAAqB,MAA0C;AAC7E,MAAI,KAAK,OAAO;AACd,WAAO,EAAE,eAAe,UAAU,KAAK,KAAK,GAAA;AAAA,EAC9C;AACA,MAAI,KAAK,QAAQ;AACf,WAAO,EAAE,aAAa,KAAK,OAAA;AAAA,EAC7B;AACA,SAAO,CAAA;AACT;AAMA,eAAsB,wBACpB,UACA,UAC2B;AAC3B,MAAI,SAAS;AACb,MAAI;AACF,UAAM,OAAgB,MAAM,SAAS,KAAA;AACrC,QAAI,QAAQ,OAAO,SAAS,YAAY,YAAY,MAAM;AACxD,eAAS,OAAQ,KAA6B,MAAM;AAAA,IACtD;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,IAAI,iBAAiB,SAAS,QAAQ,UAAU,QAAQ;AACjE;AAOO,SAAS,oBAAoB,YAA4B;AAC9D,SAAO,WACJ,QAAQ,2BAA2B,EAAE,EACrC,QAAQ,iBAAiB,EAAE,EAC3B,QAAQ,OAAO,EAAE;AACtB;AAEA,eAAsB,gBAAgB,MAKL;AAC/B,QAAM,OAAO,oBAAoB,KAAK,KAAK,UAAU;AACrD,QAAM,WAAW,MAAM;AAAA,IACrB,GAAG,IAAI,kBAAkB,mBAAmB,KAAK,OAAO,CAAC;AAAA,IACzD;AAAA,MACE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,GAAG,qBAAqB,KAAK,IAAI;AAAA,MAAA;AAAA,MAEnC,MAAM,KAAK,UAAU,EAAE,QAAQ,KAAK,QAAQ;AAAA,MAC5C,QAAQ,KAAK;AAAA,IAAA;AAAA,EACf;AAEF,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,MAAM;AAAA,MACV;AAAA,MACA,wCAAwC,SAAS,MAAM;AAAA,IAAA;AAAA,EAE3D;AACA,SAAQ,MAAM,SAAS,KAAA;AACzB;"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const websocket = require("@y/websocket");
|
|
4
|
+
const Y = require("@y/y");
|
|
5
|
+
const closePolicy = require("./close-policy.cjs");
|
|
6
|
+
const presenceMint = require("./presence-mint.cjs");
|
|
7
|
+
const presenceSlots = require("./presence-slots.cjs");
|
|
8
|
+
const tokenRefresh = require("./token-refresh.cjs");
|
|
9
|
+
function _interopNamespaceDefault(e) {
|
|
10
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
11
|
+
if (e) {
|
|
12
|
+
for (const k in e) {
|
|
13
|
+
if (k !== "default") {
|
|
14
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
15
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
get: () => e[k]
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
n.default = e;
|
|
23
|
+
return Object.freeze(n);
|
|
24
|
+
}
|
|
25
|
+
const Y__namespace = /* @__PURE__ */ _interopNamespaceDefault(Y);
|
|
26
|
+
const PRESENCE_SWEEP_INTERVAL_MS = 15e3;
|
|
27
|
+
const EMPTY_ROSTER = { humans: [], agents: [] };
|
|
28
|
+
function connectWorkspacePresence(args) {
|
|
29
|
+
const doc = new Y__namespace.Doc();
|
|
30
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
31
|
+
let provider = null;
|
|
32
|
+
let refreshTimer = null;
|
|
33
|
+
let destroyed = false;
|
|
34
|
+
let status = "connecting";
|
|
35
|
+
let accessType = null;
|
|
36
|
+
let error = null;
|
|
37
|
+
let version = 0;
|
|
38
|
+
let consecutiveRemints = 0;
|
|
39
|
+
let connectSeq = 0;
|
|
40
|
+
let roster = EMPTY_ROSTER;
|
|
41
|
+
let rosterFingerprint = JSON.stringify(EMPTY_ROSTER);
|
|
42
|
+
const notify = () => {
|
|
43
|
+
version += 1;
|
|
44
|
+
for (const listener of listeners) listener();
|
|
45
|
+
};
|
|
46
|
+
const applyRoster = (next) => {
|
|
47
|
+
const fingerprint = JSON.stringify(next);
|
|
48
|
+
if (fingerprint === rosterFingerprint) return false;
|
|
49
|
+
roster = next;
|
|
50
|
+
rosterFingerprint = fingerprint;
|
|
51
|
+
return true;
|
|
52
|
+
};
|
|
53
|
+
const recomputeRoster = () => {
|
|
54
|
+
if (destroyed) return;
|
|
55
|
+
const next = provider ? presenceSlots.mergePresenceStates(provider.awareness.getStates()) : EMPTY_ROSTER;
|
|
56
|
+
if (applyRoster(next)) notify();
|
|
57
|
+
};
|
|
58
|
+
const setStatus = (next, err = null) => {
|
|
59
|
+
if (destroyed && next !== "closed") return;
|
|
60
|
+
status = next;
|
|
61
|
+
error = err;
|
|
62
|
+
if (next === "closed" || next === "error") applyRoster(EMPTY_ROSTER);
|
|
63
|
+
notify();
|
|
64
|
+
};
|
|
65
|
+
const clearRefreshTimer = () => {
|
|
66
|
+
if (refreshTimer !== null) {
|
|
67
|
+
clearTimeout(refreshTimer);
|
|
68
|
+
refreshTimer = null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const teardownProvider = () => {
|
|
72
|
+
clearRefreshTimer();
|
|
73
|
+
if (provider) {
|
|
74
|
+
const stale = provider;
|
|
75
|
+
provider = null;
|
|
76
|
+
stale.destroy();
|
|
77
|
+
stale.awareness.destroy();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const scheduleRefresh = (grant) => {
|
|
81
|
+
clearRefreshTimer();
|
|
82
|
+
const delay = tokenRefresh.refreshDelayMs({ expiresAtMs: grant.expires_at_ms, now: Date.now() });
|
|
83
|
+
refreshTimer = setTimeout(() => {
|
|
84
|
+
void connect({ scheduledRefresh: true });
|
|
85
|
+
}, delay);
|
|
86
|
+
};
|
|
87
|
+
const connect = async (opts) => {
|
|
88
|
+
if (destroyed) return;
|
|
89
|
+
const attempt = ++connectSeq;
|
|
90
|
+
const scheduledRefresh = (opts == null ? void 0 : opts.scheduledRefresh) === true;
|
|
91
|
+
if (!scheduledRefresh) {
|
|
92
|
+
teardownProvider();
|
|
93
|
+
} else {
|
|
94
|
+
clearRefreshTimer();
|
|
95
|
+
}
|
|
96
|
+
let grant;
|
|
97
|
+
try {
|
|
98
|
+
grant = await presenceMint.mintWorkspacePresenceToken({ auth: args.auth, workspaceId: args.workspaceId });
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if (destroyed || attempt !== connectSeq) return;
|
|
101
|
+
if (scheduledRefresh) {
|
|
102
|
+
refreshTimer = setTimeout(() => {
|
|
103
|
+
void connect({ scheduledRefresh: true });
|
|
104
|
+
}, tokenRefresh.MIN_REFRESH_DELAY_MS);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
setStatus("error", err instanceof Error ? err : new Error(String(err)));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (destroyed || attempt !== connectSeq) return;
|
|
111
|
+
if (scheduledRefresh) {
|
|
112
|
+
teardownProvider();
|
|
113
|
+
}
|
|
114
|
+
accessType = grant.access_type;
|
|
115
|
+
const nextProvider = new websocket.WebsocketProvider(grant.ws_url, `${grant.org}/${grant.doc_id}`, doc, {
|
|
116
|
+
params: {
|
|
117
|
+
yauth: grant.token,
|
|
118
|
+
branch: grant.branch
|
|
119
|
+
},
|
|
120
|
+
// BroadcastChannel would share this room with same-origin tabs
|
|
121
|
+
// locally, bypassing the server's per-recipient filter. Presence must
|
|
122
|
+
// round-trip the server.
|
|
123
|
+
disableBc: true
|
|
124
|
+
});
|
|
125
|
+
provider = nextProvider;
|
|
126
|
+
nextProvider.awareness.off("update", nextProvider._awarenessUpdateHandler);
|
|
127
|
+
nextProvider.awareness.setLocalState(null);
|
|
128
|
+
nextProvider.on("status", (event) => {
|
|
129
|
+
if (provider !== nextProvider) return;
|
|
130
|
+
if (event.status === "connected") {
|
|
131
|
+
consecutiveRemints = 0;
|
|
132
|
+
}
|
|
133
|
+
setStatus(event.status);
|
|
134
|
+
});
|
|
135
|
+
nextProvider.on("connection-close", (event) => {
|
|
136
|
+
if (provider !== nextProvider) return;
|
|
137
|
+
const action = closePolicy.classifyClose(event == null ? void 0 : event.code);
|
|
138
|
+
if (action === "remint") {
|
|
139
|
+
consecutiveRemints += 1;
|
|
140
|
+
teardownProvider();
|
|
141
|
+
if (consecutiveRemints > tokenRefresh.MAX_CONSECUTIVE_REMINTS) {
|
|
142
|
+
setStatus(
|
|
143
|
+
"closed",
|
|
144
|
+
new Error("Access repeatedly revoked mid-session (4401); giving up.")
|
|
145
|
+
);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
applyRoster(EMPTY_ROSTER);
|
|
149
|
+
setStatus("connecting");
|
|
150
|
+
void connect();
|
|
151
|
+
} else if (action === "stop") {
|
|
152
|
+
teardownProvider();
|
|
153
|
+
setStatus(
|
|
154
|
+
"closed",
|
|
155
|
+
new Error(`Connection closed permanently (code ${(event == null ? void 0 : event.code) ?? "unknown"})`)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
const onAwareness = () => {
|
|
160
|
+
if (provider !== nextProvider) return;
|
|
161
|
+
recomputeRoster();
|
|
162
|
+
};
|
|
163
|
+
nextProvider.awareness.on("change", onAwareness);
|
|
164
|
+
nextProvider.awareness.on("update", onAwareness);
|
|
165
|
+
scheduleRefresh(grant);
|
|
166
|
+
};
|
|
167
|
+
void connect();
|
|
168
|
+
const sweepTimer = setInterval(recomputeRoster, PRESENCE_SWEEP_INTERVAL_MS);
|
|
169
|
+
return {
|
|
170
|
+
get status() {
|
|
171
|
+
return status;
|
|
172
|
+
},
|
|
173
|
+
get error() {
|
|
174
|
+
return error;
|
|
175
|
+
},
|
|
176
|
+
get accessType() {
|
|
177
|
+
return accessType;
|
|
178
|
+
},
|
|
179
|
+
get version() {
|
|
180
|
+
return version;
|
|
181
|
+
},
|
|
182
|
+
get humans() {
|
|
183
|
+
return roster.humans;
|
|
184
|
+
},
|
|
185
|
+
get agents() {
|
|
186
|
+
return roster.agents;
|
|
187
|
+
},
|
|
188
|
+
refreshNow: () => connect({ scheduledRefresh: true }),
|
|
189
|
+
onChange: (listener) => {
|
|
190
|
+
listeners.add(listener);
|
|
191
|
+
return () => {
|
|
192
|
+
listeners.delete(listener);
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
destroy: () => {
|
|
196
|
+
if (destroyed) return;
|
|
197
|
+
destroyed = true;
|
|
198
|
+
clearInterval(sweepTimer);
|
|
199
|
+
teardownProvider();
|
|
200
|
+
status = "closed";
|
|
201
|
+
applyRoster(EMPTY_ROSTER);
|
|
202
|
+
version += 1;
|
|
203
|
+
for (const listener of [...listeners]) listener();
|
|
204
|
+
listeners.clear();
|
|
205
|
+
doc.destroy();
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
exports.PRESENCE_SWEEP_INTERVAL_MS = PRESENCE_SWEEP_INTERVAL_MS;
|
|
210
|
+
exports.connectWorkspacePresence = connectWorkspacePresence;
|
|
211
|
+
//# sourceMappingURL=presence-client.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presence-client.cjs","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":["Y","mergePresenceStates","refreshDelayMs","mintWorkspacePresenceToken","MIN_REFRESH_DELAY_MS","WebsocketProvider","classifyClose","MAX_CONSECUTIVE_REMINTS"],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanilla (framework-agnostic) observer of a workspace's presence feed.
|
|
3
|
+
*
|
|
4
|
+
* `connectWorkspacePresence` mints a read-only token for the workspace
|
|
5
|
+
* awareness room (`<workspaceId>/awareness`), opens the Keryx WebSocket with
|
|
6
|
+
* the v14 provider, and keeps a merged roster of the people and agent runs in
|
|
7
|
+
* the room — the same per-recipient-filtered feed the Olympus People page
|
|
8
|
+
* renders. Token lifecycle matches `connectGenericDoc`: re-mint before expiry,
|
|
9
|
+
* one fresh mint on a 4401 kick, permanent stop on any other 44xx code,
|
|
10
|
+
* provider backoff for everything else.
|
|
11
|
+
*
|
|
12
|
+
* The client is a **pure observer**. It never writes to the Y.Doc, it drops
|
|
13
|
+
* the provider's default local awareness slot before the socket opens so the
|
|
14
|
+
* token holder never announces itself, and it detaches y-websocket's echo of
|
|
15
|
+
* remote awareness updates — the room's read-only grant carries no broadcast
|
|
16
|
+
* permission, so anything sent would only be discarded server-side. React
|
|
17
|
+
* apps use `useWorkspacePresence` from `@athenaintel/react/collab` instead of
|
|
18
|
+
* calling this directly.
|
|
19
|
+
*/
|
|
20
|
+
import type { GenericDocStatus } from './client';
|
|
21
|
+
import type { CollabAuth } from './mint';
|
|
22
|
+
import { type PresenceAgent, type PresenceHuman } from './presence-slots';
|
|
23
|
+
/** Same vocabulary as the Generic Doc client; a presence feed has no sync phase of its own. */
|
|
24
|
+
export type WorkspacePresenceStatus = GenericDocStatus;
|
|
25
|
+
/**
|
|
26
|
+
* How often the roster is re-derived without an awareness event. y-protocols
|
|
27
|
+
* drops a slot whose publisher went silent for its `outdatedTimeout` (30 s)
|
|
28
|
+
* from a timer of its own, and this sweep is the belt to that brace: a stale
|
|
29
|
+
* slot falls out of `humans` even if no event reaches us for it.
|
|
30
|
+
*/
|
|
31
|
+
export declare const PRESENCE_SWEEP_INTERVAL_MS = 15000;
|
|
32
|
+
export interface WorkspacePresenceHandle {
|
|
33
|
+
readonly status: WorkspacePresenceStatus;
|
|
34
|
+
/** Terminal error, when status is 'error' or 'closed'. */
|
|
35
|
+
readonly error: Error | null;
|
|
36
|
+
/** Granted access from the last successful mint — always 'r' for this feed. */
|
|
37
|
+
readonly accessType: 'r' | 'rw' | null;
|
|
38
|
+
/** Monotonic change counter — bumps on every status or roster notification. */
|
|
39
|
+
readonly version: number;
|
|
40
|
+
/**
|
|
41
|
+
* People in the room, one entry per person (tabs merged by attested id).
|
|
42
|
+
* Identity-stable between notifications: the array only changes when the
|
|
43
|
+
* derived roster does, so it is safe to key memoisation on.
|
|
44
|
+
*/
|
|
45
|
+
readonly humans: PresenceHuman[];
|
|
46
|
+
/** Agent runs in the room with something the token holder may see. */
|
|
47
|
+
readonly agents: PresenceAgent[];
|
|
48
|
+
/**
|
|
49
|
+
* Force an early token refresh. Session-preserving: the live connection
|
|
50
|
+
* stays up until the fresh token is minted; a failed mint retries on a
|
|
51
|
+
* short timer instead of dropping the session.
|
|
52
|
+
*/
|
|
53
|
+
refreshNow: () => Promise<void>;
|
|
54
|
+
onChange: (listener: () => void) => () => void;
|
|
55
|
+
destroy: () => void;
|
|
56
|
+
}
|
|
57
|
+
export declare function connectWorkspacePresence(args: {
|
|
58
|
+
workspaceId: string;
|
|
59
|
+
auth: CollabAuth;
|
|
60
|
+
}): WorkspacePresenceHandle;
|