@reddb-io/ui 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/LICENSE +17 -0
  2. package/bin/red-ui.js +259 -0
  3. package/build/CNAME +1 -0
  4. package/build/_app/env.js +1 -0
  5. package/build/_app/immutable/assets/0.C63jY5jX.css +1 -0
  6. package/build/_app/immutable/assets/10.Dblyk0Kr.css +1 -0
  7. package/build/_app/immutable/assets/EmptyState.RXcH7ryE.css +1 -0
  8. package/build/_app/immutable/assets/Workspace.BCzG1DsH.css +1 -0
  9. package/build/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
  10. package/build/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
  11. package/build/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
  12. package/build/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
  13. package/build/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
  14. package/build/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
  15. package/build/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
  16. package/build/_app/immutable/assets/jetbrains-mono-cyrillic-wght-normal.D73BlboJ.woff2 +0 -0
  17. package/build/_app/immutable/assets/jetbrains-mono-greek-wght-normal.Bw9x6K1M.woff2 +0 -0
  18. package/build/_app/immutable/assets/jetbrains-mono-latin-ext-wght-normal.DBQx-q_a.woff2 +0 -0
  19. package/build/_app/immutable/assets/jetbrains-mono-latin-wght-normal.B9CIFXIH.woff2 +0 -0
  20. package/build/_app/immutable/assets/jetbrains-mono-vietnamese-wght-normal.Bt-aOZkq.woff2 +0 -0
  21. package/build/_app/immutable/chunks/3j4cZ58b.js +1 -0
  22. package/build/_app/immutable/chunks/69_IOA4Y.js +1 -0
  23. package/build/_app/immutable/chunks/ALqaDrNQ.js +2 -0
  24. package/build/_app/immutable/chunks/BGcIe2ep.js +2 -0
  25. package/build/_app/immutable/chunks/BMHKbvs8.js +1 -0
  26. package/build/_app/immutable/chunks/BQHbwf54.js +916 -0
  27. package/build/_app/immutable/chunks/Bgso8cDH.js +311 -0
  28. package/build/_app/immutable/chunks/Bmgx1mfv.js +310 -0
  29. package/build/_app/immutable/chunks/BrvvOVaJ.js +1 -0
  30. package/build/_app/immutable/chunks/Bzak7iHL.js +1 -0
  31. package/build/_app/immutable/chunks/D9CTkMjK.js +1 -0
  32. package/build/_app/immutable/chunks/DIeogL5L.js +1 -0
  33. package/build/_app/immutable/chunks/DXY_Rxa7.js +113 -0
  34. package/build/_app/immutable/chunks/DhEqZVGG.js +1 -0
  35. package/build/_app/immutable/chunks/Ov8N-vxt.js +1 -0
  36. package/build/_app/immutable/chunks/oiO4RLqp.js +1 -0
  37. package/build/_app/immutable/entry/app.D_IVtpdv.js +2 -0
  38. package/build/_app/immutable/entry/start.3oC0UQ_Q.js +1 -0
  39. package/build/_app/immutable/nodes/0.CHPiFxDo.js +1 -0
  40. package/build/_app/immutable/nodes/1.BC_4YZ0h.js +1 -0
  41. package/build/_app/immutable/nodes/10.B7DMKSNc.js +60 -0
  42. package/build/_app/immutable/nodes/2.CAG5_cGP.js +1 -0
  43. package/build/_app/immutable/nodes/3.CAG5_cGP.js +1 -0
  44. package/build/_app/immutable/nodes/4.CAG5_cGP.js +1 -0
  45. package/build/_app/immutable/nodes/5.CAG5_cGP.js +1 -0
  46. package/build/_app/immutable/nodes/6.CAG5_cGP.js +1 -0
  47. package/build/_app/immutable/nodes/7.BZX9Iird.js +134 -0
  48. package/build/_app/immutable/nodes/8.CAG5_cGP.js +1 -0
  49. package/build/_app/immutable/nodes/9.CAG5_cGP.js +1 -0
  50. package/build/_app/version.json +1 -0
  51. package/build/favicon.png +0 -0
  52. package/build/index.html +59 -0
  53. package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js +26562 -0
  54. package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js.map +1 -0
  55. package/dist/embed/bundle-mjs-UuVRz3O4.js +3047 -0
  56. package/dist/embed/bundle-mjs-UuVRz3O4.js.map +1 -0
  57. package/dist/embed/core-QuRYvMt8.js +118 -0
  58. package/dist/embed/core-QuRYvMt8.js.map +1 -0
  59. package/dist/embed/index-B6ZQhCsu.js +577 -0
  60. package/dist/embed/index-B6ZQhCsu.js.map +1 -0
  61. package/dist/embed/index-client-ItCEU64C.js +3087 -0
  62. package/dist/embed/index-client-ItCEU64C.js.map +1 -0
  63. package/dist/embed/index-fad77a13.esm-BqPj0saH.js +1318 -0
  64. package/dist/embed/index-fad77a13.esm-BqPj0saH.js.map +1 -0
  65. package/dist/embed/index.js +11 -0
  66. package/dist/embed/index.js.map +1 -0
  67. package/dist/embed/sigma-node-border.esm-CGtlgqmo.js +432 -0
  68. package/dist/embed/sigma-node-border.esm-CGtlgqmo.js.map +1 -0
  69. package/dist/embed/sigma.esm-y0qgY-OC.js +2355 -0
  70. package/dist/embed/sigma.esm-y0qgY-OC.js.map +1 -0
  71. package/dist/embed/ui.css +1 -0
  72. package/package.json +101 -0
  73. package/src/app.css +238 -0
  74. package/src/lib/ActivityIndicator.svelte +100 -0
  75. package/src/lib/ClusterNode.svelte +119 -0
  76. package/src/lib/ClusterView.svelte +541 -0
  77. package/src/lib/CollectionHistory.svelte +266 -0
  78. package/src/lib/CollectionsView.svelte +14 -0
  79. package/src/lib/CommandPalette.svelte +237 -0
  80. package/src/lib/ConnectDropdown.svelte +222 -0
  81. package/src/lib/EmptyState.svelte +27 -0
  82. package/src/lib/LiveChanges.svelte +195 -0
  83. package/src/lib/MasterPasswordDialog.svelte +168 -0
  84. package/src/lib/PageHeader.svelte +32 -0
  85. package/src/lib/PendingChangesPanel.svelte +153 -0
  86. package/src/lib/QueryEditor.svelte +122 -0
  87. package/src/lib/ResultsPane.svelte +747 -0
  88. package/src/lib/SchemaTree.svelte +193 -0
  89. package/src/lib/SecurityView.svelte +255 -0
  90. package/src/lib/ShortcutOverlay.svelte +104 -0
  91. package/src/lib/Splash.svelte +46 -0
  92. package/src/lib/StatusBar.svelte +163 -0
  93. package/src/lib/SubNav.svelte +62 -0
  94. package/src/lib/Topbar.svelte +119 -0
  95. package/src/lib/Workspace.svelte +247 -0
  96. package/src/lib/activity.svelte.ts +91 -0
  97. package/src/lib/build-info.ts +41 -0
  98. package/src/lib/capability.ts +221 -0
  99. package/src/lib/cn.ts +6 -0
  100. package/src/lib/collection-catalog.ts +116 -0
  101. package/src/lib/collection-pages.ts +33 -0
  102. package/src/lib/connection-history.ts +301 -0
  103. package/src/lib/connections.svelte.ts +493 -0
  104. package/src/lib/embed/RedUiEmbedRoot.svelte +31 -0
  105. package/src/lib/embed/index.ts +234 -0
  106. package/src/lib/graph-loader/contract.ts +336 -0
  107. package/src/lib/graph-loader/drilldown.ts +334 -0
  108. package/src/lib/graph-loader/index.ts +13 -0
  109. package/src/lib/index.ts +114 -0
  110. package/src/lib/pending-changes.svelte.ts +149 -0
  111. package/src/lib/query-tabs.svelte.ts +130 -0
  112. package/src/lib/reddb/boot-params.ts +254 -0
  113. package/src/lib/reddb/cdc-stream-client.ts +234 -0
  114. package/src/lib/reddb/client.ts +698 -0
  115. package/src/lib/reddb/connection-bootstrap.ts +260 -0
  116. package/src/lib/reddb/connection-provider.contract.ts +72 -0
  117. package/src/lib/reddb/connection-provider.ts +72 -0
  118. package/src/lib/reddb/index.ts +14 -0
  119. package/src/lib/reddb/injected-client-provider.ts +76 -0
  120. package/src/lib/reddb/local-url-provider.ts +245 -0
  121. package/src/lib/reddb/secure-store.ts +41 -0
  122. package/src/lib/reddb/server-capabilities.ts +71 -0
  123. package/src/lib/reddb/tauri-encrypted-store.ts +64 -0
  124. package/src/lib/reddb/transports.ts +89 -0
  125. package/src/lib/reddb/types.ts +32 -0
  126. package/src/lib/reddb/uri.ts +54 -0
  127. package/src/lib/reddb/web-encrypted-store.ts +201 -0
  128. package/src/lib/renderers/DiffRenderer.svelte +184 -0
  129. package/src/lib/renderers/DocumentRenderer.svelte +134 -0
  130. package/src/lib/renderers/GraphRenderer.svelte +1328 -0
  131. package/src/lib/renderers/HypertableRenderer.svelte +253 -0
  132. package/src/lib/renderers/JsonRenderer.svelte +30 -0
  133. package/src/lib/renderers/KvRenderer.svelte +150 -0
  134. package/src/lib/renderers/QueueRenderer.svelte +286 -0
  135. package/src/lib/renderers/StatsRenderer.svelte +123 -0
  136. package/src/lib/renderers/TableRenderer.svelte +440 -0
  137. package/src/lib/renderers/VectorRenderer.svelte +388 -0
  138. package/src/lib/renderers/diff-render.ts +122 -0
  139. package/src/lib/renderers/diff.ts +9 -0
  140. package/src/lib/renderers/document-render.ts +64 -0
  141. package/src/lib/renderers/document.ts +9 -0
  142. package/src/lib/renderers/graph-render.ts +631 -0
  143. package/src/lib/renderers/graph-sigma.ts +395 -0
  144. package/src/lib/renderers/graph.ts +9 -0
  145. package/src/lib/renderers/hypertable-render.ts +320 -0
  146. package/src/lib/renderers/hypertable.ts +9 -0
  147. package/src/lib/renderers/index.ts +42 -0
  148. package/src/lib/renderers/json-render.ts +89 -0
  149. package/src/lib/renderers/json.ts +10 -0
  150. package/src/lib/renderers/kv-render.ts +193 -0
  151. package/src/lib/renderers/kv.ts +9 -0
  152. package/src/lib/renderers/queue-render.ts +197 -0
  153. package/src/lib/renderers/queue.ts +9 -0
  154. package/src/lib/renderers/registry.ts +68 -0
  155. package/src/lib/renderers/stats-render.ts +73 -0
  156. package/src/lib/renderers/stats.ts +9 -0
  157. package/src/lib/renderers/table-render.ts +156 -0
  158. package/src/lib/renderers/table.ts +11 -0
  159. package/src/lib/renderers/types.ts +33 -0
  160. package/src/lib/renderers/vector-render.ts +154 -0
  161. package/src/lib/renderers/vector.ts +9 -0
  162. package/src/lib/router.svelte.ts +163 -0
  163. package/src/lib/secureStore.svelte.ts +148 -0
  164. package/src/lib/shadow-dom-spike/ShadowDomSpikeCore.svelte +198 -0
  165. package/src/lib/shadow-dom-spike/mount-shadow-dom-spike.ts +94 -0
  166. package/src/lib/shortcuts.ts +22 -0
  167. package/src/lib/surface.ts +80 -0
  168. package/src/lib/tabs.svelte.ts +115 -0
  169. package/src/lib/theme.svelte.ts +63 -0
@@ -0,0 +1,260 @@
1
+ import { parseOpenContract } from "./boot-params";
2
+
3
+ export const DEFAULT_PERSISTED_CONNECTION_KEY = "red-ui:connection";
4
+
5
+ export interface ConnectionBootstrap {
6
+ target: string | null;
7
+ token?: string;
8
+ route?: string;
9
+ }
10
+
11
+ export type ConnectionBootstrapPayload =
12
+ | {
13
+ target?: unknown;
14
+ token?: unknown;
15
+ route?: unknown;
16
+ }
17
+ | null
18
+ | undefined;
19
+
20
+ type MaybePromise<T> = T | Promise<T>;
21
+ type BootstrapSource = () => MaybePromise<ConnectionBootstrapPayload>;
22
+ type TauriInvoke = <T>(
23
+ command: string,
24
+ args?: Record<string, unknown>
25
+ ) => Promise<T>;
26
+
27
+ export interface BootstrapStorage {
28
+ getItem(key: string): string | null;
29
+ }
30
+
31
+ export interface UrlBootstrapLocation {
32
+ pathname: string;
33
+ search: string;
34
+ hash: string;
35
+ }
36
+
37
+ export interface UrlBootstrapHistory {
38
+ readonly state: unknown;
39
+ replaceState(data: unknown, unused: string, url?: string | URL | null): void;
40
+ }
41
+
42
+ export interface UrlBootstrapReadOptions {
43
+ consume?: boolean;
44
+ location?: UrlBootstrapLocation;
45
+ history?: UrlBootstrapHistory;
46
+ }
47
+
48
+ export interface ConnectionBootstrapResolverOptions {
49
+ readTauri?: BootstrapSource;
50
+ readInjectedGlobal?: BootstrapSource;
51
+ readUrl?: BootstrapSource;
52
+ readPersisted?: BootstrapSource;
53
+ }
54
+
55
+ const GLOBAL_BOOTSTRAP_KEYS = [
56
+ "__RED_BOOTSTRAP__",
57
+ "__RED_UI_CONNECTION_BOOTSTRAP__",
58
+ "__RED_UI_BOOTSTRAP__",
59
+ ];
60
+
61
+ function clean(value: unknown): string | undefined {
62
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
63
+ }
64
+
65
+ function normalizeBootstrap(
66
+ payload: ConnectionBootstrapPayload
67
+ ): ConnectionBootstrap | null {
68
+ if (!payload || typeof payload !== "object") return null;
69
+
70
+ const target = clean(payload.target);
71
+ const token = clean(payload.token);
72
+ const route = clean(payload.route);
73
+
74
+ if (!target && !token && !route) return null;
75
+
76
+ return {
77
+ target: target ?? null,
78
+ ...(token ? { token } : {}),
79
+ ...(route ? { route } : {}),
80
+ };
81
+ }
82
+
83
+ async function firstPresent(
84
+ sources: BootstrapSource[]
85
+ ): Promise<ConnectionBootstrap | null> {
86
+ for (const source of sources) {
87
+ let resolved: ConnectionBootstrap | null;
88
+ try {
89
+ resolved = normalizeBootstrap(await source());
90
+ } catch {
91
+ resolved = null;
92
+ }
93
+ if (resolved) return resolved;
94
+ }
95
+ return null;
96
+ }
97
+
98
+ async function loadTauriInvoke(): Promise<TauriInvoke | null> {
99
+ if (typeof window === "undefined") return null;
100
+ if (!("__TAURI_INTERNALS__" in window || "__TAURI__" in window)) return null;
101
+
102
+ try {
103
+ const mod = await import("@tauri-apps/api/core");
104
+ return mod.invoke as TauriInvoke;
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ export async function readTauriIpcBootstrap(
111
+ invoke?: TauriInvoke
112
+ ): Promise<ConnectionBootstrapPayload> {
113
+ const call = invoke ?? (await loadTauriInvoke());
114
+ if (!call) return null;
115
+
116
+ try {
117
+ return await call<ConnectionBootstrapPayload>("connection_bootstrap");
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ export async function readInjectedGlobalBootstrap(
124
+ root: unknown = globalThis
125
+ ): Promise<ConnectionBootstrapPayload> {
126
+ if (!root || typeof root !== "object") return null;
127
+ const bag = root as Record<string, unknown>;
128
+
129
+ for (const key of GLOBAL_BOOTSTRAP_KEYS) {
130
+ const candidate = bag[key];
131
+ if (!candidate) continue;
132
+ try {
133
+ if (typeof candidate === "function") {
134
+ return await (
135
+ candidate as () => MaybePromise<ConnectionBootstrapPayload>
136
+ )();
137
+ }
138
+ return candidate as ConnectionBootstrapPayload;
139
+ } finally {
140
+ try {
141
+ delete bag[key];
142
+ } catch {
143
+ bag[key] = undefined;
144
+ }
145
+ }
146
+ }
147
+
148
+ return null;
149
+ }
150
+
151
+ function liveUrlBootstrapOptions(): UrlBootstrapReadOptions {
152
+ return {
153
+ consume: true,
154
+ location:
155
+ typeof location === "undefined"
156
+ ? undefined
157
+ : {
158
+ pathname: location.pathname,
159
+ search: location.search,
160
+ hash: location.hash,
161
+ },
162
+ history:
163
+ typeof history === "undefined"
164
+ ? undefined
165
+ : {
166
+ state: history.state,
167
+ replaceState: (data, unused, url) =>
168
+ history.replaceState(data, unused, url),
169
+ },
170
+ };
171
+ }
172
+
173
+ function stripTokenFromHistory(options: UrlBootstrapReadOptions): void {
174
+ if (!options.consume || !options.location || !options.history) return;
175
+ const hash = options.location.hash;
176
+ if (!hash) return;
177
+
178
+ const params = new URLSearchParams(
179
+ hash.startsWith("#") ? hash.slice(1) : hash
180
+ );
181
+ if (!params.has("token")) return;
182
+ params.delete("token");
183
+
184
+ const nextHash = params.toString();
185
+ const nextUrl = `${options.location.pathname}${options.location.search}${
186
+ nextHash ? `#${nextHash}` : ""
187
+ }`;
188
+ try {
189
+ options.history.replaceState(options.history.state, "", nextUrl);
190
+ } catch {
191
+ /* best-effort URL cleanup */
192
+ }
193
+ }
194
+
195
+ export function readUrlOpenContractBootstrap(
196
+ search = typeof location === "undefined" ? "" : location.search,
197
+ hash = typeof location === "undefined" ? "" : location.hash,
198
+ options: UrlBootstrapReadOptions = liveUrlBootstrapOptions()
199
+ ): ConnectionBootstrapPayload {
200
+ try {
201
+ const contract = parseOpenContract(search, hash);
202
+ if (contract.token) stripTokenFromHistory(options);
203
+ if (!contract.cs && !contract.to && !contract.token) return null;
204
+ return {
205
+ target: contract.cs,
206
+ token: contract.token,
207
+ route: contract.to,
208
+ };
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+
214
+ function defaultStorage(): BootstrapStorage | undefined {
215
+ try {
216
+ return typeof localStorage === "undefined" ? undefined : localStorage;
217
+ } catch {
218
+ return undefined;
219
+ }
220
+ }
221
+
222
+ export function readPersistedLocalBootstrap(
223
+ storage = defaultStorage(),
224
+ key = DEFAULT_PERSISTED_CONNECTION_KEY
225
+ ): ConnectionBootstrapPayload {
226
+ if (!storage) return null;
227
+
228
+ try {
229
+ const raw = storage.getItem(key);
230
+ if (!raw) return null;
231
+
232
+ const parsed = JSON.parse(raw) as { url?: unknown };
233
+ const target = clean(parsed?.url);
234
+ return target ? { target } : null;
235
+ } catch {
236
+ return null;
237
+ }
238
+ }
239
+
240
+ export async function resolveConnectionBootstrap(
241
+ options: ConnectionBootstrapResolverOptions = {}
242
+ ): Promise<ConnectionBootstrap> {
243
+ const readUrl = options.readUrl ?? (() => readUrlOpenContractBootstrap());
244
+ const resolved = await firstPresent([
245
+ options.readTauri ?? readTauriIpcBootstrap,
246
+ options.readInjectedGlobal ?? readInjectedGlobalBootstrap,
247
+ readUrl,
248
+ options.readPersisted ?? (() => readPersistedLocalBootstrap()),
249
+ ]);
250
+
251
+ if (resolved && !options.readUrl) {
252
+ try {
253
+ readUrlOpenContractBootstrap();
254
+ } catch {
255
+ /* best-effort live URL cleanup after a higher-priority boot source */
256
+ }
257
+ }
258
+
259
+ return resolved ?? { target: null };
260
+ }
@@ -0,0 +1,72 @@
1
+ // Contract tests for ConnectionProvider. Every implementation (LocalUrlProvider,
2
+ // the in-memory mock, future ReddbIoSsoProvider) runs through this suite, so
3
+ // the seam stays honest: behaviour, not just types.
4
+
5
+ import { describe, expect, it } from 'vitest'
6
+ import {
7
+ NotConnectedError,
8
+ UnknownConnectionError,
9
+ type Connection,
10
+ type ConnectionProvider,
11
+ } from './connection-provider'
12
+
13
+ export interface ContractFixture {
14
+ provider: ConnectionProvider
15
+ /** A connection id the provider promises to resolve in connect(). */
16
+ knownId: string
17
+ /** A connection id the provider promises NOT to resolve. */
18
+ unknownId: string
19
+ }
20
+
21
+ export function runConnectionProviderContract(
22
+ name: string,
23
+ build: () => ContractFixture | Promise<ContractFixture>,
24
+ ) {
25
+ describe(`ConnectionProvider contract — ${name}`, () => {
26
+ it('list() returns Connection objects with id/label/url', async () => {
27
+ const { provider } = await build()
28
+ const items = await provider.list()
29
+ expect(Array.isArray(items)).toBe(true)
30
+ for (const c of items) {
31
+ expect(typeof c.id).toBe('string')
32
+ expect(typeof c.label).toBe('string')
33
+ expect(typeof c.url).toBe('string')
34
+ }
35
+ })
36
+
37
+ it('list() entries have unique ids', async () => {
38
+ const { provider } = await build()
39
+ const items: Connection[] = await provider.list()
40
+ const ids = items.map((c) => c.id)
41
+ expect(new Set(ids).size).toBe(ids.length)
42
+ })
43
+
44
+ it('connect(knownId) resolves to an ActiveConnection', async () => {
45
+ const { provider, knownId } = await build()
46
+ const active = await provider.connect(knownId)
47
+ expect(active.connection.id).toBe(knownId)
48
+ expect(active.client).toBeTruthy()
49
+ expect(typeof active.rtt_ms).toBe('number')
50
+ expect(active.rtt_ms).toBeGreaterThanOrEqual(0)
51
+ })
52
+
53
+ it('connect(unknownId) rejects with UnknownConnectionError', async () => {
54
+ const { provider, unknownId } = await build()
55
+ await expect(provider.connect(unknownId)).rejects.toBeInstanceOf(UnknownConnectionError)
56
+ })
57
+
58
+ it('whoami() before connect rejects with NotConnectedError', async () => {
59
+ const { provider } = await build()
60
+ await expect(provider.whoami()).rejects.toBeInstanceOf(NotConnectedError)
61
+ })
62
+
63
+ it('whoami() after connect resolves to an Identity', async () => {
64
+ const { provider, knownId } = await build()
65
+ await provider.connect(knownId)
66
+ const id = await provider.whoami()
67
+ expect(typeof id.authenticated).toBe('boolean')
68
+ expect(typeof id.username).toBe('string')
69
+ expect(typeof id.role).toBe('string')
70
+ })
71
+ })
72
+ }
@@ -0,0 +1,72 @@
1
+ // ConnectionProvider — deep module: the UI's only seam for "how do I
2
+ // discover and select databases?". Local OSS lives behind LocalUrlProvider;
3
+ // cloud SSO (ReddbIoSsoProvider) will land in a separate slice without
4
+ // touching the consumers.
5
+
6
+ import type { RedClient } from './client'
7
+ import type { Transport } from './types'
8
+ import type { BootParams } from './boot-params'
9
+
10
+ export type ConnectionRole = 'primary' | 'replica' | 'embedded'
11
+
12
+ export interface Connection {
13
+ id: string
14
+ label: string
15
+ url: string
16
+ role?: ConnectionRole
17
+ description?: string
18
+ }
19
+
20
+ export interface Identity {
21
+ authenticated: boolean
22
+ username: string
23
+ role: string
24
+ }
25
+
26
+ export interface ActiveConnection {
27
+ connection: Connection
28
+ client: RedClient
29
+ rtt_ms: number
30
+ }
31
+
32
+ export interface ConnectionProvider {
33
+ list(): Promise<Connection[]>
34
+ connect(id: string): Promise<ActiveConnection>
35
+ whoami(): Promise<Identity>
36
+ /**
37
+ * The wire transports this provider can actually materialize on its Surface
38
+ * (#34, ADR-0003). The Connect UI offers only these, so the user never picks
39
+ * a transport that fails. Optional: a provider that omits it is treated as
40
+ * browser-reachable (http/https + coerced red://) by the consumer.
41
+ */
42
+ transports?(): Transport[]
43
+
44
+ /**
45
+ * Non-secret connection config seeded by the Surface (#36, ADR-0005) — the
46
+ * endpoint and initial view from the app URL. The Core reads it through this
47
+ * seam (not a special MCP branch) to connect without the Connect flow.
48
+ * Tokens never appear here. Returns null when nothing was seeded.
49
+ */
50
+ bootParams?(): BootParams | null
51
+ }
52
+
53
+ export class UnknownConnectionError extends Error {
54
+ constructor(public readonly id: string) {
55
+ super(`Unknown connection: ${id}`)
56
+ this.name = 'UnknownConnectionError'
57
+ }
58
+ }
59
+
60
+ export class NotConnectedError extends Error {
61
+ constructor() {
62
+ super('No active connection — call connect() first')
63
+ this.name = 'NotConnectedError'
64
+ }
65
+ }
66
+
67
+ export class UnreachableConnectionError extends Error {
68
+ constructor(public readonly id: string, public readonly reason: string) {
69
+ super(`Connection ${id} unreachable: ${reason}`)
70
+ this.name = 'UnreachableConnectionError'
71
+ }
72
+ }
@@ -0,0 +1,14 @@
1
+ export * from "./uri";
2
+ export * from "./types";
3
+ export * from "./client";
4
+ export * from "./connection-provider";
5
+ export * from "./local-url-provider";
6
+ export * from "./injected-client-provider";
7
+ export * from "./server-capabilities";
8
+ export * from "./transports";
9
+ export * from "./boot-params";
10
+ export * from "./connection-bootstrap";
11
+ export * from "./cdc-stream-client";
12
+ export * from "./secure-store";
13
+ export * from "./web-encrypted-store";
14
+ export * from "./tauri-encrypted-store";
@@ -0,0 +1,76 @@
1
+ // InjectedClientProvider — the embed-Surface implementation of
2
+ // ConnectionProvider. The embedding host owns auth (ADR-0001 / ADR-0005): it
3
+ // constructs and authenticates a RedClient, then hands it to the Core. The
4
+ // Core stays agnostic — it acquires its client through the same
5
+ // ConnectionProvider seam as the PWA/Desktop Surfaces, never knowing whether
6
+ // the connection came from a Connect flow or from its host.
7
+ //
8
+ // Unlike LocalUrlProvider this is a *single-connection* provider: there is no
9
+ // discovery, no history, no Connect flow. `list()` surfaces exactly one
10
+ // connection and `connect()` returns the host-supplied client immediately. A
11
+ // ping only measures RTT for the status bar — a failed probe does NOT block
12
+ // the mount, because the host vouches for the client it injected.
13
+
14
+ import type { RedClient } from './client'
15
+ import {
16
+ NotConnectedError,
17
+ UnknownConnectionError,
18
+ type ActiveConnection,
19
+ type Connection,
20
+ type ConnectionProvider,
21
+ type Identity,
22
+ } from './connection-provider'
23
+
24
+ export interface InjectedClientOptions {
25
+ /** The host-supplied, already-authenticated client. */
26
+ client: RedClient
27
+ /** UI metadata for the single connection this provider exposes. */
28
+ connection?: Partial<Connection>
29
+ }
30
+
31
+ export class InjectedClientProvider implements ConnectionProvider {
32
+ private readonly client: RedClient
33
+ /** The sole connection this provider resolves. */
34
+ readonly connection: Connection
35
+ private connected = false
36
+
37
+ constructor(opts: InjectedClientOptions) {
38
+ this.client = opts.client
39
+ this.connection = {
40
+ id: opts.connection?.id ?? 'host',
41
+ label: opts.connection?.label ?? 'Host connection',
42
+ url: opts.connection?.url ?? '',
43
+ role: opts.connection?.role ?? 'primary',
44
+ description: opts.connection?.description ?? 'Injected by the embedding host.',
45
+ }
46
+ }
47
+
48
+ async list(): Promise<Connection[]> {
49
+ return [this.connection]
50
+ }
51
+
52
+ /**
53
+ * Resolve the single host connection. Any other id throws
54
+ * UnknownConnectionError — there is nothing else to connect to. The probe is
55
+ * best-effort: an unreachable host still yields an ActiveConnection (rtt 0)
56
+ * rather than rejecting, since the host owns the client's lifecycle.
57
+ */
58
+ async connect(id: string): Promise<ActiveConnection> {
59
+ if (id !== this.connection.id) throw new UnknownConnectionError(id)
60
+ let rtt_ms = 0
61
+ try {
62
+ const ping = await this.client.ping()
63
+ if (ping.ok) rtt_ms = ping.rtt_ms
64
+ } catch {
65
+ /* host vouches for the client — surface rtt 0, don't block the mount */
66
+ }
67
+ this.connected = true
68
+ return { connection: this.connection, client: this.client, rtt_ms }
69
+ }
70
+
71
+ async whoami(): Promise<Identity> {
72
+ if (!this.connected) throw new NotConnectedError()
73
+ const w = await this.client.whoami()
74
+ return { authenticated: w.authenticated, username: w.username, role: w.role }
75
+ }
76
+ }