@opengeni/sdk 0.2.0 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Framework-agnostic TypeScript SDK for the OpenGeni API: typed client, session lifecycle, SSE event streaming with reconnect + replay-by-sequence, and proxy re-streaming helpers.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -36,6 +36,7 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "@opengeni/contracts": "workspace:*",
39
+ "@opengeni/deployment": "workspace:*",
39
40
  "tsup": "^8.5.0",
40
41
  "typescript": "^6.0.3",
41
42
  "zod": "^4.2.1"
package/src/client.ts CHANGED
@@ -2,6 +2,7 @@ import { OpenGeniApiError } from "./errors";
2
2
  import { streamSessionEvents, type SessionEventStreamTransport, type StreamSessionEventsOptions } from "./stream";
3
3
  import type {
4
4
  AccessContext,
5
+ AddWorkspaceMemberRequest,
5
6
  ApiKey,
6
7
  BillingEntitlementsResponse,
7
8
  BillingSummary,
@@ -9,6 +10,7 @@ import type {
9
10
  CapabilityCatalogItem,
10
11
  CapabilityCatalogResponse,
11
12
  CapabilityInstallation,
13
+ ClientConfig,
12
14
  ClientSessionEventInput,
13
15
  CompactSessionContextResult,
14
16
  CompleteFileUploadResponse,
@@ -39,6 +41,7 @@ import type {
39
41
  GitHubRepositoriesResponse,
40
42
  ListApiKeysResponse,
41
43
  ListPacksResponse,
44
+ ListWorkspaceMembersResponse,
42
45
  PackInstallation,
43
46
  ReasoningEffort,
44
47
  RegisterCapabilityPackRequest,
@@ -49,15 +52,53 @@ import type {
49
52
  SessionEvent,
50
53
  SessionGoal,
51
54
  SessionTurn,
55
+ // Stream surfacing (Phase 5): capability negotiation + viewer lifecycle + config.
56
+ SessionCapabilities,
57
+ AttachViewerRequest,
58
+ AttachViewerResponse,
59
+ AcknowledgeStreamRequest,
60
+ AcknowledgeStreamResponse,
61
+ ViewerHeartbeatRequest,
62
+ ViewerHeartbeatResponse,
63
+ // Channel-A structured services (P4.4).
64
+ FsListRequest,
65
+ FsListResponse,
66
+ FsReadRequest,
67
+ FsReadResponse,
68
+ FsWriteRequest,
69
+ FsWriteResponse,
70
+ FsDeleteRequest,
71
+ FsDeleteResponse,
72
+ FsMoveRequest,
73
+ FsMoveResponse,
74
+ FsMkdirRequest,
75
+ FsMkdirResponse,
76
+ GitStatusRequest,
77
+ GitStatusResponse,
78
+ GitDiffRequest,
79
+ GitDiffResponse,
80
+ GitLogRequest,
81
+ GitLogResponse,
82
+ GitShowRequest,
83
+ GitShowResponse,
84
+ TerminalExecRequest,
85
+ TerminalExecResponse,
86
+ PtyOpenRequest,
87
+ PtyOpenResponse,
88
+ PtyWriteRequest,
89
+ PtyResizeRequest,
90
+ PtyCloseRequest,
52
91
  ToolRef,
53
92
  UpdateScheduledTaskRequest,
54
93
  UpdateSessionGoalRequest,
55
94
  UpdateSessionTurnRequest,
56
95
  UpdateWorkspaceEnvironmentRequest,
96
+ UpdateWorkspaceMemberRequest,
57
97
  UpdateWorkspaceRequest,
58
98
  UploadFileInput,
59
99
  WorkspaceEnvironment,
60
100
  WorkspaceEnvironmentVariableMetadata,
101
+ WorkspaceMember,
61
102
  WorkspaceRegisteredPack,
62
103
  Workspace,
63
104
  } from "./types";
@@ -398,8 +439,157 @@ export class OpenGeniClient {
398
439
  return await this.requestJson<CompactSessionContextResult>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/context/compact`, {});
399
440
  }
400
441
 
442
+ // --- Channel-A structured services (P4.4) ------------------------------------
443
+ // FileSystem (Pierre tree), Git (Pierre diff), Terminal (exec + PTY). Each is a
444
+ // synchronous API-direct point query; the fs.changed/git.changed/terminal.pty.*
445
+ // notifications + the PTY output stream arrive on the existing event SSE.
446
+
447
+ /** FileSystem: list a directory tree (feeds the Pierre file tree). */
448
+ async fsList(workspaceId: string, sessionId: string, request: FsListRequest = {}): Promise<FsListResponse> {
449
+ return await this.requestJson<FsListResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/list`, request);
450
+ }
451
+
452
+ /** FileSystem: read a file (text or base64; binary-safe, size-capped). */
453
+ async fsRead(workspaceId: string, sessionId: string, request: FsReadRequest): Promise<FsReadResponse> {
454
+ return await this.requestJson<FsReadResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/read`, request);
455
+ }
456
+
457
+ /** FileSystem: write a file (last-writer-wins; emits fs.changed). */
458
+ async fsWrite(workspaceId: string, sessionId: string, request: FsWriteRequest): Promise<FsWriteResponse> {
459
+ return await this.requestJson<FsWriteResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/write`, request);
460
+ }
461
+
462
+ /** FileSystem: delete a path (emits fs.changed). */
463
+ async fsDelete(workspaceId: string, sessionId: string, request: FsDeleteRequest): Promise<FsDeleteResponse> {
464
+ return await this.requestJson<FsDeleteResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/delete`, request);
465
+ }
466
+
467
+ /** FileSystem: move/rename a path (emits fs.changed; 409 if destination exists and overwrite is false). */
468
+ async fsMove(workspaceId: string, sessionId: string, request: FsMoveRequest): Promise<FsMoveResponse> {
469
+ return await this.requestJson<FsMoveResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/move`, request);
470
+ }
471
+
472
+ /** FileSystem: create a directory (emits fs.changed; recursive defaults to true). */
473
+ async fsMkdir(workspaceId: string, sessionId: string, request: FsMkdirRequest): Promise<FsMkdirResponse> {
474
+ return await this.requestJson<FsMkdirResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/mkdir`, request);
475
+ }
476
+
477
+ /** Git: working-tree/index status (the Pierre file-status feed). */
478
+ async gitStatus(workspaceId: string, sessionId: string, request: GitStatusRequest = {}): Promise<GitStatusResponse> {
479
+ return await this.requestJson<GitStatusResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/status`, request);
480
+ }
481
+
482
+ /** Git: structured diff hunks (the Pierre diff feed). */
483
+ async gitDiff(workspaceId: string, sessionId: string, request: GitDiffRequest = {}): Promise<GitDiffResponse> {
484
+ return await this.requestJson<GitDiffResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/diff`, request);
485
+ }
486
+
487
+ /** Git: commit log. */
488
+ async gitLog(workspaceId: string, sessionId: string, request: GitLogRequest = {}): Promise<GitLogResponse> {
489
+ return await this.requestJson<GitLogResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/log`, request);
490
+ }
491
+
492
+ /** Git: show a commit (diff vs first parent) or fetch a raw blob at a ref. */
493
+ async gitShow(workspaceId: string, sessionId: string, request: GitShowRequest): Promise<GitShowResponse> {
494
+ return await this.requestJson<GitShowResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/show`, request);
495
+ }
496
+
497
+ /** Terminal: run a bounded command, returning buffered stdout/stderr inline. */
498
+ async terminalExec(workspaceId: string, sessionId: string, request: TerminalExecRequest): Promise<TerminalExecResponse> {
499
+ return await this.requestJson<TerminalExecResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/terminal/exec`, request);
500
+ }
501
+
502
+ /** Terminal: open an interactive PTY. Output streams on the event SSE as
503
+ * terminal.pty.output.delta; drive it with terminalPtyWrite. */
504
+ async terminalPtyOpen(workspaceId: string, sessionId: string, request: PtyOpenRequest = {}): Promise<PtyOpenResponse> {
505
+ return await this.requestJson<PtyOpenResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/terminal/pty`, request);
506
+ }
507
+
508
+ /** Terminal: send stdin to an open PTY (output rides A1). */
509
+ async terminalPtyWrite(workspaceId: string, sessionId: string, request: PtyWriteRequest): Promise<void> {
510
+ await this.requestVoid("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/terminal/pty/write`, request);
511
+ }
512
+
513
+ /** Terminal: resize an open PTY. */
514
+ async terminalPtyResize(workspaceId: string, sessionId: string, request: PtyResizeRequest): Promise<void> {
515
+ await this.requestVoid("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/terminal/pty/resize`, request);
516
+ }
517
+
518
+ /** Terminal: close an open PTY (idempotent). */
519
+ async terminalPtyClose(workspaceId: string, sessionId: string, request: PtyCloseRequest): Promise<void> {
520
+ await this.requestVoid("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/terminal/pty/close`, request);
521
+ }
522
+
523
+ // --- Stream surfacing: capability negotiation + viewer lifecycle (Phase 5) ---
524
+ // The capability doc is the single source of UI truth (degradation is always a
525
+ // value, never a crash). The desktop pixel plane (Channel B) is gated behind an
526
+ // un-redacted-acknowledgment + a viewer holder; the structured terminal/files/
527
+ // git surfaces (Channel A) ride the methods above and the event SSE.
528
+
529
+ /** Read the negotiated capability doc for a session WITHOUT acquiring a viewer
530
+ * holder (no warm, no spawn). Drives capability-gated rendering: which
531
+ * surfaces mount, the per-surface unavailability reasons, and the lease
532
+ * liveness the client polls on while `cold`/`warming`. The desktop URL/token
533
+ * are minted in-process only when the box is warm AND the principal has
534
+ * acknowledged the un-redacted plane. */
535
+ async getStreamCapabilities(workspaceId: string, sessionId: string): Promise<SessionCapabilities> {
536
+ return await this.requestJson<SessionCapabilities>(
537
+ "GET", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/stream-capabilities`);
538
+ }
539
+
540
+ /** Record the calling principal's acknowledgment of the un-redacted desktop
541
+ * pixel plane (and, when the box is shared, the shared-exposure disclosure).
542
+ * The desktop viewer-attach path returns 409 until this is recorded. */
543
+ async acknowledgeStream(
544
+ workspaceId: string, sessionId: string, request: AcknowledgeStreamRequest = {},
545
+ ): Promise<AcknowledgeStreamResponse> {
546
+ return await this.requestJson<AcknowledgeStreamResponse>(
547
+ "POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/stream-capabilities/acknowledge`, request);
548
+ }
549
+
550
+ /** Attach a viewer holder (refcounted liveness — keeps the box warm while
551
+ * watched/used), spinning the box up in-process when cold, and mint the scoped
552
+ * direct-to-provider URLs for the requested plane(s). `request.desktop:true`
553
+ * opts into the un-redacted pixel plane and mints the noVNC URL — that plane
554
+ * alone throws `OpenGeniApiError(409)` when the un-redacted/shared
555
+ * acknowledgment is missing (the consent gate). A terminal-only attach
556
+ * (`desktop` omitted/false) warms the box + mints the pty-ws terminal cell with
557
+ * NO consent gate. An omitted `viewerId` mints a fresh one. */
558
+ async attachViewer(
559
+ workspaceId: string, sessionId: string, request: AttachViewerRequest = {},
560
+ ): Promise<AttachViewerResponse> {
561
+ return await this.requestJson<AttachViewerResponse>(
562
+ "POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/viewers`, request);
563
+ }
564
+
565
+ /** Heartbeat a viewer holder (Channel-A app-level liveness). A closed laptop
566
+ * stops sending these → the reaper drops the holder within ~90s. Echoes
567
+ * `leaseEpoch` so a superseded epoch is rejected (`alive:false` → re-attach). */
568
+ async heartbeatViewer(
569
+ workspaceId: string, sessionId: string, viewerId: string, request: ViewerHeartbeatRequest,
570
+ ): Promise<ViewerHeartbeatResponse> {
571
+ return await this.requestJson<ViewerHeartbeatResponse>(
572
+ "POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/viewers/${viewerId}/heartbeat`, request);
573
+ }
574
+
575
+ /** Detach a viewer (delete this holder; idempotent delete-my-row). */
576
+ async detachViewer(workspaceId: string, sessionId: string, viewerId: string): Promise<void> {
577
+ await this.requestVoid("DELETE", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/viewers/${viewerId}`);
578
+ }
579
+
401
580
  // --- Access + workspaces -----------------------------------------------------
402
581
 
582
+ /**
583
+ * The deployment's public client bootstrap config: the host-exposed models
584
+ * (provider-grouped in `models`, flat in `allowedModels` for back-compat),
585
+ * reasoning efforts, MCP servers, file-upload limits, and how the client is
586
+ * expected to authenticate. Drives a composer's model picker without prior
587
+ * knowledge of the host setup; safe to call before any auth is established.
588
+ */
589
+ async getClientConfig(): Promise<ClientConfig> {
590
+ return await this.requestJson<ClientConfig>("GET", "/v1/config/client");
591
+ }
592
+
403
593
  /** The caller's access context: subject, account + workspace grants, defaults. */
404
594
  async getAccessContext(): Promise<AccessContext> {
405
595
  return await this.requestJson<AccessContext>("GET", "/v1/access/me");
@@ -421,6 +611,46 @@ export class OpenGeniClient {
421
611
  return await this.requestJson<Workspace>("PATCH", `/v1/workspaces/${workspaceId}`, request);
422
612
  }
423
613
 
614
+ /**
615
+ * Delete a workspace and everything in it. Refused (409) for the account's
616
+ * only workspace and while it still has a running session. Irreversible.
617
+ */
618
+ async deleteWorkspace(workspaceId: string): Promise<void> {
619
+ await this.requestVoid("DELETE", `/v1/workspaces/${workspaceId}`);
620
+ }
621
+
622
+ // --- Members ("People with access") -------------------------------------------
623
+
624
+ /** The workspace's members (user + api_key subjects). */
625
+ async listWorkspaceMembers(workspaceId: string): Promise<WorkspaceMember[]> {
626
+ const response = await this.requestJson<ListWorkspaceMembersResponse>("GET", `/v1/workspaces/${workspaceId}/members`);
627
+ return response.members;
628
+ }
629
+
630
+ /**
631
+ * Add an already-registered user by email. 404s when no user with that email
632
+ * exists (email invites for unknown users are deferred).
633
+ */
634
+ async addWorkspaceMember(workspaceId: string, request: AddWorkspaceMemberRequest): Promise<WorkspaceMember> {
635
+ return await this.requestJson<WorkspaceMember>("POST", `/v1/workspaces/${workspaceId}/members`, request);
636
+ }
637
+
638
+ async updateWorkspaceMember(workspaceId: string, subjectId: string, request: UpdateWorkspaceMemberRequest): Promise<WorkspaceMember> {
639
+ return await this.requestJson<WorkspaceMember>(
640
+ "PATCH",
641
+ `/v1/workspaces/${workspaceId}/members/${encodeURIComponent(subjectId)}`,
642
+ request,
643
+ );
644
+ }
645
+
646
+ /**
647
+ * Remove a member. Refused (409) for your own membership and for the last
648
+ * member who can still manage the workspace.
649
+ */
650
+ async removeWorkspaceMember(workspaceId: string, subjectId: string): Promise<void> {
651
+ await this.requestVoid("DELETE", `/v1/workspaces/${workspaceId}/members/${encodeURIComponent(subjectId)}`);
652
+ }
653
+
424
654
  // --- Scheduled tasks (write + runs) -------------------------------------------
425
655
 
426
656
  async createScheduledTask(workspaceId: string, request: CreateScheduledTaskRequest): Promise<ScheduledTask> {
@@ -614,6 +844,17 @@ export class OpenGeniClient {
614
844
  );
615
845
  }
616
846
 
847
+ /**
848
+ * Delete a document from a base. Removes the document row and its indexed
849
+ * chunks while leaving the uploaded file asset available for other uses.
850
+ */
851
+ async deleteDocument(workspaceId: string, baseId: string, documentId: string): Promise<void> {
852
+ await this.requestVoid(
853
+ "DELETE",
854
+ `/v1/workspaces/${workspaceId}/document-bases/${baseId}/documents/${documentId}`,
855
+ );
856
+ }
857
+
617
858
  async searchDocuments(
618
859
  workspaceId: string,
619
860
  baseId: string,
package/src/desktop.ts ADDED
@@ -0,0 +1,152 @@
1
+ // Zero-dependency desktop (noVNC) transport contract.
2
+ //
3
+ // The actual `@novnc/novnc` RFB import is a browser-only DOM dependency and
4
+ // lives in `@opengeni/react` (lazily imported, SSR-safe). The framework-agnostic
5
+ // SDK ships only the transport CONTRACT: a URL assembler, the minimal RFB
6
+ // surface the component drives, the connection state machine, and the
7
+ // rotation/fence reducers — all pure and unit-testable, with no DOM and no deps.
8
+ //
9
+ // Channel A (terminal-as-events, files, git) needs NO new transport here: those
10
+ // are event projections + the synchronous fs/git/terminal point queries on
11
+ // `OpenGeniClient`. Only the desktop pixel plane (Channel B, direct-to-provider)
12
+ // needs this.
13
+
14
+ import type { DesktopStreamCapability, StreamUrlRotatedPayload } from "./types";
15
+
16
+ /**
17
+ * Translate the negotiated desktop capability into the WebSocket URL the noVNC
18
+ * RFB client connects to. The scoped provider token is ALREADY embedded in the
19
+ * minted `url` (Modal tunnel host, Blaxel `bl_preview_token`, Daytona signed
20
+ * preview) by `session.resolveExposedPort(6080)` — we do NOT append `cap.token`
21
+ * as a query param (that double-auth was an adversarial-review bug: the box runs
22
+ * `-nopw` in v1, so the RFB password is meaningless and the real auth is the
23
+ * tunnel token in the host). We only normalize the scheme to `ws`/`wss` and, when
24
+ * the minted URL points at a `vnc.html` viewer page, rewrite it to the
25
+ * websockify socket path noVNC actually dials.
26
+ */
27
+ export function desktopSocketUrl(cap: Pick<DesktopStreamCapability, "url">): string {
28
+ if (!cap.url) {
29
+ throw new Error("desktop capability has no url (transport is null)");
30
+ }
31
+ const u = new URL(cap.url);
32
+ // https → wss, http → ws (noVNC dials a WebSocket, not the HTTP viewer page).
33
+ if (u.protocol === "https:") {
34
+ u.protocol = "wss:";
35
+ } else if (u.protocol === "http:") {
36
+ u.protocol = "ws:";
37
+ }
38
+ // A minted `…/vnc.html` (or `…/vnc_lite.html`) viewer page → the websockify
39
+ // socket the page itself would open (`…/websockify`). A bare host/path with no
40
+ // viewer page is already the socket path; leave it untouched.
41
+ if (/\/vnc(_lite)?\.html$/.test(u.pathname)) {
42
+ u.pathname = u.pathname.replace(/\/vnc(_lite)?\.html$/, "/websockify");
43
+ u.search = "";
44
+ u.hash = "";
45
+ }
46
+ return u.toString();
47
+ }
48
+
49
+ /**
50
+ * The minimal RFB surface the React component drives. Lets tests (and 3rd
51
+ * parties swapping noVNC for a WebRTC client in v3) provide a fake without the
52
+ * DOM. Matches `@novnc/novnc`'s RFB constructor + lifecycle.
53
+ */
54
+ export interface DesktopRfbLike {
55
+ viewOnly: boolean;
56
+ scaleViewport: boolean;
57
+ /**
58
+ * 1:1 viewport clipping. We always drive this FALSE: with clipping on, noVNC
59
+ * paints the framebuffer pixel-for-pixel and scrolls/crops to the container
60
+ * (the "zoomed in" look). FALSE lets `scaleViewport` shrink the 1280x800 frame
61
+ * to fit the panel (aspect-preserved). Declared so the hook can pin it instead
62
+ * of relying on noVNC's default — `scaleViewport=true` forces clip off
63
+ * internally, but a stale/partial state on reconnect could leave it on.
64
+ */
65
+ clipViewport: boolean;
66
+ addEventListener(
67
+ type: "connect" | "disconnect" | "securityfailure",
68
+ cb: (e?: unknown) => void,
69
+ ): void;
70
+ removeEventListener?: (
71
+ type: "connect" | "disconnect" | "securityfailure",
72
+ cb: (e?: unknown) => void,
73
+ ) => void;
74
+ disconnect(): void;
75
+ }
76
+
77
+ export type DesktopRfbFactory = (
78
+ target: HTMLElement,
79
+ url: string,
80
+ opts: { credentials?: { password?: string | undefined } | undefined },
81
+ ) => DesktopRfbLike;
82
+
83
+ export type DesktopConnectionState =
84
+ | "idle"
85
+ | "negotiating"
86
+ | "connecting"
87
+ | "connected"
88
+ | "rotating"
89
+ | "reconnecting"
90
+ | "error"
91
+ | "ended";
92
+
93
+ export type DesktopStreamEvent =
94
+ | { type: "negotiated" }
95
+ | { type: "connected" }
96
+ | { type: "disconnected" }
97
+ | { type: "rotate" }
98
+ | { type: "fail" }
99
+ | { type: "abort" };
100
+
101
+ /**
102
+ * Pure reducer for the desktop connection lifecycle. The component owns the RFB
103
+ * object + DOM; this owns the transitions so they are unit-testable. Mirrors the
104
+ * Channel-A stream reducer discipline.
105
+ */
106
+ export function nextDesktopState(
107
+ current: DesktopConnectionState,
108
+ ev: DesktopStreamEvent,
109
+ ): DesktopConnectionState {
110
+ switch (ev.type) {
111
+ case "negotiated":
112
+ return "connecting";
113
+ case "connected":
114
+ return "connected";
115
+ case "rotate":
116
+ return current === "connected" ? "rotating" : current;
117
+ case "disconnected":
118
+ // A deliberate teardown (ended) stays ended; everything else reconnects.
119
+ return current === "ended" ? "ended" : "reconnecting";
120
+ case "fail":
121
+ return "error";
122
+ case "abort":
123
+ return "ended";
124
+ default:
125
+ return current;
126
+ }
127
+ }
128
+
129
+ // `DesktopStreamCapability` is the desktop cell of `SessionCapabilities`; alias
130
+ // it for the rotation reducer below without depending on the whole doc.
131
+ export type DesktopStreamCapabilityLike = {
132
+ url: string | null;
133
+ token: string | null;
134
+ expiresAt: string | null;
135
+ };
136
+
137
+ /**
138
+ * Apply a `stream.url.rotated` event onto a desktop capability, fencing on
139
+ * leaseEpoch (split-brain). A rotation minted under an epoch the client has
140
+ * already advanced PAST is from a superseded owner and is dropped (returns
141
+ * null); otherwise the fresh url/token/expiresAt are folded in.
142
+ */
143
+ export function applyUrlRotation<T extends DesktopStreamCapabilityLike>(
144
+ cap: T,
145
+ payload: StreamUrlRotatedPayload,
146
+ knownEpoch: number,
147
+ ): T | null {
148
+ if (payload.leaseEpoch < knownEpoch) {
149
+ return null;
150
+ }
151
+ return { ...cap, url: payload.url, token: payload.token, expiresAt: payload.expiresAt };
152
+ }
package/src/index.ts CHANGED
@@ -11,6 +11,27 @@ export {
11
11
  export type { ProxySessionEventStreamOptions, SseReStreamOptions } from "./proxy";
12
12
  export { parseSseStream } from "./sse";
13
13
  export type { SseMessage } from "./sse";
14
+ // Desktop (noVNC) transport contract — pure, zero-dep (the RFB import lives in
15
+ // @opengeni/react). URL assembler + connection state machine + rotation fence.
16
+ export { desktopSocketUrl, nextDesktopState, applyUrlRotation } from "./desktop";
17
+ export type {
18
+ DesktopRfbLike,
19
+ DesktopRfbFactory,
20
+ DesktopConnectionState,
21
+ DesktopStreamEvent,
22
+ } from "./desktop";
23
+ // Interactive terminal (ttyd PTY-over-websocket) transport contract — pure,
24
+ // zero-dep (the WebSocket + xterm attach live in @opengeni/react). URL assembler
25
+ // + the ttyd wire-protocol frame codec, symmetric with the desktop above.
26
+ export {
27
+ terminalSocketUrl,
28
+ ttydAuthFrame,
29
+ ttydInputFrame,
30
+ ttydResizeFrame,
31
+ TTYD_SUBPROTOCOL,
32
+ TtydClientCommand,
33
+ TtydServerCommand,
34
+ } from "./terminal";
14
35
  export { streamSessionEvents } from "./stream";
15
36
  export type {
16
37
  SessionEventStreamTransport,
@@ -23,6 +44,7 @@ export type {
23
44
  AccessGrant,
24
45
  AccountGrant,
25
46
  AccountRole,
47
+ AddWorkspaceMemberRequest,
26
48
  AgentMessageCompletedPayload,
27
49
  AgentTextDeltaPayload,
28
50
  AgentToolCallCreatedPayload,
@@ -48,6 +70,9 @@ export type {
48
70
  CapabilityPackSkillFile,
49
71
  CapabilityRuntime,
50
72
  CapabilitySource,
73
+ CapabilityUnavailableReason,
74
+ ClientConfig,
75
+ ClientModel,
51
76
  CompactSessionContextResult,
52
77
  ClientSessionEventInput,
53
78
  CompleteFileUploadResponse,
@@ -92,15 +117,25 @@ export type {
92
117
  KnownUsageEventType,
93
118
  ListApiKeysResponse,
94
119
  ListPacksResponse,
120
+ ListWorkspaceMembersResponse,
95
121
  PackInstallation,
96
122
  PackInstallationStatus,
97
123
  Permission,
98
124
  ProductAccessMode,
99
125
  ReasoningEffort,
126
+ RecordingAvailablePayload,
127
+ RecordingCodec,
128
+ RecordingContentType,
129
+ RecordingFailedPayload,
130
+ RecordingFailedReason,
131
+ RecordingMode,
132
+ RecordingStartedPayload,
100
133
  RegisterCapabilityPackRequest,
101
134
  RepositoryResourceRef,
102
135
  ResourceRef,
103
136
  SandboxBackend,
137
+ SandboxCapabilityName,
138
+ SandboxOs,
104
139
  ScheduledTask,
105
140
  ScheduledTaskAgentConfig,
106
141
  ScheduledTaskAgentConfigInput,
@@ -113,6 +148,27 @@ export type {
113
148
  ScheduledTaskStatus,
114
149
  ScheduledTaskTriggerType,
115
150
  Session,
151
+ SessionCapabilities,
152
+ // Per-surface capability cell aliases (views of SessionCapabilities).
153
+ FileSystemCapability,
154
+ TerminalCapability,
155
+ GitCapability,
156
+ DesktopStreamCapability,
157
+ RecordingCapability,
158
+ ComputerUseCapability,
159
+ // Stream-surfacing client surface (Phase 5).
160
+ ClientAuthConfig,
161
+ StreamUrlRotatedPayload,
162
+ StreamOpenedPayload,
163
+ StreamClosedPayload,
164
+ StreamRevokedPayload,
165
+ AttachViewerRequest,
166
+ AttachViewerResponse,
167
+ ViewerHolder,
168
+ AcknowledgeStreamRequest,
169
+ AcknowledgeStreamResponse,
170
+ ViewerHeartbeatRequest,
171
+ ViewerHeartbeatResponse,
116
172
  SessionEvent,
117
173
  SessionEventType,
118
174
  SessionGoal,
@@ -120,14 +176,61 @@ export type {
120
176
  SessionGoalStatus,
121
177
  SessionStatus,
122
178
  SessionStatusChangedPayload,
179
+ SessionStructuredCapabilities,
123
180
  SessionTurn,
124
181
  SessionTurnSource,
125
182
  SessionTurnStatus,
183
+ // Channel-A structured services (P4.4) — A1 payloads + A2 request/response.
184
+ SandboxCommandOutputDeltaPayload,
185
+ FsChangeKind,
186
+ FsChangedPayload,
187
+ GitChangedPayload,
188
+ TerminalPtyStartedPayload,
189
+ TerminalPtyOutputDeltaPayload,
190
+ TerminalPtyExitedPayload,
191
+ FsNodeType,
192
+ FsTreeNode,
193
+ FsEncoding,
194
+ FsListRequest,
195
+ FsListResponse,
196
+ FsReadRequest,
197
+ FsReadResponse,
198
+ FsWriteRequest,
199
+ FsWriteResponse,
200
+ FsDeleteRequest,
201
+ FsDeleteResponse,
202
+ FsMoveRequest,
203
+ FsMoveResponse,
204
+ FsMkdirRequest,
205
+ FsMkdirResponse,
206
+ GitFileStatusCode,
207
+ GitFileStatus,
208
+ GitStatusRequest,
209
+ GitStatusResponse,
210
+ GitDiffLineType,
211
+ GitDiffLine,
212
+ GitDiffHunk,
213
+ GitFileDiff,
214
+ GitDiffRequest,
215
+ GitDiffResponse,
216
+ GitLogRequest,
217
+ GitCommit,
218
+ GitLogResponse,
219
+ GitShowRequest,
220
+ GitShowResponse,
221
+ TerminalExecRequest,
222
+ TerminalExecResponse,
223
+ PtyOpenRequest,
224
+ PtyOpenResponse,
225
+ PtyWriteRequest,
226
+ PtyResizeRequest,
227
+ PtyCloseRequest,
126
228
  ToolRef,
127
229
  UpdateScheduledTaskRequest,
128
230
  UpdateSessionGoalRequest,
129
231
  UpdateSessionTurnRequest,
130
232
  UpdateWorkspaceEnvironmentRequest,
233
+ UpdateWorkspaceMemberRequest,
131
234
  UpdateWorkspaceRequest,
132
235
  UploadFileInput,
133
236
  UsageEvent,
@@ -138,5 +241,6 @@ export type {
138
241
  Workspace,
139
242
  WorkspaceEnvironment,
140
243
  WorkspaceEnvironmentVariableMetadata,
244
+ WorkspaceMember,
141
245
  WorkspaceRegisteredPack,
142
246
  } from "./types";