@parall/sdk 1.47.0 → 1.49.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/src/client.ts CHANGED
@@ -167,6 +167,8 @@ import type {
167
167
  DispatchExpireResult,
168
168
  ResolveRefsResponse,
169
169
  BacklinksResponse,
170
+ OutboundRefsRequest,
171
+ OutboundRefsResponse,
170
172
  RefGraphResponse,
171
173
  BrokenRefsResponse,
172
174
  PushSubscribeRequest,
@@ -209,6 +211,8 @@ import type {
209
211
  InvokeClipResponse,
210
212
  OnlineClipInfo,
211
213
  RegistryClipInfo,
214
+ ClipRegistryEntry,
215
+ InstallRegistryClipResponse,
212
216
  MachineClip,
213
217
  BrowserProfile,
214
218
  MachineBrowserProfile,
@@ -221,12 +225,20 @@ import type {
221
225
  BrowserProfileLifecycleRequest,
222
226
  BrowserViewerCommandRequest,
223
227
  BrowserViewerCommandResponse,
228
+ EdgeViewerCommandRequest,
229
+ EdgeViewerCommandResponse,
224
230
  GrantBrowserProfileConsentRequest,
225
231
  EdgeDevice,
226
232
  EdgePlacement,
227
233
  EdgeBrowserProfile,
234
+ EdgeProfileProxyStatus,
235
+ SetEdgeProfileProxyRequest,
228
236
  ClipConnection,
229
237
  EdgeOnboardingStatus,
238
+ ExecEdgeClipRequest,
239
+ EdgeClipExecResult,
240
+ ReactionSummary,
241
+ ToggleReactionResponse,
230
242
  } from './types.js';
231
243
 
232
244
  export interface ParallClientOptions {
@@ -417,6 +429,8 @@ export class ParallClient {
417
429
  timeoutMs?: number;
418
430
  signal?: AbortSignal;
419
431
  keepalive?: boolean;
432
+ /** Additional request preconditions such as If-Match. */
433
+ headers?: Record<string, string>;
420
434
  /** Observes the final HTTP status of a successful request (e.g. 200-idempotent-replay vs 201-created). */
421
435
  onStatus?: (status: number) => void;
422
436
  },
@@ -439,7 +453,7 @@ export class ParallClient {
439
453
  if (qs) url += `?${qs}`;
440
454
  }
441
455
 
442
- const headers = this.buildHeaders(path);
456
+ const headers = this.buildHeaders(path, opts?.headers);
443
457
 
444
458
  // Cancellation: the caller's AbortSignal (e.g. a superseded search query)
445
459
  // races the per-request timeout. AbortSignal.any aborts as soon as either
@@ -1091,6 +1105,20 @@ export class ParallClient {
1091
1105
  return this.request('GET', ENDPOINTS.MESSAGE_REPLIES(id), undefined, params);
1092
1106
  }
1093
1107
 
1108
+ // ---- Reactions ----
1109
+
1110
+ async toggleReaction(messageId: string, emoji: string): Promise<ToggleReactionResponse> {
1111
+ return this.request('PUT', ENDPOINTS.MESSAGE_REACTION(messageId, emoji));
1112
+ }
1113
+
1114
+ async listReactions(messageId: string): Promise<ReactionSummary[]> {
1115
+ const res = await this.request<{ reactions: ReactionSummary[] }>(
1116
+ 'GET',
1117
+ ENDPOINTS.MESSAGE_REACTIONS(messageId),
1118
+ );
1119
+ return res.reactions;
1120
+ }
1121
+
1094
1122
  // ---- File Upload ----
1095
1123
 
1096
1124
  async getUploadPresignUrl(orgId: string, req: PresignUploadRequest): Promise<PresignResponse> {
@@ -1753,6 +1781,28 @@ export class ParallClient {
1753
1781
  );
1754
1782
  }
1755
1783
 
1784
+ /**
1785
+ * Drive the Cloud Edge live viewer (V1b, design §6): WebRTC signaling + input +
1786
+ * tab nav for a hosted browser (Cloud Profile). Same request/reply shape as
1787
+ * browserViewerCommand, on the v3 edge pipe; additive — the v2 browser-profile
1788
+ * viewer is unchanged.
1789
+ */
1790
+ async edgeViewerCommand(
1791
+ orgId: string,
1792
+ edgeId: string,
1793
+ req: EdgeViewerCommandRequest,
1794
+ opts?: { timeoutMs?: number; keepalive?: boolean },
1795
+ ): Promise<EdgeViewerCommandResponse> {
1796
+ return this.request(
1797
+ 'POST',
1798
+ ENDPOINTS.ORG_EDGE_VIEWER_COMMAND(orgId, edgeId),
1799
+ req,
1800
+ undefined,
1801
+ false,
1802
+ opts,
1803
+ );
1804
+ }
1805
+
1756
1806
  async resizeMachine(
1757
1807
  orgId: string,
1758
1808
  machineId: string,
@@ -1809,9 +1859,23 @@ export class ParallClient {
1809
1859
 
1810
1860
  // ---- Unread ----
1811
1861
 
1812
- async getUnreadCounts(orgId?: string): Promise<Record<string, UnreadEntry>> {
1862
+ async getUnreadCounts(
1863
+ orgId?: string,
1864
+ opts?: {
1865
+ /** Add unread thread replies (per-thread cursors) to count/mentions.
1866
+ * Opt-in: only clients that can clear thread cursors should pass it. */
1867
+ includeThreadReplies?: boolean;
1868
+ },
1869
+ ): Promise<Record<string, UnreadEntry>> {
1813
1870
  const endpoint = orgId ? ENDPOINTS.ORG_UNREAD(orgId) : ENDPOINTS.UNREAD;
1814
- const res = await this.request<{ data: Record<string, UnreadEntry> }>('GET', endpoint);
1871
+ const res = await this.request<{ data: Record<string, UnreadEntry> }>(
1872
+ 'GET',
1873
+ endpoint,
1874
+ undefined,
1875
+ {
1876
+ include_thread_replies: opts?.includeThreadReplies ? 'true' : undefined,
1877
+ },
1878
+ );
1815
1879
  return res.data;
1816
1880
  }
1817
1881
 
@@ -1819,6 +1883,14 @@ export class ParallClient {
1819
1883
  return this.request('POST', ENDPOINTS.CHAT_READ(orgId, chatId), { message_id: messageId });
1820
1884
  }
1821
1885
 
1886
+ /** Mark everything in a chat read: the channel cursor jumps to the latest
1887
+ * top-level message and every thread cursor to its latest reply, in one
1888
+ * idempotent call. The server echoes the same per-cursor WS events the
1889
+ * single-cursor routes emit and auto-clears covered inbox items. */
1890
+ async markAllRead(orgId: string, chatId: string): Promise<void> {
1891
+ return this.request('POST', ENDPOINTS.CHAT_READ_ALL(orgId, chatId));
1892
+ }
1893
+
1822
1894
  async getThreadUnread(
1823
1895
  orgId: string,
1824
1896
  chatId: string,
@@ -3157,6 +3229,17 @@ export class ParallClient {
3157
3229
  return this.request('GET', ENDPOINTS.REFS_BACKLINKS(orgId), undefined, params);
3158
3230
  }
3159
3231
 
3232
+ /**
3233
+ * Outbound prll:// refs authored in a set of sources, as raw ref_links rows
3234
+ * (dedupe/group client-side). Pass `{ thread_root_id }` to list a whole
3235
+ * thread's refs (root + all replies, resolved server-side — the client's
3236
+ * reply window may be partial), or `{ source_type, source_ids }` for
3237
+ * explicit sources (max 500; v1 accepts only message sources).
3238
+ */
3239
+ async listOutboundRefs(orgId: string, req: OutboundRefsRequest): Promise<OutboundRefsResponse> {
3240
+ return this.request('POST', ENDPOINTS.REFS_OUTBOUND(orgId), req);
3241
+ }
3242
+
3160
3243
  /**
3161
3244
  * Bounded multi-hop walk of the prll:// reference graph around `uri`. `uri`
3162
3245
  * must be an entity-level prll:// URI — a refined URI (path/query/fragment) is
@@ -3472,6 +3555,43 @@ export class ParallClient {
3472
3555
  return resp.data;
3473
3556
  }
3474
3557
 
3558
+ // ---- Clip registry (v3, api-server org registry — `crg_` entries) ----
3559
+
3560
+ /**
3561
+ * List registry clips visible to the org: its own plus public+approved
3562
+ * cross-org entries. This is the surface `installRegistryClip` and clip
3563
+ * connections operate on — NOT the Pinix catalog proxy
3564
+ * ({@link listRegistryClips}), whose entries carry no `crg_` id.
3565
+ */
3566
+ async listOrgRegistryClips(orgId: string): Promise<ClipRegistryEntry[]> {
3567
+ // Server default page is 50 (max 100); one max-size page covers today's
3568
+ // catalogs — revisit with real pagination if registries outgrow it.
3569
+ const resp = await this.request<ClipRegistryEntry[] | null>(
3570
+ 'GET',
3571
+ `${ENDPOINTS.ORG_CLIP_REGISTRY(orgId)}?limit=100`,
3572
+ );
3573
+ return resp ?? [];
3574
+ }
3575
+
3576
+ /**
3577
+ * Install a registry clip into the org (a reference in `clip_installs`, not a
3578
+ * copy). Idempotent: installing an already-installed clip returns the same
3579
+ * `200 {ok:true}`. Fails closed with `403 CLIP_NOT_APPROVED` when the clip is
3580
+ * not eligible (cross-org requires public + approved).
3581
+ */
3582
+ async installRegistryClip(orgId: string, clipId: string): Promise<InstallRegistryClipResponse> {
3583
+ return this.request('POST', ENDPOINTS.ORG_CLIP_INSTALL(orgId), { clip_id: clipId });
3584
+ }
3585
+
3586
+ /** List the org's installed registry clips (full entries). */
3587
+ async listInstalledRegistryClips(orgId: string): Promise<ClipRegistryEntry[]> {
3588
+ const resp = await this.request<ClipRegistryEntry[] | null>(
3589
+ 'GET',
3590
+ ENDPOINTS.ORG_CLIPS_INSTALLED(orgId),
3591
+ );
3592
+ return resp ?? [];
3593
+ }
3594
+
3475
3595
  // ---- Edge devices ----
3476
3596
 
3477
3597
  async listEdgeDevices(orgId: string): Promise<{ data: EdgeDevice[] }> {
@@ -3495,8 +3615,8 @@ export class ParallClient {
3495
3615
  }
3496
3616
 
3497
3617
  /**
3498
- * Delete a hosted Cloud Profile. Hosted only — a BYOC device is removed by
3499
- * uninstalling Parall Clip on that machine (`400 EDGE_PLACEMENT_UNSUPPORTED`).
3618
+ * Delete a hosted Cloud Profile. Hosted only — use {@link unregisterEdgeDevice}
3619
+ * for an offline BYOC registration (`400 EDGE_PLACEMENT_UNSUPPORTED` here).
3500
3620
  *
3501
3621
  * Idempotent and ASYNC: returns `202` with `hosted_state: 'deleting'` on the first
3502
3622
  * call and on every repeat. The device stops being usable immediately (no exec, no
@@ -3507,6 +3627,17 @@ export class ParallClient {
3507
3627
  return this.request('DELETE', ENDPOINTS.ORG_EDGE_DEVICE(orgId, edgeId));
3508
3628
  }
3509
3629
 
3630
+ /**
3631
+ * Remove the interactive human caller's own offline BYOC registration.
3632
+ *
3633
+ * Synchronous and idempotent: a committed removal and a repeat after removal
3634
+ * both resolve with no response body. A live connection returns `EDGE_ONLINE`;
3635
+ * callers must not clear local device identity until this method resolves.
3636
+ */
3637
+ async unregisterEdgeDevice(orgId: string, edgeId: string): Promise<void> {
3638
+ await this.request('DELETE', ENDPOINTS.ORG_EDGE_DEVICE_UNREGISTER(orgId, edgeId));
3639
+ }
3640
+
3510
3641
  async getEdgeOnboarding(orgId: string): Promise<EdgeOnboardingStatus> {
3511
3642
  return this.request('GET', ENDPOINTS.ORG_EDGE_ONBOARDING(orgId));
3512
3643
  }
@@ -3515,6 +3646,137 @@ export class ParallClient {
3515
3646
  return this.request('GET', ENDPOINTS.ORG_EDGE_PROFILES(orgId, edgeId));
3516
3647
  }
3517
3648
 
3649
+ /**
3650
+ * Read a hosted Cloud Profile's egress-proxy status (manager-only: hosted
3651
+ * human maintainer or org admin). Sanitized — the password never comes back.
3652
+ * Typed errors: `EDGE_NOT_HOSTED` (BYOC device), `PROXY_CONFIG_CORRUPT` /
3653
+ * validation codes as 422 when a stored config no longer passes current rules.
3654
+ * `can_mutate` and `lease_status` are the authoritative idle gate; device
3655
+ * list status is not a substitute.
3656
+ */
3657
+ async getEdgeProfileProxy(
3658
+ orgId: string,
3659
+ edgeId: string,
3660
+ profileName: string,
3661
+ ): Promise<EdgeProfileProxyStatus> {
3662
+ return this.request('GET', ENDPOINTS.ORG_EDGE_PROFILE_PROXY(orgId, edgeId, profileName));
3663
+ }
3664
+
3665
+ /**
3666
+ * Set/replace the profile's egress proxy (full triple every time) — IDLE
3667
+ * ONLY: while the profile's hosted browser is running (a viewer session is
3668
+ * open or a pod is otherwise live) the server answers 409
3669
+ * `EDGE_PROFILE_IN_USE`; close the viewer, wait for idle scale-to-zero, and
3670
+ * retry. The next cold start uses the new egress; browser login state is
3671
+ * preserved across it. Other typed errors: the validation vocabulary
3672
+ * (`INVALID_PROXY_SERVER`, `PROXY_AUTH_INCOMPLETE`,
3673
+ * `PROXY_SERVER_FORBIDDEN_TARGET`, …), `EDGE_DELETING` (409), and
3674
+ * `SECRETBOX_UNCONFIGURED` (503 — server cannot store credentials safely),
3675
+ * and `EDGE_PROFILE_PROXY_STALE` (409 — expectedVersion lost a tab race).
3676
+ */
3677
+ async setEdgeProfileProxy(
3678
+ orgId: string,
3679
+ edgeId: string,
3680
+ profileName: string,
3681
+ req: SetEdgeProfileProxyRequest,
3682
+ expectedVersion?: string,
3683
+ ): Promise<EdgeProfileProxyStatus> {
3684
+ return this.request(
3685
+ 'PUT',
3686
+ ENDPOINTS.ORG_EDGE_PROFILE_PROXY(orgId, edgeId, profileName),
3687
+ req,
3688
+ undefined,
3689
+ false,
3690
+ {
3691
+ headers: expectedVersion ? { 'If-Match': `"proxy-${expectedVersion}"` } : undefined,
3692
+ },
3693
+ );
3694
+ }
3695
+
3696
+ /**
3697
+ * Clear the profile's egress proxy; the next pod start egresses directly.
3698
+ * Idle-only like set — 409 `EDGE_PROFILE_IN_USE` while the browser is live.
3699
+ */
3700
+ async clearEdgeProfileProxy(
3701
+ orgId: string,
3702
+ edgeId: string,
3703
+ profileName: string,
3704
+ expectedVersion?: string,
3705
+ ): Promise<EdgeProfileProxyStatus> {
3706
+ return this.request(
3707
+ 'DELETE',
3708
+ ENDPOINTS.ORG_EDGE_PROFILE_PROXY(orgId, edgeId, profileName),
3709
+ undefined,
3710
+ undefined,
3711
+ false,
3712
+ {
3713
+ headers: expectedVersion ? { 'If-Match': `"proxy-${expectedVersion}"` } : undefined,
3714
+ },
3715
+ );
3716
+ }
3717
+
3718
+ /**
3719
+ * Execute a registry clip command on an Edge device.
3720
+ *
3721
+ * A hosted (Cloud Profile) device is reachable ONLY through an explicit
3722
+ * `connection` (id `ccn_…` or alias) — there is no implicit route to an
3723
+ * org-shared browser login. BYOC keeps its legacy selectors (`edge_id`, or
3724
+ * nothing for the caller's own online device).
3725
+ *
3726
+ * Returns the result envelope on completion (`success` may be false when the
3727
+ * command RAN and failed — `error`/`error_code` describe why). Everything
3728
+ * else throws a typed {@link ApiError}; match on `err.code`:
3729
+ *
3730
+ * Safe to retry (guaranteed nothing was dispatched):
3731
+ * - `EDGE_ACTIVATING` 503 + `Retry-After` — cold cloud profile is starting.
3732
+ * Bounded backoff, same `correlation_id` across the loop.
3733
+ * - `EDGE_BUSY` 409 — the device is executing another request.
3734
+ * - `EDGE_CONCURRENCY_LIMIT` 429 — org at its concurrent-session limit.
3735
+ * - `EDGE_UNAVAILABLE` 503 — session torn down / replaced mid-dispatch.
3736
+ *
3737
+ * NOT retryable:
3738
+ * - `OUTCOME_UNKNOWN` 504 — dispatched, but no result arrived. The command
3739
+ * MAY HAVE EXECUTED (posted, ordered, deleted…). Never retry
3740
+ * automatically: verify the effect first, then decide. The message carries
3741
+ * the request id for audit.
3742
+ * - `EDGE_DEADLINE_EXCEEDED` 504 — arrived late, provably NOT executed.
3743
+ * - `EDGE_HOSTED_DISABLED_FOR_ORG` 403, `EDGE_REPAIR` 503 (operator-held),
3744
+ * `EDGE_RUNTIME_UNAVAILABLE` 503 (deployment has no hosted runtime).
3745
+ * - Routing errors: `HOSTED_CONNECTION_REQUIRED`, `CONNECTION_NOT_FOUND`,
3746
+ * `CONNECTION_CLIP_MISMATCH`, `CONNECTION_TARGET_GONE`,
3747
+ * `CONNECTION_PROFILE_MISMATCH`, `EDGE_DELETING`, `DEVICE_OFFLINE`.
3748
+ */
3749
+ async execEdgeClip(orgId: string, req: ExecEdgeClipRequest): Promise<EdgeClipExecResult> {
3750
+ // The server holds the connection open for timeout + ~5s of result wait;
3751
+ // give the HTTP layer headroom past that so a slow-but-successful exec is
3752
+ // not chopped locally into a fake transport error. The effective value
3753
+ // mirrors the SERVER's rule exactly (out-of-range → its 30s default): a
3754
+ // local clamp that disagreed would abort the HTTP call while the server
3755
+ // legitimately keeps executing — manufacturing a transport error for a
3756
+ // request that may still succeed.
3757
+ const t = req.timeout;
3758
+ const serverTimeout = t !== undefined && t > 0 && t <= 120000 ? t : 30000;
3759
+ const timeoutMs = serverTimeout + 10_000;
3760
+ try {
3761
+ return await this.request('POST', ENDPOINTS.ORG_EDGE_EXEC(orgId), req, undefined, false, {
3762
+ timeoutMs,
3763
+ });
3764
+ } catch (err) {
3765
+ // A 422 is the RESULT envelope (the command ran and failed), not the
3766
+ // standard error envelope — lift its stable `error_code` into
3767
+ // ApiError.code so callers match one field for every failure.
3768
+ if (
3769
+ err instanceof ApiError &&
3770
+ err.status === 422 &&
3771
+ !err.code &&
3772
+ typeof err.extras?.error_code === 'string'
3773
+ ) {
3774
+ err.code = err.extras.error_code;
3775
+ }
3776
+ throw err;
3777
+ }
3778
+ }
3779
+
3518
3780
  // ---- Clip connections ----
3519
3781
 
3520
3782
  async listClipConnections(orgId: string, clipId: string): Promise<{ data: ClipConnection[] }> {
@@ -3550,6 +3812,8 @@ function normalizeWikiChangeset(changeset: WikiChangeset): WikiChangeset {
3550
3812
 
3551
3813
  export class ApiError extends Error {
3552
3814
  extras?: Record<string, unknown>;
3815
+ /** Retry-After delta seconds when the server supplies one. */
3816
+ retryAfterSeconds?: number;
3553
3817
  /** Attempted action (authorization denials) — e.g. "chat.add_member". */
3554
3818
  action?: string;
3555
3819
  /** Target resource URI that was evaluated — e.g. "prll://cht_…". */
@@ -3577,7 +3841,7 @@ export class ApiError extends Error {
3577
3841
  * See docs/engineering-design/error-contract-design.md
3578
3842
  */
3579
3843
  function buildApiError(
3580
- res: { status: number; statusText: string },
3844
+ res: { status: number; statusText: string; headers?: { get(name: string): string | null } },
3581
3845
  rawErrorBody: unknown,
3582
3846
  ): ApiError {
3583
3847
  const errorBody =
@@ -3600,6 +3864,8 @@ function buildApiError(
3600
3864
  (typeof errorObj?.code === 'string' ? errorObj.code : undefined) ??
3601
3865
  (typeof errorBody.code === 'string' ? (errorBody.code as string) : undefined);
3602
3866
  const apiError = new ApiError(res.status, errMsg ?? res.statusText, errCode);
3867
+ const retryAfter = Number(res.headers?.get('Retry-After'));
3868
+ if (Number.isFinite(retryAfter) && retryAfter > 0) apiError.retryAfterSeconds = retryAfter;
3603
3869
  // Machine anchors: present under `error`, with a legacy flat fallback.
3604
3870
  const anchors = errorObj ?? errorBody;
3605
3871
  if (typeof anchors.action === 'string') apiError.action = anchors.action;
package/src/constants.ts CHANGED
@@ -410,6 +410,9 @@ export const ENDPOINTS = {
410
410
  MESSAGE_WATCH: (id: string) => `${API_BASE}/messages/${id}/watch`,
411
411
  MESSAGE_WATCHERS: (id: string) => `${API_BASE}/messages/${id}/watchers`,
412
412
  MESSAGE_WATCHING: (id: string) => `${API_BASE}/messages/${id}/watching`,
413
+ MESSAGE_REACTIONS: (id: string) => `${API_BASE}/messages/${id}/reactions`,
414
+ MESSAGE_REACTION: (id: string, emoji: string) =>
415
+ `${API_BASE}/messages/${id}/reactions/${encodeURIComponent(emoji)}`,
413
416
 
414
417
  // Upload (org-scoped)
415
418
  UPLOAD_PRESIGN: (orgId: string) => `${API_BASE}/orgs/${orgId}/upload/presign`,
@@ -780,6 +783,8 @@ export const ENDPOINTS = {
780
783
  UNREAD: `${API_BASE}/me/unread`,
781
784
  ORG_UNREAD: (orgId: string) => `${API_BASE}/orgs/${orgId}/unread`,
782
785
  CHAT_READ: (orgId: string, chatId: string) => `${API_BASE}/orgs/${orgId}/chats/${chatId}/read`,
786
+ CHAT_READ_ALL: (orgId: string, chatId: string) =>
787
+ `${API_BASE}/orgs/${orgId}/chats/${chatId}/read-all`,
783
788
  THREAD_UNREAD: (orgId: string, chatId: string, threadRootId: string) =>
784
789
  `${API_BASE}/orgs/${orgId}/chats/${chatId}/threads/${threadRootId}/unread`,
785
790
  THREAD_READ: (orgId: string, chatId: string, threadRootId: string) =>
@@ -788,6 +793,7 @@ export const ENDPOINTS = {
788
793
  // References (org-scoped)
789
794
  REFS_RESOLVE: (orgId: string) => `${API_BASE}/orgs/${orgId}/refs/resolve`,
790
795
  REFS_BACKLINKS: (orgId: string) => `${API_BASE}/orgs/${orgId}/refs/backlinks`,
796
+ REFS_OUTBOUND: (orgId: string) => `${API_BASE}/orgs/${orgId}/refs/outbound`,
791
797
  REFS_GRAPH: (orgId: string) => `${API_BASE}/orgs/${orgId}/refs/graph`,
792
798
  REFS_CHECK: (orgId: string) => `${API_BASE}/orgs/${orgId}/refs/check`,
793
799
 
@@ -871,13 +877,30 @@ export const ENDPOINTS = {
871
877
  ORG_EDGE: (orgId: string) => `/api/v1/orgs/${orgId}/edge`,
872
878
  ORG_EDGE_DEVICES: (orgId: string) => `/api/v1/orgs/${orgId}/edge/devices`,
873
879
  ORG_EDGE_DEVICE: (orgId: string, edgeId: string) => `/api/v1/orgs/${orgId}/edge/${edgeId}`,
880
+ ORG_EDGE_DEVICE_UNREGISTER: (orgId: string, edgeId: string) =>
881
+ `/api/v1/orgs/${orgId}/edge/${edgeId}/unregister`,
874
882
  ORG_EDGE_ONBOARDING: (orgId: string) => `/api/v1/orgs/${orgId}/edge/onboarding`,
875
883
  ORG_EDGE_PROFILES: (orgId: string, edgeId: string) =>
876
884
  `/api/v1/orgs/${orgId}/edge/${edgeId}/profiles`,
885
+ // Per-profile egress proxy (hosted Cloud Profiles; manager-only, human-only).
886
+ ORG_EDGE_PROFILE_PROXY: (orgId: string, edgeId: string, profileName: string) =>
887
+ `/api/v1/orgs/${orgId}/edge/${edgeId}/profiles/${encodeURIComponent(profileName)}/proxy`,
888
+ ORG_EDGE_EXEC: (orgId: string) => `/api/v1/orgs/${orgId}/edge/exec`,
889
+ // Cloud Edge live viewer command (V1b) — api-server, gated on cap:edge-viewer.
890
+ // Same request/reply shape as the v2 browser-profile viewer, on the v3 edge
891
+ // pipe. Additive: does NOT replace BROWSER_PROFILE_VIEWER_COMMAND.
892
+ ORG_EDGE_VIEWER_COMMAND: (orgId: string, edgeId: string) =>
893
+ `/api/v1/orgs/${orgId}/edge/${edgeId}/viewer/command`,
877
894
  CLIP_CONNECTIONS: (orgId: string, clipId: string) =>
878
895
  `/api/v1/orgs/${orgId}/clip-registry/${clipId}/connections`,
879
896
  CLIP_CONNECTION: (orgId: string, connId: string) =>
880
897
  `/api/v1/orgs/${orgId}/clip-connections/${connId}`,
898
+
899
+ // Clip registry (v3, org-scoped, served by api-server — `crg_` entries; the
900
+ // Pinix Hub catalog proxy above is a different, id-less surface)
901
+ ORG_CLIP_REGISTRY: (orgId: string) => `/api/v1/orgs/${orgId}/clip-registry`,
902
+ ORG_CLIP_INSTALL: (orgId: string) => `/api/v1/orgs/${orgId}/clips/install`,
903
+ ORG_CLIPS_INSTALLED: (orgId: string) => `/api/v1/orgs/${orgId}/clips/installed`,
881
904
  } as const;
882
905
 
883
906
  /**
@@ -932,6 +955,7 @@ export const WS_EVENTS = {
932
955
  MESSAGE_PATCH: 'message.patch',
933
956
  MESSAGE_EDIT: 'message.edit',
934
957
  MESSAGE_DELETE: 'message.delete',
958
+ MESSAGE_REACTION_UPDATED: 'message.reaction.updated',
935
959
  TYPING_UPDATE: 'typing.update',
936
960
  CHAT_UPDATE: 'chat.update',
937
961
  CHAT_DELETED: 'chat.deleted',
@@ -972,6 +996,7 @@ export const WS_EVENTS = {
972
996
  INBOX_UPDATE: 'inbox.update',
973
997
  INBOX_BULK_UPDATE: 'inbox.bulk_update',
974
998
  READ_POSITION_UPDATED: 'read_position.updated',
999
+ THREAD_READ_POSITION_UPDATED: 'thread_read_position.updated',
975
1000
  DISPATCH_NEW: 'dispatch.new',
976
1001
  DISPATCH_RECEIVED: 'dispatch.received',
977
1002
  DISPATCH_RESOLVED: 'dispatch.resolved',