uidex 0.5.2 → 0.7.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.
Files changed (39) hide show
  1. package/README.md +3 -3
  2. package/dist/cli/cli.cjs +1542 -1227
  3. package/dist/cli/cli.cjs.map +1 -1
  4. package/dist/cloud/index.cjs +385 -175
  5. package/dist/cloud/index.cjs.map +1 -1
  6. package/dist/cloud/index.d.cts +192 -4
  7. package/dist/cloud/index.d.ts +192 -4
  8. package/dist/cloud/index.js +377 -177
  9. package/dist/cloud/index.js.map +1 -1
  10. package/dist/headless/index.cjs +116 -251
  11. package/dist/headless/index.cjs.map +1 -1
  12. package/dist/headless/index.d.cts +6 -11
  13. package/dist/headless/index.d.ts +6 -11
  14. package/dist/headless/index.js +116 -253
  15. package/dist/headless/index.js.map +1 -1
  16. package/dist/index.cjs +776 -1055
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +152 -160
  19. package/dist/index.d.ts +152 -160
  20. package/dist/index.js +792 -1066
  21. package/dist/index.js.map +1 -1
  22. package/dist/react/index.cjs +801 -1019
  23. package/dist/react/index.cjs.map +1 -1
  24. package/dist/react/index.d.cts +102 -86
  25. package/dist/react/index.d.ts +102 -86
  26. package/dist/react/index.js +821 -1038
  27. package/dist/react/index.js.map +1 -1
  28. package/dist/scan/index.cjs +1550 -1220
  29. package/dist/scan/index.cjs.map +1 -1
  30. package/dist/scan/index.d.cts +210 -12
  31. package/dist/scan/index.d.ts +210 -12
  32. package/dist/scan/index.js +1547 -1219
  33. package/dist/scan/index.js.map +1 -1
  34. package/package.json +22 -21
  35. package/templates/claude/SKILL.md +71 -0
  36. package/templates/claude/references/audit.md +43 -0
  37. package/templates/claude/{rules.md → references/conventions.md} +25 -28
  38. package/templates/claude/audit.md +0 -43
  39. /package/templates/claude/{api.md → references/api.md} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/cloud/index.ts","../../src/cloud/client.ts","../../../api-client/src/client.gen.ts","../../../api-client/src/sdk.gen.ts","../../src/cloud/realtime.ts","../../src/cloud/types.ts"],"sourcesContent":["export { cloud } from \"./client\"\nexport { createRealtimeChannel } from \"./realtime\"\nexport {\n CloudError,\n DEFAULT_CLOUD_ENDPOINT,\n type CloudAdapter,\n type CloudOptions,\n type RealtimeChannel,\n type RealtimeChannelState,\n type RealtimeConnectOpts,\n type RealtimePresenceUser,\n} from \"./types\"\nexport type {\n ArchiveReason,\n ReportListRecord,\n ReportListResponse,\n ReportPayload,\n ReportResult,\n IngestConfig,\n PinRecord,\n} from \"@uidex/api-client\"\n","import { createClient, createConfig } from \"@hey-api/client-fetch\"\nimport {\n submitReport as submitReportSdk,\n getIngestConfig as getIngestConfigSdk,\n listIngestReports as listIngestReportsSdk,\n listPins as listPinsSdk,\n archivePin as archivePinSdk,\n} from \"@uidex/api-client\"\nimport { createRealtimeChannel } from \"./realtime\"\nimport {\n CloudError,\n DEFAULT_CLOUD_ENDPOINT,\n type ArchiveReason,\n type CloudAdapter,\n type CloudOptions,\n type ReportListResponse,\n type ReportPayload,\n type ReportResult,\n type IngestConfig,\n type PinRecord,\n type RealtimeChannel,\n type RealtimeConnectOpts,\n} from \"./types\"\n\nfunction trimEndpoint(endpoint: string): string {\n return endpoint.endsWith(\"/\") ? endpoint.slice(0, -1) : endpoint\n}\n\nfunction unwrap<T>(result: {\n data?: T\n error?: unknown\n response: Response\n}): T {\n if (result.error !== undefined || !result.data) {\n const status = result.response.status\n const retryAfter =\n status === 429\n ? parseRetryAfter(result.response.headers.get(\"Retry-After\"))\n : undefined\n const msg =\n result.error &&\n typeof result.error === \"object\" &&\n \"error\" in result.error &&\n typeof (result.error as { error: unknown }).error === \"string\"\n ? (result.error as { error: string }).error\n : `Request failed (${status})`\n throw new CloudError(msg, { status, retryAfter, details: result.error })\n }\n return result.data\n}\n\nfunction parseRetryAfter(header: string | null): number | undefined {\n if (!header) return undefined\n const seconds = Number(header)\n if (Number.isFinite(seconds) && seconds >= 0) return seconds\n const date = Date.parse(header)\n if (Number.isFinite(date)) {\n const delta = Math.ceil((date - Date.now()) / 1000)\n return delta > 0 ? delta : 0\n }\n return undefined\n}\n\nexport function cloud(options: CloudOptions): CloudAdapter {\n let cachedConfig: Promise<IngestConfig> | null = null\n let resolvedConfig: IngestConfig | null = null\n\n const projectKey = options.projectKey\n if (!projectKey) {\n throw new Error(\"uidex/cloud: `projectKey` is required\")\n }\n const endpoint = trimEndpoint(options.endpoint ?? DEFAULT_CLOUD_ENDPOINT)\n const git = options.git\n\n const apiClient = createClient(\n createConfig({\n baseUrl: endpoint,\n ...(options.fetch ? { fetch: options.fetch } : {}),\n headers: { Authorization: `Bearer ${projectKey}` },\n })\n )\n\n async function submit(payload: ReportPayload): Promise<ReportResult> {\n const enriched =\n git?.branch || git?.commit\n ? {\n ...payload,\n context: {\n ...payload.context,\n git: payload.context?.git ?? {\n branch: git.branch,\n commit: git.commit,\n },\n },\n }\n : payload\n const result = await submitReportSdk({\n client: apiClient,\n body: enriched as Parameters<typeof submitReportSdk>[0][\"body\"],\n })\n return unwrap(result) as ReportResult\n }\n\n async function fetchConfig(): Promise<IngestConfig> {\n const result = await getIngestConfigSdk({ client: apiClient })\n return unwrap(result) as IngestConfig\n }\n\n function startFetch(): Promise<IngestConfig> {\n const promise = fetchConfig()\n promise.then(\n (config) => {\n resolvedConfig = config\n },\n () => {}\n )\n return promise\n }\n\n cachedConfig = startFetch()\n\n function getConfig(): Promise<IngestConfig> {\n return cachedConfig ?? (cachedConfig = startFetch())\n }\n\n function getCachedConfig(): IngestConfig | null {\n return resolvedConfig\n }\n\n function realtimeUrl(\n route: string,\n user: RealtimeConnectOpts[\"user\"]\n ): string {\n const httpToWs = endpoint\n .replace(/^http:/, \"ws:\")\n .replace(/^https:/, \"wss:\")\n const params = new URLSearchParams()\n params.set(\"key\", projectKey)\n params.set(\"route\", route)\n params.set(\"userId\", user.id)\n if (user.name) params.set(\"name\", user.name)\n if (user.avatar) params.set(\"avatar\", user.avatar)\n return `${httpToWs}/ws?${params.toString()}`\n }\n\n function connectRealtime(opts: RealtimeConnectOpts): RealtimeChannel {\n if (!opts || !opts.user || typeof opts.user.id !== \"string\") {\n throw new TypeError(\"uidex/cloud: realtime.connect requires `user.id`\")\n }\n let currentRoute = opts.route\n const channel = createRealtimeChannel({\n buildUrl: () => realtimeUrl(currentRoute, opts.user),\n })\n const originalJoinRoute = channel.joinRoute\n const wrapped: RealtimeChannel = {\n get state() {\n return channel.state\n },\n connect: () => channel.connect(),\n disconnect: () => channel.disconnect(),\n joinRoute: (route: string) => {\n currentRoute = route\n originalJoinRoute(route)\n },\n onPresence: (cb) => channel.onPresence(cb),\n onPin: (cb) => channel.onPin(cb),\n }\n channel.connect()\n return wrapped\n }\n\n async function listPins(params: {\n route?: string\n entities?: string\n }): Promise<PinRecord[]> {\n const result = await listPinsSdk({\n client: apiClient,\n query: params,\n })\n const data = unwrap(result)\n return data.pins\n }\n\n async function archivePin(\n reportId: string,\n reason?: ArchiveReason\n ): Promise<void> {\n const result = await archivePinSdk({\n client: apiClient,\n body: { reportId, ...(reason ? { reason } : {}) },\n })\n unwrap(result)\n }\n\n async function listReports(opts?: {\n page?: number\n limit?: number\n }): Promise<ReportListResponse> {\n const result = await listIngestReportsSdk({\n client: apiClient,\n query: opts,\n })\n return unwrap(result) as ReportListResponse\n }\n\n return {\n reports: { submit, list: listReports },\n integrations: { getConfig, getCachedConfig },\n realtime: { connect: connectRealtime },\n pins: { list: listPins, archive: archivePin },\n }\n}\n","// This file is auto-generated by @hey-api/openapi-ts\n\nimport type { ClientOptions } from \"./types.gen\"\nimport {\n type Config,\n type ClientOptions as DefaultClientOptions,\n createClient,\n createConfig,\n} from \"@hey-api/client-fetch\"\n\n/**\n * The `createClientConfig()` function will be called on client initialization\n * and the returned object will become the client's initial configuration.\n *\n * You may want to initialize your client this way instead of calling\n * `setConfig()`. This is useful for example if you're using Next.js\n * to ensure your client always has the correct values.\n */\nexport type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> =\n (\n override?: Config<DefaultClientOptions & T>\n ) => Config<Required<DefaultClientOptions> & T>\n\nexport const client = createClient(\n createConfig<ClientOptions>({\n baseUrl: \"https://app.uidex.dev\",\n })\n)\n","// This file is auto-generated by @hey-api/openapi-ts\n\nimport type {\n Options as ClientOptions,\n TDataShape,\n Client,\n} from \"@hey-api/client-fetch\"\nimport type {\n SubmitReportData,\n SubmitReportResponse,\n SubmitReportError,\n GetIngestConfigData,\n GetIngestConfigResponse,\n ListIngestReportsData,\n ListIngestReportsResponse,\n ListPinsData,\n ListPinsResponse,\n ListPinsError,\n ArchivePinData,\n ArchivePinResponse,\n ArchivePinError,\n AuthorizeCliData,\n AuthorizeCliResponse,\n AuthorizeCliError,\n ListOrganizationsData,\n ListOrganizationsResponse,\n CreateOrganizationData,\n CreateOrganizationResponse,\n CreateOrganizationError,\n SwitchOrganizationData,\n SwitchOrganizationResponse,\n DeleteOrganizationData,\n DeleteOrganizationResponse,\n GetOrganizationData,\n GetOrganizationResponse,\n GetOrganizationError,\n UpdateOrganizationData,\n UpdateOrganizationResponse,\n RemoveMemberData,\n RemoveMemberResponse,\n RemoveMemberError,\n ListMembersData,\n ListMembersResponse,\n UpdateMemberData,\n UpdateMemberResponse,\n UpdateMemberError,\n ListInvitationsData,\n ListInvitationsResponse,\n CreateInvitationData,\n CreateInvitationResponse,\n DeleteInvitationData,\n DeleteInvitationResponse,\n DeleteInvitationError,\n ListProjectsData,\n ListProjectsResponse,\n CreateProjectData,\n CreateProjectResponse,\n CreateProjectError,\n DeleteProjectData,\n DeleteProjectResponse,\n GetProjectData,\n GetProjectResponse,\n GetProjectError,\n UpdateProjectData,\n UpdateProjectResponse,\n UpdateProjectError,\n ResolveProjectData,\n ResolveProjectResponse,\n ResolveProjectError,\n ListApiKeysData,\n ListApiKeysResponse,\n CreateApiKeyData,\n CreateApiKeyResponse,\n CreateApiKeyError,\n DeleteApiKeyData,\n DeleteApiKeyResponse,\n DeleteApiKeyError,\n RevokeApiKeyData,\n RevokeApiKeyResponse,\n RevokeApiKeyError,\n ListProjectReportsData,\n ListProjectReportsResponse,\n DeleteReportData,\n DeleteReportResponse,\n DeleteReportError,\n GetReportData,\n GetReportResponse,\n GetReportError,\n UpdateReportData,\n UpdateReportResponse,\n BulkArchiveReportData,\n BulkArchiveReportResponse,\n ListIntegrationsData,\n ListIntegrationsResponse,\n CreateIntegrationData,\n CreateIntegrationResponse,\n CreateIntegrationError,\n DeleteIntegrationData,\n DeleteIntegrationResponse,\n DeleteIntegrationError,\n GetIntegrationData,\n GetIntegrationResponse,\n GetIntegrationError,\n ListIntegrationTargetsData,\n ListIntegrationTargetsResponse,\n TestIntegrationData,\n TestIntegrationResponse,\n ListExternalIssuesData,\n ListExternalIssuesResponse,\n ListIssueDraftsData,\n ListIssueDraftsResponse,\n CreateIssueDraftData,\n CreateIssueDraftResponse,\n CreateIssueDraftError,\n DeleteIssueDraftData,\n DeleteIssueDraftResponse,\n DeleteIssueDraftError,\n UpdateIssueDraftData,\n UpdateIssueDraftResponse,\n UpdateIssueDraftError,\n SubmitIssueDraftData,\n SubmitIssueDraftResponse,\n SubmitIssueDraftError,\n RunTriageData,\n RunTriageResponse,\n RunTriageError,\n ListUserTokensData,\n ListUserTokensResponse,\n CreateUserTokenData,\n CreateUserTokenResponse,\n CreateUserTokenError,\n DeleteUserTokenData,\n DeleteUserTokenResponse,\n DeleteUserTokenError,\n} from \"./types.gen\"\nimport { client as _heyApiClient } from \"./client.gen\"\n\nexport type Options<\n TData extends TDataShape = TDataShape,\n ThrowOnError extends boolean = boolean,\n> = ClientOptions<TData, ThrowOnError> & {\n /**\n * You can provide a client instance returned by `createClient()` instead of\n * individual options. This might be also useful if you want to implement a\n * custom client.\n */\n client?: Client\n /**\n * You can pass arbitrary values through the `meta` object. This can be\n * used to access values that aren't defined as part of the SDK function.\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Submit feedback from the SDK.\n */\nexport const submitReport = <ThrowOnError extends boolean = false>(\n options: Options<SubmitReportData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).post<\n SubmitReportResponse,\n SubmitReportError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/ingest\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * Get integration configuration for the project.\n */\nexport const getIngestConfig = <ThrowOnError extends boolean = false>(\n options?: Options<GetIngestConfigData, ThrowOnError>\n) => {\n return (options?.client ?? _heyApiClient).get<\n GetIngestConfigResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/ingest/config\",\n ...options,\n })\n}\n\n/**\n * List feedback for the project (SDK view).\n */\nexport const listIngestReports = <ThrowOnError extends boolean = false>(\n options?: Options<ListIngestReportsData, ThrowOnError>\n) => {\n return (options?.client ?? _heyApiClient).get<\n ListIngestReportsResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/ingest/reports\",\n ...options,\n })\n}\n\n/**\n * List pins by route and/or entity.\n * At least one of `route` or `entities` must be provided.\n */\nexport const listPins = <ThrowOnError extends boolean = false>(\n options?: Options<ListPinsData, ThrowOnError>\n) => {\n return (options?.client ?? _heyApiClient).get<\n ListPinsResponse,\n ListPinsError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/ingest/pins\",\n ...options,\n })\n}\n\n/**\n * Archive a pin.\n */\nexport const archivePin = <ThrowOnError extends boolean = false>(\n options: Options<ArchivePinData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).post<\n ArchivePinResponse,\n ArchivePinError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/ingest/pins/archive\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * Authorize a CLI session by delivering a token.\n */\nexport const authorizeCli = <ThrowOnError extends boolean = false>(\n options: Options<AuthorizeCliData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).post<\n AuthorizeCliResponse,\n AuthorizeCliError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/cli-auth/authorize\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * List organizations for the current user.\n */\nexport const listOrganizations = <ThrowOnError extends boolean = false>(\n options?: Options<ListOrganizationsData, ThrowOnError>\n) => {\n return (options?.client ?? _heyApiClient).get<\n ListOrganizationsResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations\",\n ...options,\n })\n}\n\n/**\n * Create an organization.\n */\nexport const createOrganization = <ThrowOnError extends boolean = false>(\n options: Options<CreateOrganizationData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).post<\n CreateOrganizationResponse,\n CreateOrganizationError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * Switch the active organization.\n */\nexport const switchOrganization = <ThrowOnError extends boolean = false>(\n options: Options<SwitchOrganizationData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).post<\n SwitchOrganizationResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/switch\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * Delete an organization.\n */\nexport const deleteOrganization = <ThrowOnError extends boolean = false>(\n options: Options<DeleteOrganizationData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).delete<\n DeleteOrganizationResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}\",\n ...options,\n })\n}\n\n/**\n * Get organization details.\n */\nexport const getOrganization = <ThrowOnError extends boolean = false>(\n options: Options<GetOrganizationData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).get<\n GetOrganizationResponse,\n GetOrganizationError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}\",\n ...options,\n })\n}\n\n/**\n * Update an organization.\n */\nexport const updateOrganization = <ThrowOnError extends boolean = false>(\n options: Options<UpdateOrganizationData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).patch<\n UpdateOrganizationResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * Remove a member from the organization.\n */\nexport const removeMember = <ThrowOnError extends boolean = false>(\n options: Options<RemoveMemberData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).delete<\n RemoveMemberResponse,\n RemoveMemberError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/members\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * List organization members.\n */\nexport const listMembers = <ThrowOnError extends boolean = false>(\n options: Options<ListMembersData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).get<\n ListMembersResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/members\",\n ...options,\n })\n}\n\n/**\n * Update a member's role.\n */\nexport const updateMember = <ThrowOnError extends boolean = false>(\n options: Options<UpdateMemberData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).patch<\n UpdateMemberResponse,\n UpdateMemberError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/members/{memberId}\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * List organization invitations.\n */\nexport const listInvitations = <ThrowOnError extends boolean = false>(\n options: Options<ListInvitationsData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).get<\n ListInvitationsResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/invitations\",\n ...options,\n })\n}\n\n/**\n * Create an invitation link.\n */\nexport const createInvitation = <ThrowOnError extends boolean = false>(\n options: Options<CreateInvitationData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).post<\n CreateInvitationResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/invitations\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * Revoke an invitation.\n */\nexport const deleteInvitation = <ThrowOnError extends boolean = false>(\n options: Options<DeleteInvitationData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).delete<\n DeleteInvitationResponse,\n DeleteInvitationError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/invitations/{inviteId}\",\n ...options,\n })\n}\n\n/**\n * List projects in an organization.\n */\nexport const listProjects = <ThrowOnError extends boolean = false>(\n options: Options<ListProjectsData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).get<\n ListProjectsResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/projects\",\n ...options,\n })\n}\n\n/**\n * Create a project.\n */\nexport const createProject = <ThrowOnError extends boolean = false>(\n options: Options<CreateProjectData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).post<\n CreateProjectResponse,\n CreateProjectError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/projects\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * Delete a project.\n */\nexport const deleteProject = <ThrowOnError extends boolean = false>(\n options: Options<DeleteProjectData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).delete<\n DeleteProjectResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/projects/{projectId}\",\n ...options,\n })\n}\n\n/**\n * Get project details.\n */\nexport const getProject = <ThrowOnError extends boolean = false>(\n options: Options<GetProjectData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).get<\n GetProjectResponse,\n GetProjectError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/projects/{projectId}\",\n ...options,\n })\n}\n\n/**\n * Update a project.\n */\nexport const updateProject = <ThrowOnError extends boolean = false>(\n options: Options<UpdateProjectData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).patch<\n UpdateProjectResponse,\n UpdateProjectError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/projects/{projectId}\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * Resolve an API key to its project and organization.\n */\nexport const resolveProject = <ThrowOnError extends boolean = false>(\n options: Options<ResolveProjectData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).get<\n ResolveProjectResponse,\n ResolveProjectError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/projects/resolve\",\n ...options,\n })\n}\n\n/**\n * List API keys for a project.\n */\nexport const listApiKeys = <ThrowOnError extends boolean = false>(\n options: Options<ListApiKeysData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).get<\n ListApiKeysResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/projects/{projectId}/api-keys\",\n ...options,\n })\n}\n\n/**\n * Create an API key.\n */\nexport const createApiKey = <ThrowOnError extends boolean = false>(\n options: Options<CreateApiKeyData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).post<\n CreateApiKeyResponse,\n CreateApiKeyError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/projects/{projectId}/api-keys\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * Permanently delete an API key.\n */\nexport const deleteApiKey = <ThrowOnError extends boolean = false>(\n options: Options<DeleteApiKeyData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).delete<\n DeleteApiKeyResponse,\n DeleteApiKeyError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/projects/{projectId}/api-keys/{keyId}\",\n ...options,\n })\n}\n\n/**\n * Revoke an API key.\n */\nexport const revokeApiKey = <ThrowOnError extends boolean = false>(\n options: Options<RevokeApiKeyData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).patch<\n RevokeApiKeyResponse,\n RevokeApiKeyError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/projects/{projectId}/api-keys/{keyId}\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * List feedback for a project.\n */\nexport const listProjectReports = <ThrowOnError extends boolean = false>(\n options: Options<ListProjectReportsData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).get<\n ListProjectReportsResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/projects/{projectId}/reports\",\n ...options,\n })\n}\n\n/**\n * Delete a feedback record.\n */\nexport const deleteReport = <ThrowOnError extends boolean = false>(\n options: Options<DeleteReportData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).delete<\n DeleteReportResponse,\n DeleteReportError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}\",\n ...options,\n })\n}\n\n/**\n * Get feedback details.\n */\nexport const getReport = <ThrowOnError extends boolean = false>(\n options: Options<GetReportData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).get<\n GetReportResponse,\n GetReportError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}\",\n ...options,\n })\n}\n\n/**\n * Update feedback status, priority, or assignment.\n */\nexport const updateReport = <ThrowOnError extends boolean = false>(\n options: Options<UpdateReportData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).patch<\n UpdateReportResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * Archive multiple feedback records.\n */\nexport const bulkArchiveReport = <ThrowOnError extends boolean = false>(\n options: Options<BulkArchiveReportData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).post<\n BulkArchiveReportResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/projects/{projectId}/reports/archive\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * List integrations for an organization.\n */\nexport const listIntegrations = <ThrowOnError extends boolean = false>(\n options: Options<ListIntegrationsData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).get<\n ListIntegrationsResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/integrations\",\n ...options,\n })\n}\n\n/**\n * Create an integration.\n */\nexport const createIntegration = <ThrowOnError extends boolean = false>(\n options: Options<CreateIntegrationData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).post<\n CreateIntegrationResponse,\n CreateIntegrationError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/integrations\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * Delete an integration.\n */\nexport const deleteIntegration = <ThrowOnError extends boolean = false>(\n options: Options<DeleteIntegrationData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).delete<\n DeleteIntegrationResponse,\n DeleteIntegrationError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/integrations/{integrationId}\",\n ...options,\n })\n}\n\n/**\n * Get integration details.\n */\nexport const getIntegration = <ThrowOnError extends boolean = false>(\n options: Options<GetIntegrationData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).get<\n GetIntegrationResponse,\n GetIntegrationError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/integrations/{integrationId}\",\n ...options,\n })\n}\n\n/**\n * List available targets (e.g. Jira projects/boards).\n */\nexport const listIntegrationTargets = <ThrowOnError extends boolean = false>(\n options: Options<ListIntegrationTargetsData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).get<\n ListIntegrationTargetsResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/integrations/{integrationId}/targets\",\n ...options,\n })\n}\n\n/**\n * Test integration connectivity.\n */\nexport const testIntegration = <ThrowOnError extends boolean = false>(\n options: Options<TestIntegrationData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).post<\n TestIntegrationResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/integrations/{integrationId}/test\",\n ...options,\n })\n}\n\n/**\n * List external issues, optionally filtered by feedback.\n */\nexport const listExternalIssues = <ThrowOnError extends boolean = false>(\n options: Options<ListExternalIssuesData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).get<\n ListExternalIssuesResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/external-issues\",\n ...options,\n })\n}\n\n/**\n * List issue drafts for a project.\n */\nexport const listIssueDrafts = <ThrowOnError extends boolean = false>(\n options: Options<ListIssueDraftsData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).get<\n ListIssueDraftsResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/issue-drafts\",\n ...options,\n })\n}\n\n/**\n * Manually create an issue draft.\n */\nexport const createIssueDraft = <ThrowOnError extends boolean = false>(\n options: Options<CreateIssueDraftData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).post<\n CreateIssueDraftResponse,\n CreateIssueDraftError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/issue-drafts\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * Delete an issue draft.\n */\nexport const deleteIssueDraft = <ThrowOnError extends boolean = false>(\n options: Options<DeleteIssueDraftData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).delete<\n DeleteIssueDraftResponse,\n DeleteIssueDraftError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/issue-drafts/{draftId}\",\n ...options,\n })\n}\n\n/**\n * Update an issue draft.\n */\nexport const updateIssueDraft = <ThrowOnError extends boolean = false>(\n options: Options<UpdateIssueDraftData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).patch<\n UpdateIssueDraftResponse,\n UpdateIssueDraftError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/issue-drafts/{draftId}\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * Submit a draft to the configured integration.\n */\nexport const submitIssueDraft = <ThrowOnError extends boolean = false>(\n options: Options<SubmitIssueDraftData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).post<\n SubmitIssueDraftResponse,\n SubmitIssueDraftError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/issue-drafts/{draftId}/submit\",\n ...options,\n })\n}\n\n/**\n * Run LLM-powered triage on untriaged feedback.\n */\nexport const runTriage = <ThrowOnError extends boolean = false>(\n options: Options<RunTriageData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).post<\n RunTriageResponse,\n RunTriageError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/organizations/{orgId}/projects/{projectId}/triage\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * List personal access tokens.\n */\nexport const listUserTokens = <ThrowOnError extends boolean = false>(\n options?: Options<ListUserTokensData, ThrowOnError>\n) => {\n return (options?.client ?? _heyApiClient).get<\n ListUserTokensResponse,\n unknown,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/user/tokens\",\n ...options,\n })\n}\n\n/**\n * Create a personal access token.\n */\nexport const createUserToken = <ThrowOnError extends boolean = false>(\n options: Options<CreateUserTokenData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).post<\n CreateUserTokenResponse,\n CreateUserTokenError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/user/tokens\",\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options?.headers,\n },\n })\n}\n\n/**\n * Revoke and delete a personal access token.\n */\nexport const deleteUserToken = <ThrowOnError extends boolean = false>(\n options: Options<DeleteUserTokenData, ThrowOnError>\n) => {\n return (options.client ?? _heyApiClient).delete<\n DeleteUserTokenResponse,\n DeleteUserTokenError,\n ThrowOnError\n >({\n security: [\n {\n scheme: \"bearer\",\n type: \"http\",\n },\n ],\n url: \"/api/user/tokens/{tokenId}\",\n ...options,\n })\n}\n","import type { PinRecord } from \"@uidex/api-client\"\nimport type {\n RealtimeChannel,\n RealtimeChannelState,\n RealtimePresenceUser,\n} from \"./types\"\n\nconst RECONNECT_INITIAL_MS = 1000\nconst RECONNECT_MAX_MS = 30_000\nconst CLOSE_CODE_AUTH_FAILED = 4001\n\nexport type RealtimeChannelOptions = {\n /** Builds the WebSocket URL on each (re)connect attempt. Allows the caller to refresh `route` query params. */\n buildUrl: () => string\n /** Optional WebSocket constructor override (testing). Defaults to `globalThis.WebSocket`. */\n WebSocketImpl?: typeof WebSocket\n}\n\ntype Listener<T> = (value: T) => void\n\nfunction emit<T>(listeners: Set<Listener<T>>, value: T): void {\n for (const cb of listeners) {\n try {\n cb(value)\n } catch {\n // listener errors must not break the channel\n }\n }\n}\n\nfunction resolveWebSocket(override?: typeof WebSocket): typeof WebSocket {\n if (override) return override\n if (\n typeof globalThis !== \"undefined\" &&\n typeof (globalThis as { WebSocket?: typeof WebSocket }).WebSocket ===\n \"function\"\n ) {\n return (globalThis as { WebSocket: typeof WebSocket }).WebSocket\n }\n throw new Error(\n \"uidex/cloud: global WebSocket is not available; pass a `WebSocketImpl` override\"\n )\n}\n\nexport function createRealtimeChannel(\n options: RealtimeChannelOptions\n): RealtimeChannel {\n const WS = resolveWebSocket(options.WebSocketImpl)\n\n const presenceListeners = new Set<Listener<RealtimePresenceUser[]>>()\n const pinListeners = new Set<Listener<PinRecord>>()\n\n let ws: WebSocket | null = null\n let state: RealtimeChannelState = \"disconnected\"\n let disposed = false\n let reconnectAttempts = 0\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null\n\n function clearReconnectTimer(): void {\n if (reconnectTimer !== null) {\n clearTimeout(reconnectTimer)\n reconnectTimer = null\n }\n }\n\n function scheduleReconnect(): void {\n if (disposed) return\n const delay = Math.min(\n RECONNECT_INITIAL_MS * 2 ** reconnectAttempts,\n RECONNECT_MAX_MS\n )\n reconnectAttempts += 1\n state = \"connecting\"\n clearReconnectTimer()\n reconnectTimer = setTimeout(() => {\n reconnectTimer = null\n openSocket()\n }, delay)\n }\n\n function handleMessage(event: MessageEvent): void {\n if (typeof event.data !== \"string\") return\n let parsed: unknown\n try {\n parsed = JSON.parse(event.data)\n } catch {\n return\n }\n if (!parsed || typeof parsed !== \"object\") return\n const msg = parsed as { type?: unknown }\n\n if (msg.type === \"presence\") {\n const p = msg as { users?: unknown }\n if (!Array.isArray(p.users)) return\n const users: RealtimePresenceUser[] = []\n for (const raw of p.users) {\n if (!raw || typeof raw !== \"object\") continue\n const u = raw as Partial<RealtimePresenceUser>\n if (typeof u.userId !== \"string\" || typeof u.name !== \"string\") continue\n users.push({\n userId: u.userId,\n name: u.name,\n avatar: typeof u.avatar === \"string\" ? u.avatar : null,\n })\n }\n emit(presenceListeners, users)\n return\n }\n\n if (msg.type === \"pin\") {\n // Wire format uses legacy field names; map to API PinRecord shape\n const p = msg as Record<string, unknown>\n if (\n typeof p.feedbackId !== \"string\" ||\n !p.elementRef ||\n typeof p.elementRef !== \"object\" ||\n typeof (p.elementRef as { kind?: unknown }).kind !== \"string\" ||\n typeof (p.elementRef as { id?: unknown }).id !== \"string\" ||\n !p.author ||\n typeof p.author !== \"object\" ||\n typeof p.body !== \"string\" ||\n typeof p.reportType !== \"string\" ||\n typeof p.reportSeverity !== \"string\" ||\n typeof p.createdAt !== \"string\"\n ) {\n return\n }\n const author = p.author as Record<string, unknown>\n const elRef = p.elementRef as { kind: string; id: string }\n const pin: PinRecord = {\n id: p.feedbackId,\n entity: `${elRef.kind}:${elRef.id}`,\n reporter: {\n name: typeof author.name === \"string\" ? author.name : undefined,\n email: typeof author.email === \"string\" ? author.email : undefined,\n },\n body: p.body,\n type: p.reportType as PinRecord[\"type\"],\n severity: p.reportSeverity as PinRecord[\"severity\"],\n status: \"open\" as PinRecord[\"status\"],\n createdAt: p.createdAt,\n url: \"\",\n }\n emit(pinListeners, pin)\n return\n }\n }\n\n function openSocket(): void {\n if (disposed) return\n state = \"connecting\"\n let url: string\n try {\n url = options.buildUrl()\n } catch {\n state = \"disconnected\"\n return\n }\n let socket: WebSocket\n try {\n socket = new WS(url)\n } catch {\n scheduleReconnect()\n return\n }\n ws = socket\n\n socket.addEventListener(\"open\", () => {\n if (ws !== socket) return\n state = \"connected\"\n reconnectAttempts = 0\n })\n\n socket.addEventListener(\"message\", (event) => {\n if (ws !== socket) return\n handleMessage(event as MessageEvent)\n })\n\n socket.addEventListener(\"close\", (event) => {\n if (ws !== socket) return\n ws = null\n const code = (event as CloseEvent).code\n if (disposed || code === CLOSE_CODE_AUTH_FAILED) {\n state = \"disconnected\"\n return\n }\n scheduleReconnect()\n })\n\n socket.addEventListener(\"error\", () => {\n // close fires after error; reconnect handled there\n })\n }\n\n function connect(): void {\n if (disposed) {\n disposed = false\n }\n if (ws) return\n if (state === \"connecting\") return\n reconnectAttempts = 0\n openSocket()\n }\n\n function disconnect(): void {\n disposed = true\n clearReconnectTimer()\n state = \"disconnected\"\n if (ws) {\n try {\n ws.close(1000)\n } catch {\n // ignore\n }\n ws = null\n }\n }\n\n function joinRoute(route: string): void {\n emit(presenceListeners, [])\n if (ws && ws.readyState === WS.OPEN) {\n try {\n ws.send(JSON.stringify({ type: \"join\", route }))\n } catch {\n // socket may have closed mid-call\n }\n }\n }\n\n function onPresence(cb: Listener<RealtimePresenceUser[]>): () => void {\n presenceListeners.add(cb)\n return () => {\n presenceListeners.delete(cb)\n }\n }\n\n function onPin(cb: Listener<PinRecord>): () => void {\n pinListeners.add(cb)\n return () => {\n pinListeners.delete(cb)\n }\n }\n\n return {\n get state() {\n return state\n },\n connect,\n disconnect,\n joinRoute,\n onPresence,\n onPin,\n }\n}\n","import type {\n ArchiveReason,\n ReportListResponse,\n ReportPayload,\n ReportResult,\n IngestConfig,\n PinRecord,\n} from \"@uidex/api-client\"\nimport type { UserIdentity } from \"../shared/user\"\n\nexport type {\n ArchiveReason,\n ReportListResponse,\n ReportPayload,\n ReportResult,\n IngestConfig,\n PinRecord,\n} from \"@uidex/api-client\"\n\nexport const DEFAULT_CLOUD_ENDPOINT = \"https://app.uidex.dev\"\n\nexport interface CloudOptions {\n projectKey: string\n endpoint?: string\n fetch?: typeof fetch\n git?: { branch?: string; commit?: string }\n}\n\nexport class CloudError extends Error {\n readonly status: number\n readonly retryAfter?: number\n readonly details?: unknown\n\n constructor(\n message: string,\n options: { status: number; retryAfter?: number; details?: unknown }\n ) {\n super(message)\n this.name = \"CloudError\"\n this.status = options.status\n this.retryAfter = options.retryAfter\n this.details = options.details\n }\n}\n\nexport type RealtimePresenceUser = {\n userId: string\n name: string\n avatar: string | null\n}\n\nexport type RealtimeChannelState = \"connecting\" | \"connected\" | \"disconnected\"\n\nexport interface RealtimeConnectOpts {\n user: UserIdentity\n route: string\n}\n\nexport interface RealtimeChannel {\n readonly state: RealtimeChannelState\n connect(): void\n disconnect(): void\n joinRoute(route: string): void\n onPresence(cb: (users: RealtimePresenceUser[]) => void): () => void\n onPin(cb: (pin: PinRecord) => void): () => void\n}\n\nexport interface CloudAdapter<\n TPayload = ReportPayload,\n TResult = ReportResult,\n TIntegrations = {\n getConfig(): Promise<IngestConfig>\n getCachedConfig(): IngestConfig | null\n },\n> {\n readonly reports: {\n submit(payload: TPayload): Promise<TResult>\n list?(opts?: { page?: number; limit?: number }): Promise<ReportListResponse>\n }\n readonly integrations: TIntegrations\n readonly realtime: {\n connect(opts: RealtimeConnectOpts): RealtimeChannel\n }\n readonly pins: {\n list(params: { route?: string; entities?: string }): Promise<PinRecord[]>\n archive(reportId: string, reason?: ArchiveReason): Promise<void>\n }\n}\n"],"mappings":"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","names":["import_client_fetch","listPins","archivePin"]}
1
+ {"version":3,"sources":["../../src/cloud/index.ts","../../src/cloud/protocol.ts","../../src/cloud/realtime.ts","../../src/cloud/types.ts","../../src/cloud/rpc.ts","../../src/cloud/client.ts"],"sourcesContent":["export { cloud } from \"./client\"\nexport { createRealtimeChannel, type InternalRealtimeChannel } from \"./realtime\"\nexport { createRpcClient, DEFAULT_RPC_TIMEOUT_MS, type RpcClient } from \"./rpc\"\nexport {\n RPC_METHODS,\n SCREENSHOT_CHUNK_BYTES,\n SCREENSHOT_INLINE_MAX_BYTES,\n isRpcMethod,\n parseRpcResponseFrame,\n type IdentifyFrame,\n type LeaveFrame,\n type PinArchivedFrame,\n type ReportSubmitParams,\n type RpcErrFrame,\n type RpcMethod,\n type RpcOkFrame,\n type RpcParamsMap,\n type RpcRequestFrame,\n type RpcResponseFrame,\n type RpcResultMap,\n type ScreenshotChunkParams,\n} from \"./protocol\"\nexport {\n CloudError,\n DEFAULT_CLOUD_ENDPOINT,\n type CloudAdapter,\n type CloudOptions,\n type RealtimeChannel,\n type RealtimeChannelState,\n type RealtimeConnectOpts,\n type RealtimePresenceUser,\n} from \"./types\"\nexport type {\n ArchiveReason,\n ReportListRecord,\n ReportListResponse,\n ReportPayload,\n ReportResult,\n IngestConfig,\n PinRecord,\n} from \"@uidex/api-client\"\n","import type {\n ArchiveReason,\n IngestConfig,\n PinRecord,\n ReportListResponse,\n ReportPayload,\n ReportResult,\n} from \"@uidex/api-client\"\n\n/**\n * WS RPC protocol shared by the SDK (`packages/sdk/src/cloud`) and the relay\n * server (`apps/app/src/lib/relay-handler.ts`, type-only import from\n * \"uidex/cloud\"). These frames ride the same socket as the existing\n * fire-and-forget messages (`cursor`, `join`, `presence`, `pin`); peers that\n * predate a frame type silently drop it.\n */\n\nexport const RPC_METHODS = [\n \"reports.submit\",\n \"reports.list\",\n \"config.get\",\n \"pins.list\",\n \"pins.screenshot\",\n \"pins.archive\",\n \"screenshot.chunk\",\n] as const\n\nexport type RpcMethod = (typeof RPC_METHODS)[number]\n\n/**\n * Screenshots whose base64 data-URL length is at or below this ride inline in\n * the `reports.submit` frame (the common case). Larger ones are streamed via\n * `screenshot.chunk` so no single frame approaches the relay's 25 MiB WS\n * `maxPayload` — an oversized frame is dropped server-side (close 1009), which\n * the SDK surfaces as \"Connection lost\".\n */\nexport const SCREENSHOT_INLINE_MAX_BYTES = 20 * 1024 * 1024\n/** Per-chunk size when a screenshot is streamed. Each `screenshot.chunk` frame stays well under `maxPayload`. */\nexport const SCREENSHOT_CHUNK_BYTES = 8 * 1024 * 1024\n\n/** One streamed screenshot fragment. Parts are keyed by `seq` and reassembled in order server-side. */\nexport type ScreenshotChunkParams = {\n /** Client-generated id grouping a screenshot's chunks; referenced by `reports.submit` as `screenshotUploadId`. */\n uploadId: string\n /** 0-based index of this part. */\n seq: number\n /** Total number of parts in this upload. */\n total: number\n /** A slice of the screenshot data URL. */\n data: string\n}\n\n/**\n * `reports.submit` params. Either the screenshot rides inline (`screenshot`) or,\n * for large captures, it is streamed via `screenshot.chunk` and referenced here\n * by `screenshotUploadId` — the server reassembles it before persisting.\n */\nexport type ReportSubmitParams = ReportPayload & {\n screenshotUploadId?: string\n}\n\nexport interface RpcParamsMap {\n \"reports.submit\": ReportSubmitParams\n \"reports.list\": { page?: number; limit?: number }\n \"config.get\": undefined\n \"pins.list\": { route?: string; entities?: string[] }\n \"pins.screenshot\": { reportId: string }\n \"pins.archive\": { reportId: string; reason?: ArchiveReason }\n \"screenshot.chunk\": ScreenshotChunkParams\n}\n\nexport interface RpcResultMap {\n \"reports.submit\": ReportResult\n \"reports.list\": ReportListResponse\n \"config.get\": IngestConfig\n // Pins travel without their screenshot (a multi-MB base64 frame would\n // head-of-line-block cursor/presence on the shared socket); the detail\n // view lazy-fetches it per pin via `pins.screenshot`.\n \"pins.list\": { pins: PinRecord[] }\n \"pins.screenshot\": { screenshot: string | null }\n \"pins.archive\": { ok: true }\n /** Acks a stored chunk; `received` is how many distinct parts have arrived. */\n \"screenshot.chunk\": { received: number }\n}\n\nexport type RpcRequestFrame = {\n type: \"rpc\"\n id: string\n method: RpcMethod\n params?: unknown\n}\n\nexport type RpcOkFrame = {\n type: \"rpc:res\"\n id: string\n ok: true\n data: unknown\n}\n\nexport type RpcErrFrame = {\n type: \"rpc:res\"\n id: string\n ok: false\n /** HTTP-equivalent status so CloudError semantics survive the transport. */\n status: number\n error: string\n /** Seconds, present on 429 responses (mirrors the Retry-After header). */\n retryAfter?: number\n details?: unknown\n}\n\nexport type RpcResponseFrame = RpcOkFrame | RpcErrFrame\n\n/**\n * Sent by the SDK after `realtime.connect(opts)` to attach a user identity to\n * an initially anonymous connection. The server marks the connection visible\n * in presence from this point on. Connections that never identify stay\n * RPC-only: excluded from presence, no room auto-join.\n */\nexport type IdentifyFrame = {\n type: \"identify\"\n userId: string\n name?: string\n avatar?: string\n}\n\n/**\n * Reverse of `IdentifyFrame`: sent when a realtime consumer disconnects\n * (e.g. the uidex surface unmounts) while the adapter keeps using the socket\n * for RPC. The server drops the connection from presence and its room but\n * keeps the socket open.\n */\nexport type LeaveFrame = {\n type: \"leave\"\n}\n\n/**\n * Server→client broadcast when a pin's report leaves the open statuses\n * (closed via the SDK, the dashboard, or the REST route). Sent to the room of\n * the report's route so live overlays drop the pin without a refresh.\n */\nexport type PinArchivedFrame = {\n type: \"pin:archived\"\n reportId: string\n}\n\nexport function isRpcMethod(value: unknown): value is RpcMethod {\n return (\n typeof value === \"string\" &&\n (RPC_METHODS as readonly string[]).includes(value)\n )\n}\n\n/** Client-side guard for inbound `rpc:res` frames (server validates inbound `rpc` frames itself). */\nexport function parseRpcResponseFrame(msg: unknown): RpcResponseFrame | null {\n if (!msg || typeof msg !== \"object\") return null\n const m = msg as Record<string, unknown>\n if (m.type !== \"rpc:res\" || typeof m.id !== \"string\") return null\n if (m.ok === true) {\n return { type: \"rpc:res\", id: m.id, ok: true, data: m.data }\n }\n if (m.ok === false) {\n if (typeof m.status !== \"number\" || typeof m.error !== \"string\") return null\n return {\n type: \"rpc:res\",\n id: m.id,\n ok: false,\n status: m.status,\n error: m.error,\n retryAfter: typeof m.retryAfter === \"number\" ? m.retryAfter : undefined,\n details: m.details,\n }\n }\n return null\n}\n","import type { PinRecord } from \"@uidex/api-client\"\nimport { parseRpcResponseFrame, type RpcResponseFrame } from \"./protocol\"\nimport type { UserIdentity } from \"../shared/user\"\nimport type {\n RealtimeChannel,\n RealtimeChannelState,\n RealtimePresenceUser,\n} from \"./types\"\n\nconst RECONNECT_INITIAL_MS = 1000\nconst RECONNECT_MAX_MS = 30_000\n/**\n * Give up after ~3 minutes of consecutive failures (1+2+4+8+16+5×30s) instead\n * of retrying forever: an unreachable endpoint (server down, adblocker,\n * corporate proxy) must not make every host-app tab churn connections for its\n * whole lifetime. Any later `connect()` — every RPC call issues one on a\n * closed socket — resets the budget and revives the channel.\n */\nconst MAX_RECONNECT_ATTEMPTS = 10\nexport const CLOSE_CODE_AUTH_FAILED = 4001\n/** Reported to `onClose` when the socket never produced a real close code (failed constructor, failed `buildUrl`, local disconnect). */\nexport const CLOSE_CODE_SYNTHETIC = 1006\n\nexport type RealtimeChannelOptions = {\n /** Builds the WebSocket URL on each (re)connect attempt. Only needs the project key — identity and route ride dedicated frames. */\n buildUrl: () => string\n /** Optional WebSocket constructor override (testing). Defaults to `globalThis.WebSocket`. */\n WebSocketImpl?: typeof WebSocket\n}\n\n/**\n * Superset of the public `RealtimeChannel` used internally by the cloud\n * adapter: the RPC client (`./rpc`) shares the same socket and needs raw\n * frame access plus open/close notifications.\n */\nexport interface InternalRealtimeChannel extends RealtimeChannel {\n /** Required here (optional on the public interface only for channel stubs). */\n onPinArchived(cb: (reportId: string) => void): () => void\n /** Stores the identity and (re)sends the `identify` frame on every open. */\n identify(user: UserIdentity): void\n /** Drops the stored identity/route without touching the socket (soft leave). */\n clearSession(): void\n /** Sends a JSON frame if the socket is OPEN. Returns false otherwise. */\n sendFrame(frame: object): boolean\n /** Fires after the channel finished its identify/join replay on (re)open. */\n onOpen(cb: () => void): () => void\n /**\n * Fires with the close code (or `CLOSE_CODE_SYNTHETIC`) whenever the socket\n * goes away. `willReconnect` is false for terminal closes (`disconnect()`,\n * auth failure, no WebSocket implementation): queued work must reject\n * instead of riding a reconnect that will never come.\n */\n onClose(cb: (code: number, willReconnect: boolean) => void): () => void\n onRpcResponse(cb: (frame: RpcResponseFrame) => void): () => void\n}\n\ntype Listener<T> = (value: T) => void\n\nfunction emit<T>(listeners: Set<Listener<T>>, value: T): void {\n for (const cb of listeners) {\n try {\n cb(value)\n } catch {\n // listener errors must not break the channel\n }\n }\n}\n\nfunction resolveWebSocket(override?: typeof WebSocket): typeof WebSocket {\n if (override) return override\n if (\n typeof globalThis !== \"undefined\" &&\n typeof (globalThis as { WebSocket?: typeof WebSocket }).WebSocket ===\n \"function\"\n ) {\n return (globalThis as { WebSocket: typeof WebSocket }).WebSocket\n }\n throw new Error(\n \"uidex/cloud: global WebSocket is not available; pass a `WebSocketImpl` override\"\n )\n}\n\nexport function createRealtimeChannel(\n options: RealtimeChannelOptions\n): InternalRealtimeChannel {\n // Resolved lazily on the first connect attempt so channel construction\n // never throws on runtimes without a WebSocket global (SSR, old Node).\n let WS: typeof WebSocket | null = options.WebSocketImpl ?? null\n\n const presenceListeners = new Set<Listener<RealtimePresenceUser[]>>()\n const pinListeners = new Set<Listener<PinRecord>>()\n const pinArchivedListeners = new Set<Listener<string>>()\n const openListeners = new Set<Listener<void>>()\n const closeListeners = new Set<\n (code: number, willReconnect: boolean) => void\n >()\n const rpcListeners = new Set<Listener<RpcResponseFrame>>()\n\n let ws: WebSocket | null = null\n let state: RealtimeChannelState = \"disconnected\"\n let disposed = false\n let reconnectAttempts = 0\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null\n let identity: UserIdentity | null = null\n let route: string | null = null\n\n function emitClose(code: number, willReconnect: boolean): void {\n for (const cb of closeListeners) {\n try {\n cb(code, willReconnect)\n } catch {\n // listener errors must not break the channel\n }\n }\n }\n\n function clearReconnectTimer(): void {\n if (reconnectTimer !== null) {\n clearTimeout(reconnectTimer)\n reconnectTimer = null\n }\n }\n\n function scheduleReconnect(): void {\n if (disposed) return\n const delay = Math.min(\n RECONNECT_INITIAL_MS * 2 ** reconnectAttempts,\n RECONNECT_MAX_MS\n )\n reconnectAttempts += 1\n state = \"connecting\"\n clearReconnectTimer()\n reconnectTimer = setTimeout(() => {\n reconnectTimer = null\n openSocket()\n }, delay)\n }\n\n function handleMessage(event: MessageEvent): void {\n if (typeof event.data !== \"string\") return\n let parsed: unknown\n try {\n parsed = JSON.parse(event.data)\n } catch {\n return\n }\n if (!parsed || typeof parsed !== \"object\") return\n const msg = parsed as { type?: unknown }\n\n if (msg.type === \"rpc:res\") {\n const frame = parseRpcResponseFrame(parsed)\n if (frame) emit(rpcListeners, frame)\n return\n }\n\n if (msg.type === \"presence\") {\n const p = msg as { users?: unknown }\n if (!Array.isArray(p.users)) return\n const users: RealtimePresenceUser[] = []\n for (const raw of p.users) {\n if (!raw || typeof raw !== \"object\") continue\n const u = raw as Partial<RealtimePresenceUser>\n if (typeof u.userId !== \"string\" || typeof u.name !== \"string\") continue\n users.push({\n userId: u.userId,\n name: u.name,\n avatar: typeof u.avatar === \"string\" ? u.avatar : null,\n })\n }\n emit(presenceListeners, users)\n return\n }\n\n if (msg.type === \"pin:archived\") {\n const p = msg as { reportId?: unknown }\n if (typeof p.reportId !== \"string\") return\n emit(pinArchivedListeners, p.reportId)\n return\n }\n\n if (msg.type === \"pin\") {\n // Wire format is the relay's PinPayload (apps/app/src/lib/relay.ts):\n // `reportId` + legacy field names; map to the API PinRecord shape.\n const p = msg as Record<string, unknown>\n if (\n typeof p.reportId !== \"string\" ||\n !p.elementRef ||\n typeof p.elementRef !== \"object\" ||\n typeof (p.elementRef as { kind?: unknown }).kind !== \"string\" ||\n typeof (p.elementRef as { id?: unknown }).id !== \"string\" ||\n !p.author ||\n typeof p.author !== \"object\" ||\n typeof p.body !== \"string\" ||\n typeof p.reportType !== \"string\" ||\n typeof p.reportSeverity !== \"string\" ||\n typeof p.createdAt !== \"string\"\n ) {\n return\n }\n const author = p.author as Record<string, unknown>\n const elRef = p.elementRef as { kind: string; id: string }\n const pin: PinRecord = {\n id: p.reportId,\n entity: `${elRef.kind}:${elRef.id}`,\n reporter: {\n name: typeof author.name === \"string\" ? author.name : undefined,\n email: typeof author.email === \"string\" ? author.email : undefined,\n },\n body: p.body,\n type: p.reportType as PinRecord[\"type\"],\n severity: p.reportSeverity as PinRecord[\"severity\"],\n status: \"open\" as PinRecord[\"status\"],\n createdAt: p.createdAt,\n url: \"\",\n }\n emit(pinListeners, pin)\n return\n }\n }\n\n function sendRaw(frame: object): boolean {\n if (!ws || !WS || ws.readyState !== WS.OPEN) return false\n try {\n ws.send(JSON.stringify(frame))\n return true\n } catch {\n // socket may have closed mid-call\n return false\n }\n }\n\n function openSocket(): void {\n if (disposed) return\n state = \"connecting\"\n if (!WS) {\n try {\n WS = resolveWebSocket(options.WebSocketImpl)\n } catch {\n // No implementation will ever appear; reconnecting is pointless.\n state = \"disconnected\"\n emitClose(CLOSE_CODE_SYNTHETIC, false)\n return\n }\n }\n let url: string\n try {\n url = options.buildUrl()\n } catch {\n state = \"disconnected\"\n emitClose(CLOSE_CODE_SYNTHETIC, false)\n return\n }\n let socket: WebSocket\n try {\n socket = new WS(url)\n } catch {\n if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {\n state = \"disconnected\"\n emitClose(CLOSE_CODE_SYNTHETIC, false)\n return\n }\n emitClose(CLOSE_CODE_SYNTHETIC, true)\n scheduleReconnect()\n return\n }\n ws = socket\n\n socket.addEventListener(\"open\", () => {\n if (ws !== socket) return\n state = \"connected\"\n reconnectAttempts = 0\n // Replay session frames before notifying open listeners so RPC frames\n // queued in `./rpc` are always flushed after identify/join.\n if (identity) {\n sendRaw({\n type: \"identify\",\n userId: identity.id,\n ...(identity.name ? { name: identity.name } : {}),\n ...(identity.avatar ? { avatar: identity.avatar } : {}),\n })\n }\n if (route !== null) {\n sendRaw({ type: \"join\", route })\n }\n emit(openListeners, undefined)\n })\n\n socket.addEventListener(\"message\", (event) => {\n if (ws !== socket) return\n handleMessage(event as MessageEvent)\n })\n\n socket.addEventListener(\"close\", (event) => {\n if (ws !== socket) return\n ws = null\n const code = (event as CloseEvent).code\n if (\n disposed ||\n code === CLOSE_CODE_AUTH_FAILED ||\n reconnectAttempts >= MAX_RECONNECT_ATTEMPTS\n ) {\n // Terminal: queued RPCs must reject now instead of waiting on a\n // reconnect that will never come.\n state = \"disconnected\"\n emitClose(code, false)\n return\n }\n emitClose(code, true)\n scheduleReconnect()\n })\n\n socket.addEventListener(\"error\", () => {\n // close fires after error; reconnect handled there\n })\n }\n\n function connect(): void {\n if (disposed) {\n disposed = false\n }\n if (ws) return\n if (state === \"connecting\") return\n reconnectAttempts = 0\n openSocket()\n }\n\n function disconnect(): void {\n disposed = true\n clearReconnectTimer()\n state = \"disconnected\"\n // Forget the session so a later revival (any RPC call re-connects the\n // socket) is anonymous instead of replaying identify/join for a user who\n // explicitly disconnected.\n identity = null\n route = null\n if (ws) {\n const socket = ws\n // Null first so the socket's own close event is ignored; we emit one\n // synthetic close instead.\n ws = null\n try {\n socket.close(1000)\n } catch {\n // ignore\n }\n }\n // Notify even when no socket was open (e.g. reconnect backoff armed):\n // the RPC client must reject queued requests rather than strand them.\n emitClose(CLOSE_CODE_SYNTHETIC, false)\n }\n\n function clearSession(): void {\n identity = null\n route = null\n }\n\n function identify(user: UserIdentity): void {\n identity = user\n sendRaw({\n type: \"identify\",\n userId: user.id,\n ...(user.name ? { name: user.name } : {}),\n ...(user.avatar ? { avatar: user.avatar } : {}),\n })\n }\n\n function joinRoute(nextRoute: string): void {\n route = nextRoute\n emit(presenceListeners, [])\n sendRaw({ type: \"join\", route: nextRoute })\n }\n\n function onPresence(cb: Listener<RealtimePresenceUser[]>): () => void {\n presenceListeners.add(cb)\n return () => {\n presenceListeners.delete(cb)\n }\n }\n\n function onPin(cb: Listener<PinRecord>): () => void {\n pinListeners.add(cb)\n return () => {\n pinListeners.delete(cb)\n }\n }\n\n function onPinArchived(cb: Listener<string>): () => void {\n pinArchivedListeners.add(cb)\n return () => {\n pinArchivedListeners.delete(cb)\n }\n }\n\n function onOpen(cb: Listener<void>): () => void {\n openListeners.add(cb)\n return () => {\n openListeners.delete(cb)\n }\n }\n\n function onClose(\n cb: (code: number, willReconnect: boolean) => void\n ): () => void {\n closeListeners.add(cb)\n return () => {\n closeListeners.delete(cb)\n }\n }\n\n function onRpcResponse(cb: Listener<RpcResponseFrame>): () => void {\n rpcListeners.add(cb)\n return () => {\n rpcListeners.delete(cb)\n }\n }\n\n return {\n get state() {\n return state\n },\n connect,\n disconnect,\n identify,\n clearSession,\n joinRoute,\n sendFrame: sendRaw,\n onPresence,\n onPin,\n onPinArchived,\n onOpen,\n onClose,\n onRpcResponse,\n }\n}\n","import type {\n ArchiveReason,\n ReportListResponse,\n ReportPayload,\n ReportResult,\n IngestConfig,\n PinRecord,\n} from \"@uidex/api-client\"\nimport type { UserIdentity } from \"../shared/user\"\n\nexport type {\n ArchiveReason,\n ReportListResponse,\n ReportPayload,\n ReportResult,\n IngestConfig,\n PinRecord,\n} from \"@uidex/api-client\"\n\nexport const DEFAULT_CLOUD_ENDPOINT = \"https://app.uidex.dev\"\n\nexport interface CloudOptions {\n projectKey: string\n endpoint?: string\n /** @deprecated Unused — the adapter talks RPC over WebSocket. Kept for API compatibility. */\n fetch?: typeof fetch\n /** Optional WebSocket constructor override (tests, Node). Defaults to `globalThis.WebSocket`. */\n WebSocketImpl?: typeof WebSocket\n git?: { branch?: string; commit?: string }\n}\n\nexport class CloudError extends Error {\n readonly status: number\n readonly retryAfter?: number\n readonly details?: unknown\n\n constructor(\n message: string,\n options: { status: number; retryAfter?: number; details?: unknown }\n ) {\n super(message)\n this.name = \"CloudError\"\n this.status = options.status\n this.retryAfter = options.retryAfter\n this.details = options.details\n }\n}\n\nexport type RealtimePresenceUser = {\n userId: string\n name: string\n avatar: string | null\n}\n\nexport type RealtimeChannelState = \"connecting\" | \"connected\" | \"disconnected\"\n\nexport interface RealtimeConnectOpts {\n user: UserIdentity\n route: string\n}\n\nexport interface RealtimeChannel {\n readonly state: RealtimeChannelState\n connect(): void\n disconnect(): void\n joinRoute(route: string): void\n onPresence(cb: (users: RealtimePresenceUser[]) => void): () => void\n onPin(cb: (pin: PinRecord) => void): () => void\n /** Optional so pre-existing channel stubs keep satisfying the interface. */\n onPinArchived?(cb: (reportId: string) => void): () => void\n}\n\nexport interface CloudAdapter<\n TPayload = ReportPayload,\n TResult = ReportResult,\n TIntegrations = {\n getConfig(): Promise<IngestConfig>\n getCachedConfig(): IngestConfig | null\n },\n> {\n readonly reports: {\n submit(payload: TPayload): Promise<TResult>\n list?(opts?: { page?: number; limit?: number }): Promise<ReportListResponse>\n }\n readonly integrations: TIntegrations\n readonly realtime: {\n connect(opts: RealtimeConnectOpts): RealtimeChannel\n }\n readonly pins: {\n list(params: { route?: string; entities?: string }): Promise<PinRecord[]>\n /**\n * Pin records travel without their screenshot (kept out of `pins.list`\n * so multi-MB frames don't stall the shared socket); the report detail\n * view fetches it lazily through here. Optional for host-provided\n * adapters that inline screenshots on the record instead.\n */\n screenshot?(reportId: string): Promise<string | null>\n close(reportId: string, reason?: ArchiveReason): Promise<void>\n }\n /** Closes the adapter's shared socket. Any later RPC call revives it. */\n dispose?(): void\n}\n","import type {\n RpcMethod,\n RpcParamsMap,\n RpcRequestFrame,\n RpcResultMap,\n} from \"./protocol\"\nimport {\n CLOSE_CODE_AUTH_FAILED,\n type InternalRealtimeChannel,\n} from \"./realtime\"\nimport { CloudError } from \"./types\"\n\nexport const DEFAULT_RPC_TIMEOUT_MS = 30_000\n/**\n * `reports.submit` frames embed an inline base64 screenshot (multi-MB is\n * routine) and the server persists the report even when the client stops\n * waiting — a timeout there means a duplicate on retry, so give slow uplinks\n * far more headroom than a quick list/config call.\n */\nexport const SUBMIT_RPC_TIMEOUT_MS = 120_000\n\ntype PendingEntry = {\n resolve: (data: unknown) => void\n reject: (err: unknown) => void\n timer: ReturnType<typeof setTimeout>\n}\n\ntype QueuedRequest = {\n frame: RpcRequestFrame\n entry: PendingEntry\n}\n\nexport interface RpcClient {\n call<M extends RpcMethod>(\n method: M,\n params: RpcParamsMap[M],\n opts?: { timeoutMs?: number }\n ): Promise<RpcResultMap[M]>\n}\n\nfunction authFailedError(): CloudError {\n return new CloudError(\"WebSocket authentication failed\", { status: 401 })\n}\n\n/**\n * Request/response RPC over the realtime channel's socket. Requests made\n * while the socket is closed are queued and flushed on (re)open — after the\n * channel's identify/join replay, because the channel emits `onOpen` last.\n */\nexport function createRpcClient(\n channel: InternalRealtimeChannel,\n options?: { timeoutMs?: number }\n): RpcClient {\n const timeoutMs = options?.timeoutMs ?? DEFAULT_RPC_TIMEOUT_MS\n const idSuffix = Math.random().toString(36).slice(2, 8)\n let idCounter = 0\n\n /** Sent, awaiting an `rpc:res` frame. */\n const pending = new Map<string, PendingEntry>()\n /** Not yet sent; rides the next (re)open. */\n const queue: QueuedRequest[] = []\n /** Set after a 4001 close — the channel will not reconnect, so fail fast. */\n let terminalError: CloudError | null = null\n\n function settlePending(err: CloudError): void {\n for (const entry of pending.values()) {\n clearTimeout(entry.timer)\n entry.reject(err)\n }\n pending.clear()\n }\n\n channel.onOpen(() => {\n while (queue.length > 0) {\n const next = queue[0]\n if (!channel.sendFrame(next.frame)) return\n queue.shift()\n pending.set(next.frame.id, next.entry)\n }\n })\n\n function rejectQueue(err: CloudError): void {\n for (const queued of queue) {\n clearTimeout(queued.entry.timer)\n queued.entry.reject(err)\n }\n queue.length = 0\n }\n\n channel.onClose((code, willReconnect) => {\n if (code === CLOSE_CODE_AUTH_FAILED) {\n const err = authFailedError()\n terminalError = err\n settlePending(err)\n rejectQueue(err)\n return\n }\n // In-flight requests are lost with the socket; queued ones ride the\n // reconnect — unless the close is terminal (disconnect()/dispose(), no\n // WebSocket implementation), where waiting would only strand them until\n // the timeout.\n settlePending(new CloudError(\"Connection lost\", { status: 0 }))\n if (!willReconnect) {\n rejectQueue(new CloudError(\"Connection lost\", { status: 0 }))\n }\n })\n\n channel.onRpcResponse((frame) => {\n const entry = pending.get(frame.id)\n if (!entry) return // late response after timeout/close — ignore\n pending.delete(frame.id)\n clearTimeout(entry.timer)\n if (frame.ok) {\n entry.resolve(frame.data)\n } else {\n entry.reject(\n new CloudError(frame.error, {\n status: frame.status,\n retryAfter: frame.retryAfter,\n details: frame.details,\n })\n )\n }\n })\n\n function call<M extends RpcMethod>(\n method: M,\n params: RpcParamsMap[M],\n opts?: { timeoutMs?: number }\n ): Promise<RpcResultMap[M]> {\n if (terminalError) return Promise.reject(terminalError)\n const callTimeoutMs = opts?.timeoutMs ?? timeoutMs\n return new Promise<RpcResultMap[M]>((resolve, reject) => {\n idCounter += 1\n const id = `${idCounter.toString(36)}-${idSuffix}`\n const frame: RpcRequestFrame = {\n type: \"rpc\",\n id,\n method,\n ...(params !== undefined ? { params } : {}),\n }\n const timer = setTimeout(() => {\n pending.delete(id)\n const queuedIndex = queue.findIndex((q) => q.frame.id === id)\n if (queuedIndex !== -1) queue.splice(queuedIndex, 1)\n reject(new CloudError(\"Request timed out\", { status: 0 }))\n }, callTimeoutMs)\n const entry: PendingEntry = {\n resolve: resolve as (data: unknown) => void,\n reject,\n timer,\n }\n if (channel.sendFrame(frame)) {\n pending.set(id, entry)\n } else {\n queue.push({ frame, entry })\n channel.connect()\n }\n })\n }\n\n return { call }\n}\n","import { createRealtimeChannel } from \"./realtime\"\nimport { SCREENSHOT_CHUNK_BYTES, SCREENSHOT_INLINE_MAX_BYTES } from \"./protocol\"\nimport { createRpcClient, SUBMIT_RPC_TIMEOUT_MS } from \"./rpc\"\nimport {\n DEFAULT_CLOUD_ENDPOINT,\n type ArchiveReason,\n type CloudAdapter,\n type CloudOptions,\n type ReportListResponse,\n type ReportPayload,\n type ReportResult,\n type IngestConfig,\n type PinRecord,\n type RealtimeChannel,\n type RealtimeConnectOpts,\n} from \"./types\"\n\nfunction trimEndpoint(endpoint: string): string {\n return endpoint.endsWith(\"/\") ? endpoint.slice(0, -1) : endpoint\n}\n\n/** Unique id grouping a streamed screenshot's chunks (server also scopes it by project). */\nfunction nextUploadId(): string {\n return `up-${crypto.randomUUID()}`\n}\n\nexport function cloud(options: CloudOptions): CloudAdapter {\n let cachedConfig: Promise<IngestConfig> | null = null\n let resolvedConfig: IngestConfig | null = null\n\n const projectKey = options.projectKey\n if (!projectKey) {\n throw new Error(\"uidex/cloud: `projectKey` is required\")\n }\n const endpoint = trimEndpoint(options.endpoint ?? DEFAULT_CLOUD_ENDPOINT)\n const git = options.git\n\n function buildWsUrl(): string {\n const httpToWs = endpoint\n .replace(/^http:/, \"ws:\")\n .replace(/^https:/, \"wss:\")\n const params = new URLSearchParams()\n params.set(\"key\", projectKey)\n return `${httpToWs}/ws?${params.toString()}`\n }\n\n // One socket per adapter: realtime events and RPC share this channel.\n const channel = createRealtimeChannel({\n buildUrl: buildWsUrl,\n ...(options.WebSocketImpl ? { WebSocketImpl: options.WebSocketImpl } : {}),\n })\n const rpc = createRpcClient(channel)\n // Eager presence-invisible connect + config prefetch happen in the browser\n // only (or with an explicit WebSocketImpl): `use client` components still\n // construct adapters during SSR renders, which must stay side-effect free.\n // RPC calls connect on demand, so server-side usage still works.\n const eagerConnect =\n options.WebSocketImpl !== undefined ||\n (typeof window !== \"undefined\" && typeof WebSocket === \"function\")\n\n async function submit(payload: ReportPayload): Promise<ReportResult> {\n const enriched =\n git?.branch || git?.commit\n ? {\n ...payload,\n context: {\n ...payload.context,\n git: payload.context?.git ?? {\n branch: git.branch,\n commit: git.commit,\n },\n },\n }\n : payload\n\n const screenshot = enriched.screenshot\n if (\n typeof screenshot === \"string\" &&\n screenshot.length > SCREENSHOT_INLINE_MAX_BYTES\n ) {\n // A screenshot this large would blow the relay's WS `maxPayload` as a\n // single frame (the server drops it → close 1009 → \"Connection lost\").\n // Stream it as a sequence of sub-cap `screenshot.chunk` frames, then\n // submit the report referencing the upload; the server reassembles it.\n //\n // Send sequentially (await each ack) rather than all at once: this bounds\n // the WS send buffer and live substring memory to ~one chunk, and starts\n // each chunk's timeout when it is actually sent (a parallel burst would\n // start every timer at once, so a slow uplink times out the tail).\n const uploadId = nextUploadId()\n const total = Math.ceil(screenshot.length / SCREENSHOT_CHUNK_BYTES)\n for (let seq = 0; seq < total; seq++) {\n const data = screenshot.slice(\n seq * SCREENSHOT_CHUNK_BYTES,\n (seq + 1) * SCREENSHOT_CHUNK_BYTES\n )\n await rpc.call(\n \"screenshot.chunk\",\n { uploadId, seq, total, data },\n { timeoutMs: SUBMIT_RPC_TIMEOUT_MS }\n )\n }\n return rpc.call(\n \"reports.submit\",\n { ...enriched, screenshot: undefined, screenshotUploadId: uploadId },\n { timeoutMs: SUBMIT_RPC_TIMEOUT_MS }\n )\n }\n\n return rpc.call(\"reports.submit\", enriched, {\n timeoutMs: SUBMIT_RPC_TIMEOUT_MS,\n })\n }\n\n function startFetch(): Promise<IngestConfig> {\n const promise = rpc.call(\"config.get\", undefined)\n cachedConfig = promise\n promise.then(\n (config) => {\n resolvedConfig = config\n },\n () => {\n // Drop the rejected promise so the next getConfig() retries.\n if (cachedConfig === promise) cachedConfig = null\n }\n )\n return promise\n }\n\n if (eagerConnect) {\n channel.connect()\n startFetch()\n }\n\n function getConfig(): Promise<IngestConfig> {\n return cachedConfig ?? startFetch()\n }\n\n function getCachedConfig(): IngestConfig | null {\n return resolvedConfig\n }\n\n function connectRealtime(opts: RealtimeConnectOpts): RealtimeChannel {\n if (!opts || !opts.user || typeof opts.user.id !== \"string\") {\n throw new TypeError(\"uidex/cloud: realtime.connect requires `user.id`\")\n }\n let currentRoute = opts.route\n // Facade-level disconnected flag: the adapter's shared socket outlives a\n // facade disconnect(), so `state` must not mirror the channel's.\n let detached = false\n // Listeners registered through this facade. disconnect() detaches them\n // from the shared channel (an unmounted surface stops receiving\n // presence/pin events) but keeps the registrations, so a later connect()\n // re-attaches them — matching the REST-era channel semantics.\n type Registration = {\n attach: () => () => void\n detach: (() => void) | null\n }\n const registrations = new Set<Registration>()\n function register(attach: () => () => void): () => void {\n const reg: Registration = { attach, detach: detached ? null : attach() }\n registrations.add(reg)\n return () => {\n registrations.delete(reg)\n reg.detach?.()\n reg.detach = null\n }\n }\n function arm(): void {\n channel.identify(opts.user)\n channel.joinRoute(currentRoute)\n channel.connect()\n }\n arm()\n return {\n get state() {\n return detached ? \"disconnected\" : channel.state\n },\n // Re-arms identity/route and re-attaches this facade's listeners so an\n // explicit disconnect() -> connect() restores presence and pin events.\n connect: () => {\n if (detached) {\n detached = false\n for (const reg of registrations) reg.detach ??= reg.attach()\n }\n arm()\n },\n // Soft leave: drop presence and the stored identity (so a later\n // reopen/revival stays anonymous) but keep the adapter's shared socket\n // alive — in-flight and future RPCs (reports.submit, pins.*) must\n // survive a surface unmount. Socket teardown is reserved for\n // CloudAdapter.dispose().\n disconnect: () => {\n detached = true\n channel.sendFrame({ type: \"leave\" })\n channel.clearSession()\n for (const reg of registrations) {\n reg.detach?.()\n reg.detach = null\n }\n },\n joinRoute: (route: string) => {\n currentRoute = route\n channel.joinRoute(route)\n },\n onPresence: (cb) => register(() => channel.onPresence(cb)),\n onPin: (cb) => register(() => channel.onPin(cb)),\n onPinArchived: (cb) => register(() => channel.onPinArchived(cb)),\n }\n }\n\n async function listPins(params: {\n route?: string\n entities?: string\n }): Promise<PinRecord[]> {\n // Mirror the REST route's split exactly (no trim, send present-but-empty\n // arrays): tokens travel verbatim and `entities: \",\"` still resolves to\n // an empty pin list instead of a 400.\n const entities = params.entities\n ? params.entities.split(\",\").filter(Boolean)\n : undefined\n const result = await rpc.call(\"pins.list\", {\n ...(params.route !== undefined ? { route: params.route } : {}),\n ...(entities ? { entities } : {}),\n })\n return result.pins\n }\n\n async function getPinScreenshot(reportId: string): Promise<string | null> {\n const result = await rpc.call(\"pins.screenshot\", { reportId })\n return result.screenshot\n }\n\n async function closePin(\n reportId: string,\n reason?: ArchiveReason\n ): Promise<void> {\n await rpc.call(\"pins.archive\", { reportId, ...(reason ? { reason } : {}) })\n }\n\n function listReports(opts?: {\n page?: number\n limit?: number\n }): Promise<ReportListResponse> {\n return rpc.call(\"reports.list\", opts ?? {})\n }\n\n function dispose(): void {\n // Pending and queued RPCs reject through the channel's close notification.\n channel.disconnect()\n }\n\n return {\n reports: { submit, list: listReports },\n integrations: { getConfig, getCachedConfig },\n realtime: { connect: connectRealtime },\n pins: { list: listPins, screenshot: getPinScreenshot, close: closePin },\n dispose,\n }\n}\n"],"mappings":"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","names":[]}
@@ -11,7 +11,10 @@ declare const DEFAULT_CLOUD_ENDPOINT = "https://app.uidex.dev";
11
11
  interface CloudOptions {
12
12
  projectKey: string;
13
13
  endpoint?: string;
14
+ /** @deprecated Unused — the adapter talks RPC over WebSocket. Kept for API compatibility. */
14
15
  fetch?: typeof fetch;
16
+ /** Optional WebSocket constructor override (tests, Node). Defaults to `globalThis.WebSocket`. */
17
+ WebSocketImpl?: typeof WebSocket;
15
18
  git?: {
16
19
  branch?: string;
17
20
  commit?: string;
@@ -44,6 +47,8 @@ interface RealtimeChannel {
44
47
  joinRoute(route: string): void;
45
48
  onPresence(cb: (users: RealtimePresenceUser[]) => void): () => void;
46
49
  onPin(cb: (pin: PinRecord) => void): () => void;
50
+ /** Optional so pre-existing channel stubs keep satisfying the interface. */
51
+ onPinArchived?(cb: (reportId: string) => void): () => void;
47
52
  }
48
53
  interface CloudAdapter<TPayload = ReportPayload, TResult = ReportResult, TIntegrations = {
49
54
  getConfig(): Promise<IngestConfig>;
@@ -65,18 +70,201 @@ interface CloudAdapter<TPayload = ReportPayload, TResult = ReportResult, TIntegr
65
70
  route?: string;
66
71
  entities?: string;
67
72
  }): Promise<PinRecord[]>;
68
- archive(reportId: string, reason?: ArchiveReason): Promise<void>;
73
+ /**
74
+ * Pin records travel without their screenshot (kept out of `pins.list`
75
+ * so multi-MB frames don't stall the shared socket); the report detail
76
+ * view fetches it lazily through here. Optional for host-provided
77
+ * adapters that inline screenshots on the record instead.
78
+ */
79
+ screenshot?(reportId: string): Promise<string | null>;
80
+ close(reportId: string, reason?: ArchiveReason): Promise<void>;
69
81
  };
82
+ /** Closes the adapter's shared socket. Any later RPC call revives it. */
83
+ dispose?(): void;
70
84
  }
71
85
 
72
86
  declare function cloud(options: CloudOptions): CloudAdapter;
73
87
 
88
+ /**
89
+ * WS RPC protocol shared by the SDK (`packages/sdk/src/cloud`) and the relay
90
+ * server (`apps/app/src/lib/relay-handler.ts`, type-only import from
91
+ * "uidex/cloud"). These frames ride the same socket as the existing
92
+ * fire-and-forget messages (`cursor`, `join`, `presence`, `pin`); peers that
93
+ * predate a frame type silently drop it.
94
+ */
95
+ declare const RPC_METHODS: readonly ["reports.submit", "reports.list", "config.get", "pins.list", "pins.screenshot", "pins.archive", "screenshot.chunk"];
96
+ type RpcMethod = (typeof RPC_METHODS)[number];
97
+ /**
98
+ * Screenshots whose base64 data-URL length is at or below this ride inline in
99
+ * the `reports.submit` frame (the common case). Larger ones are streamed via
100
+ * `screenshot.chunk` so no single frame approaches the relay's 25 MiB WS
101
+ * `maxPayload` — an oversized frame is dropped server-side (close 1009), which
102
+ * the SDK surfaces as "Connection lost".
103
+ */
104
+ declare const SCREENSHOT_INLINE_MAX_BYTES: number;
105
+ /** Per-chunk size when a screenshot is streamed. Each `screenshot.chunk` frame stays well under `maxPayload`. */
106
+ declare const SCREENSHOT_CHUNK_BYTES: number;
107
+ /** One streamed screenshot fragment. Parts are keyed by `seq` and reassembled in order server-side. */
108
+ type ScreenshotChunkParams = {
109
+ /** Client-generated id grouping a screenshot's chunks; referenced by `reports.submit` as `screenshotUploadId`. */
110
+ uploadId: string;
111
+ /** 0-based index of this part. */
112
+ seq: number;
113
+ /** Total number of parts in this upload. */
114
+ total: number;
115
+ /** A slice of the screenshot data URL. */
116
+ data: string;
117
+ };
118
+ /**
119
+ * `reports.submit` params. Either the screenshot rides inline (`screenshot`) or,
120
+ * for large captures, it is streamed via `screenshot.chunk` and referenced here
121
+ * by `screenshotUploadId` — the server reassembles it before persisting.
122
+ */
123
+ type ReportSubmitParams = ReportPayload & {
124
+ screenshotUploadId?: string;
125
+ };
126
+ interface RpcParamsMap {
127
+ "reports.submit": ReportSubmitParams;
128
+ "reports.list": {
129
+ page?: number;
130
+ limit?: number;
131
+ };
132
+ "config.get": undefined;
133
+ "pins.list": {
134
+ route?: string;
135
+ entities?: string[];
136
+ };
137
+ "pins.screenshot": {
138
+ reportId: string;
139
+ };
140
+ "pins.archive": {
141
+ reportId: string;
142
+ reason?: ArchiveReason;
143
+ };
144
+ "screenshot.chunk": ScreenshotChunkParams;
145
+ }
146
+ interface RpcResultMap {
147
+ "reports.submit": ReportResult;
148
+ "reports.list": ReportListResponse;
149
+ "config.get": IngestConfig;
150
+ "pins.list": {
151
+ pins: PinRecord[];
152
+ };
153
+ "pins.screenshot": {
154
+ screenshot: string | null;
155
+ };
156
+ "pins.archive": {
157
+ ok: true;
158
+ };
159
+ /** Acks a stored chunk; `received` is how many distinct parts have arrived. */
160
+ "screenshot.chunk": {
161
+ received: number;
162
+ };
163
+ }
164
+ type RpcRequestFrame = {
165
+ type: "rpc";
166
+ id: string;
167
+ method: RpcMethod;
168
+ params?: unknown;
169
+ };
170
+ type RpcOkFrame = {
171
+ type: "rpc:res";
172
+ id: string;
173
+ ok: true;
174
+ data: unknown;
175
+ };
176
+ type RpcErrFrame = {
177
+ type: "rpc:res";
178
+ id: string;
179
+ ok: false;
180
+ /** HTTP-equivalent status so CloudError semantics survive the transport. */
181
+ status: number;
182
+ error: string;
183
+ /** Seconds, present on 429 responses (mirrors the Retry-After header). */
184
+ retryAfter?: number;
185
+ details?: unknown;
186
+ };
187
+ type RpcResponseFrame = RpcOkFrame | RpcErrFrame;
188
+ /**
189
+ * Sent by the SDK after `realtime.connect(opts)` to attach a user identity to
190
+ * an initially anonymous connection. The server marks the connection visible
191
+ * in presence from this point on. Connections that never identify stay
192
+ * RPC-only: excluded from presence, no room auto-join.
193
+ */
194
+ type IdentifyFrame = {
195
+ type: "identify";
196
+ userId: string;
197
+ name?: string;
198
+ avatar?: string;
199
+ };
200
+ /**
201
+ * Reverse of `IdentifyFrame`: sent when a realtime consumer disconnects
202
+ * (e.g. the uidex surface unmounts) while the adapter keeps using the socket
203
+ * for RPC. The server drops the connection from presence and its room but
204
+ * keeps the socket open.
205
+ */
206
+ type LeaveFrame = {
207
+ type: "leave";
208
+ };
209
+ /**
210
+ * Server→client broadcast when a pin's report leaves the open statuses
211
+ * (closed via the SDK, the dashboard, or the REST route). Sent to the room of
212
+ * the report's route so live overlays drop the pin without a refresh.
213
+ */
214
+ type PinArchivedFrame = {
215
+ type: "pin:archived";
216
+ reportId: string;
217
+ };
218
+ declare function isRpcMethod(value: unknown): value is RpcMethod;
219
+ /** Client-side guard for inbound `rpc:res` frames (server validates inbound `rpc` frames itself). */
220
+ declare function parseRpcResponseFrame(msg: unknown): RpcResponseFrame | null;
221
+
74
222
  type RealtimeChannelOptions = {
75
- /** Builds the WebSocket URL on each (re)connect attempt. Allows the caller to refresh `route` query params. */
223
+ /** Builds the WebSocket URL on each (re)connect attempt. Only needs the project key identity and route ride dedicated frames. */
76
224
  buildUrl: () => string;
77
225
  /** Optional WebSocket constructor override (testing). Defaults to `globalThis.WebSocket`. */
78
226
  WebSocketImpl?: typeof WebSocket;
79
227
  };
80
- declare function createRealtimeChannel(options: RealtimeChannelOptions): RealtimeChannel;
228
+ /**
229
+ * Superset of the public `RealtimeChannel` used internally by the cloud
230
+ * adapter: the RPC client (`./rpc`) shares the same socket and needs raw
231
+ * frame access plus open/close notifications.
232
+ */
233
+ interface InternalRealtimeChannel extends RealtimeChannel {
234
+ /** Required here (optional on the public interface only for channel stubs). */
235
+ onPinArchived(cb: (reportId: string) => void): () => void;
236
+ /** Stores the identity and (re)sends the `identify` frame on every open. */
237
+ identify(user: UserIdentity): void;
238
+ /** Drops the stored identity/route without touching the socket (soft leave). */
239
+ clearSession(): void;
240
+ /** Sends a JSON frame if the socket is OPEN. Returns false otherwise. */
241
+ sendFrame(frame: object): boolean;
242
+ /** Fires after the channel finished its identify/join replay on (re)open. */
243
+ onOpen(cb: () => void): () => void;
244
+ /**
245
+ * Fires with the close code (or `CLOSE_CODE_SYNTHETIC`) whenever the socket
246
+ * goes away. `willReconnect` is false for terminal closes (`disconnect()`,
247
+ * auth failure, no WebSocket implementation): queued work must reject
248
+ * instead of riding a reconnect that will never come.
249
+ */
250
+ onClose(cb: (code: number, willReconnect: boolean) => void): () => void;
251
+ onRpcResponse(cb: (frame: RpcResponseFrame) => void): () => void;
252
+ }
253
+ declare function createRealtimeChannel(options: RealtimeChannelOptions): InternalRealtimeChannel;
254
+
255
+ declare const DEFAULT_RPC_TIMEOUT_MS = 30000;
256
+ interface RpcClient {
257
+ call<M extends RpcMethod>(method: M, params: RpcParamsMap[M], opts?: {
258
+ timeoutMs?: number;
259
+ }): Promise<RpcResultMap[M]>;
260
+ }
261
+ /**
262
+ * Request/response RPC over the realtime channel's socket. Requests made
263
+ * while the socket is closed are queued and flushed on (re)open — after the
264
+ * channel's identify/join replay, because the channel emits `onOpen` last.
265
+ */
266
+ declare function createRpcClient(channel: InternalRealtimeChannel, options?: {
267
+ timeoutMs?: number;
268
+ }): RpcClient;
81
269
 
82
- export { type CloudAdapter, CloudError, type CloudOptions, DEFAULT_CLOUD_ENDPOINT, type RealtimeChannel, type RealtimeChannelState, type RealtimeConnectOpts, type RealtimePresenceUser, cloud, createRealtimeChannel };
270
+ export { type CloudAdapter, CloudError, type CloudOptions, DEFAULT_CLOUD_ENDPOINT, DEFAULT_RPC_TIMEOUT_MS, type IdentifyFrame, type InternalRealtimeChannel, type LeaveFrame, type PinArchivedFrame, RPC_METHODS, type RealtimeChannel, type RealtimeChannelState, type RealtimeConnectOpts, type RealtimePresenceUser, type ReportSubmitParams, type RpcClient, type RpcErrFrame, type RpcMethod, type RpcOkFrame, type RpcParamsMap, type RpcRequestFrame, type RpcResponseFrame, type RpcResultMap, SCREENSHOT_CHUNK_BYTES, SCREENSHOT_INLINE_MAX_BYTES, type ScreenshotChunkParams, cloud, createRealtimeChannel, createRpcClient, isRpcMethod, parseRpcResponseFrame };