@valentinkolb/cloud 0.3.1 → 0.5.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 (194) hide show
  1. package/package.json +18 -8
  2. package/scripts/preload.ts +78 -23
  3. package/src/_internal/define-app.ts +119 -47
  4. package/src/_internal/runtime-context.ts +1 -0
  5. package/src/api/accounts-entities.ts +4 -0
  6. package/src/api/admin-core-settings.ts +98 -0
  7. package/src/api/announcements.ts +131 -0
  8. package/src/api/auth/schemas.ts +24 -0
  9. package/src/api/auth.ts +113 -10
  10. package/src/api/index.ts +15 -25
  11. package/src/api/me.ts +203 -14
  12. package/src/api/search/schemas.ts +1 -0
  13. package/src/api/search.ts +62 -8
  14. package/src/config/ssr.ts +2 -9
  15. package/src/contracts/announcements.test.ts +37 -0
  16. package/src/contracts/announcements.ts +121 -0
  17. package/src/contracts/app.ts +4 -0
  18. package/src/contracts/index.ts +3 -2
  19. package/src/contracts/registry.ts +4 -0
  20. package/src/contracts/shared.ts +108 -1
  21. package/src/desktop/index.ts +704 -0
  22. package/src/desktop/solid.tsx +938 -0
  23. package/src/server/api/index.ts +1 -1
  24. package/src/server/api/respond.ts +50 -10
  25. package/src/server/index.ts +44 -38
  26. package/src/server/middleware/auth.ts +98 -9
  27. package/src/server/middleware/index.ts +2 -1
  28. package/src/server/middleware/settings.ts +26 -0
  29. package/src/server/services/access.test.ts +197 -0
  30. package/src/server/services/access.ts +254 -6
  31. package/src/server/services/index.ts +14 -11
  32. package/src/server/services/pagination.ts +22 -0
  33. package/src/server/time.ts +45 -0
  34. package/src/services/account-lifecycle/index.ts +142 -18
  35. package/src/services/accounts/app.ts +658 -170
  36. package/src/services/accounts/authz.test.ts +77 -0
  37. package/src/services/accounts/authz.ts +22 -0
  38. package/src/services/accounts/entities.ts +84 -5
  39. package/src/services/accounts/groups.ts +30 -24
  40. package/src/services/accounts/model.test.ts +30 -0
  41. package/src/services/accounts/switching.test.ts +14 -0
  42. package/src/services/accounts/switching.ts +15 -6
  43. package/src/services/accounts/users.ts +75 -52
  44. package/src/services/announcements/index.test.ts +32 -0
  45. package/src/services/announcements/index.ts +224 -0
  46. package/src/services/audit/index.test.ts +84 -0
  47. package/src/services/audit/index.ts +431 -0
  48. package/src/services/auth-flows/index.ts +9 -2
  49. package/src/services/auth-flows/ipa.ts +0 -2
  50. package/src/services/auth-flows/magic-link.ts +3 -2
  51. package/src/services/auth-flows/password-reset.ts +284 -0
  52. package/src/services/auth-flows/proxy-return.test.ts +24 -0
  53. package/src/services/auth-flows/proxy-return.ts +49 -0
  54. package/src/services/gateway.ts +162 -0
  55. package/src/services/index.ts +44 -2
  56. package/src/services/ipa/effective-groups.test.ts +33 -0
  57. package/src/services/ipa/effective-groups.ts +70 -0
  58. package/src/services/ipa/profile.ts +45 -3
  59. package/src/services/ipa/search.ts +3 -5
  60. package/src/services/ipa/service-account.ts +15 -0
  61. package/src/services/ipa/sync-planning.test.ts +32 -0
  62. package/src/services/ipa/sync-planning.ts +22 -0
  63. package/src/services/ipa/sync.ts +110 -38
  64. package/src/services/oauth-tokens.ts +104 -0
  65. package/src/services/postgres.ts +21 -6
  66. package/src/services/providers/local/auth.test.ts +22 -0
  67. package/src/services/providers/local/auth.ts +46 -3
  68. package/src/services/secrets.ts +10 -0
  69. package/src/services/service-account-credentials.test.ts +210 -0
  70. package/src/services/service-account-credentials.ts +715 -0
  71. package/src/services/service-accounts.ts +188 -0
  72. package/src/services/session/index.ts +7 -8
  73. package/src/services/settings/app.ts +4 -20
  74. package/src/services/settings/defaults.ts +64 -22
  75. package/src/services/settings/store.ts +47 -0
  76. package/src/services/weather/forecast.ts +40 -7
  77. package/src/services/webauthn.test.ts +36 -0
  78. package/src/services/webauthn.ts +384 -0
  79. package/src/shared/icons.ts +391 -100
  80. package/src/shared/index.ts +7 -0
  81. package/src/shared/markdown/extensions/code.ts +38 -1
  82. package/src/shared/markdown/extensions/images.ts +39 -3
  83. package/src/shared/markdown/extensions/info-blocks.ts +5 -5
  84. package/src/shared/markdown/extensions/mark.ts +48 -0
  85. package/src/shared/markdown/extensions/sub-sup.ts +60 -0
  86. package/src/shared/markdown/extensions/tables.ts +79 -58
  87. package/src/shared/markdown/formula.test.ts +1089 -0
  88. package/src/shared/markdown/formula.ts +1187 -0
  89. package/src/shared/markdown/index.ts +76 -2
  90. package/src/shared/mock-cover.ts +130 -0
  91. package/src/shared/redirect.test.ts +49 -0
  92. package/src/shared/redirect.ts +52 -0
  93. package/src/shared/theme.test.ts +24 -0
  94. package/src/shared/theme.ts +68 -0
  95. package/src/shared/time.ts +13 -0
  96. package/src/ssr/AdminLayout.tsx +7 -3
  97. package/src/ssr/AdminSidebar.tsx +115 -49
  98. package/src/ssr/AppLaunchpad.island.tsx +176 -0
  99. package/src/ssr/Footer.island.tsx +3 -8
  100. package/src/ssr/GlobalAnnouncements.island.tsx +141 -0
  101. package/src/ssr/GlobalSearchDialog.tsx +545 -117
  102. package/src/ssr/HotkeysHelpRail.island.tsx +3 -70
  103. package/src/ssr/Layout.tsx +74 -66
  104. package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
  105. package/src/ssr/LayoutHelp.tsx +266 -0
  106. package/src/ssr/NavMenu.island.tsx +0 -39
  107. package/src/ssr/ThemeToggleRail.island.tsx +3 -3
  108. package/src/ssr/TimezoneCookie.island.tsx +23 -0
  109. package/src/ssr/islands/index.ts +13 -0
  110. package/src/styles/base-popover.css +5 -2
  111. package/src/styles/effects.css +87 -6
  112. package/src/styles/global.css +146 -9
  113. package/src/styles/input.css +3 -1
  114. package/src/styles/utilities-buttons.css +133 -27
  115. package/src/styles/utilities-code-display.css +67 -0
  116. package/src/styles/utilities-completion.css +223 -0
  117. package/src/styles/utilities-detail.css +73 -0
  118. package/src/styles/utilities-feedback.css +16 -15
  119. package/src/styles/utilities-layout.css +42 -2
  120. package/src/styles/utilities-markdown-editor.css +472 -0
  121. package/src/styles/utilities-navigation.css +63 -8
  122. package/src/styles/utilities-script.css +84 -0
  123. package/src/styles/utilities-table-tile.css +229 -0
  124. package/src/types/ambient.d.ts +9 -0
  125. package/src/ui/completion/behaviors.test.ts +95 -0
  126. package/src/ui/completion/behaviors.ts +205 -0
  127. package/src/ui/completion/engine.ts +368 -0
  128. package/src/ui/completion/index.ts +40 -0
  129. package/src/ui/completion/overlay.ts +92 -0
  130. package/src/ui/dialog-core.ts +173 -45
  131. package/src/ui/filter/FilterChip.tsx +42 -40
  132. package/src/ui/index.ts +11 -12
  133. package/src/ui/input/AutocompleteEditor.tsx +656 -0
  134. package/src/ui/input/CheckboxCard.tsx +91 -0
  135. package/src/ui/input/Combobox.tsx +375 -0
  136. package/src/ui/input/DatePicker.tsx +846 -0
  137. package/src/ui/input/DateTimeInput.tsx +29 -4
  138. package/src/ui/input/FileDropzone.tsx +116 -0
  139. package/src/ui/input/IconInput.tsx +116 -0
  140. package/src/ui/input/ImageInput.tsx +19 -2
  141. package/src/ui/input/MultiSelectInput.tsx +448 -0
  142. package/src/ui/input/NumberInput.tsx +417 -61
  143. package/src/ui/input/SegmentedControl.tsx +2 -2
  144. package/src/ui/input/Select.tsx +172 -10
  145. package/src/ui/input/Slider.tsx +3 -4
  146. package/src/ui/input/Switch.tsx +3 -2
  147. package/src/ui/input/TemplateEditor.tsx +212 -0
  148. package/src/ui/input/TextInput.tsx +144 -13
  149. package/src/ui/input/index.ts +53 -8
  150. package/src/ui/input/markdown/MarkdownEditor.tsx +774 -0
  151. package/src/ui/input/markdown/Toolbar.tsx +90 -0
  152. package/src/ui/input/markdown/actions.ts +233 -0
  153. package/src/ui/input/markdown/active-formats.ts +94 -0
  154. package/src/ui/input/markdown/behaviors.ts +193 -0
  155. package/src/ui/input/markdown/code-zone.ts +23 -0
  156. package/src/ui/input/markdown/highlight.ts +316 -0
  157. package/src/ui/layout.ts +22 -0
  158. package/src/ui/misc/AppOverview.tsx +105 -0
  159. package/src/ui/misc/AppWorkspace.tsx +607 -0
  160. package/src/ui/misc/Calendar.tsx +1291 -0
  161. package/src/ui/misc/Chart.tsx +162 -0
  162. package/src/ui/misc/CodeDisplay.tsx +54 -0
  163. package/src/ui/misc/ContextMenu.tsx +2 -2
  164. package/src/ui/misc/DataTable.tsx +269 -0
  165. package/src/ui/misc/DockWorkspace.tsx +425 -0
  166. package/src/ui/misc/Docs.tsx +153 -0
  167. package/src/ui/misc/Dropdown.tsx +2 -2
  168. package/src/ui/misc/EntitySearch.tsx +260 -129
  169. package/src/ui/misc/LinkCard.tsx +14 -2
  170. package/src/ui/misc/LogEntriesTable.tsx +34 -31
  171. package/src/ui/misc/Pagination.tsx +31 -12
  172. package/src/ui/misc/PanelDialog.tsx +109 -0
  173. package/src/ui/misc/Panes.tsx +873 -0
  174. package/src/ui/misc/PermissionEditor.tsx +358 -262
  175. package/src/ui/misc/Placeholder.tsx +40 -0
  176. package/src/ui/misc/ProgressBar.tsx +1 -1
  177. package/src/ui/misc/ResourceApiKeys.tsx +260 -0
  178. package/src/ui/misc/SettingsModal.tsx +150 -0
  179. package/src/ui/misc/StatCell.tsx +182 -40
  180. package/src/ui/misc/StatGrid.tsx +149 -0
  181. package/src/ui/misc/StructuredDataPreview.tsx +107 -0
  182. package/src/ui/misc/code-highlight.ts +213 -0
  183. package/src/ui/misc/index.ts +93 -12
  184. package/src/ui/prompts.tsx +362 -312
  185. package/src/ui/toast.ts +384 -0
  186. package/src/ui/widgets/Widget.tsx +12 -4
  187. package/src/ssr/MoreAppsDropdown.island.tsx +0 -61
  188. package/src/ui/ipa/GroupView.tsx +0 -36
  189. package/src/ui/ipa/LoginBtn.tsx +0 -16
  190. package/src/ui/ipa/UserView.tsx +0 -58
  191. package/src/ui/ipa/index.ts +0 -4
  192. package/src/ui/navigation.ts +0 -32
  193. package/src/ui/sidebar.tsx +0 -468
  194. /package/src/ui/{ipa → misc}/Avatar.tsx +0 -0
@@ -0,0 +1,704 @@
1
+ export type DesktopPlatform = "browser" | "macos" | "linux" | "windows";
2
+ export type DesktopRuntime = "browser" | "electrobun";
3
+ export type DesktopWindowControls = "browser" | "native-inset" | "system-titlebar" | "custom";
4
+
5
+ export type DesktopEnvironment = {
6
+ runtime: DesktopRuntime;
7
+ platform: DesktopPlatform;
8
+ windowControls: DesktopWindowControls;
9
+ supportsNativeDialogs: boolean;
10
+ supportsNativeMenus: boolean;
11
+ supportsContextMenus: boolean;
12
+ };
13
+
14
+ export type DesktopResult<T> = { ok: true; data: T } | { ok: false; error: string };
15
+
16
+ export type DesktopFileDialogOptions = {
17
+ multiple?: boolean;
18
+ directories?: boolean;
19
+ files?: boolean;
20
+ startingFolder?: string;
21
+ };
22
+
23
+ export type DesktopFileDialogResult = {
24
+ paths: string[];
25
+ };
26
+
27
+ export type DesktopMessageOptions = {
28
+ type?: "info" | "warning" | "error" | "question";
29
+ title?: string;
30
+ message: string;
31
+ detail?: string;
32
+ buttons?: string[];
33
+ };
34
+
35
+ export type DesktopMessageResult = {
36
+ buttonIndex?: number;
37
+ };
38
+
39
+ export type DesktopNotificationOptions = {
40
+ title: string;
41
+ subtitle?: string;
42
+ body?: string;
43
+ };
44
+
45
+ export type DesktopContextMenuItem = { type: "divider" } | { label: string; action?: string; role?: string; enabled?: boolean };
46
+
47
+ export type DesktopLogger = {
48
+ debug: (message: string, metadata?: Record<string, unknown>) => void;
49
+ info: (message: string, metadata?: Record<string, unknown>) => void;
50
+ warn: (message: string, metadata?: Record<string, unknown>) => void;
51
+ error: (message: string, metadata?: Record<string, unknown>) => void;
52
+ };
53
+
54
+ export type DesktopTaskState = "idle" | "running" | "scheduled" | "error" | "stopped";
55
+
56
+ export type DesktopTaskStatus = {
57
+ id: string;
58
+ state: DesktopTaskState;
59
+ runCount: number;
60
+ failureCount: number;
61
+ lastStartedAt?: string;
62
+ lastFinishedAt?: string;
63
+ lastError?: string;
64
+ nextRunAt?: string;
65
+ };
66
+
67
+ export type DesktopTaskRetryOptions = {
68
+ attempts?: number;
69
+ baseMs?: number;
70
+ maxMs?: number;
71
+ };
72
+
73
+ export type DesktopTaskRunContext = {
74
+ id: string;
75
+ app: DesktopAppConfig;
76
+ sql: DesktopSql;
77
+ logger: DesktopLogger;
78
+ signal: AbortSignal;
79
+ };
80
+
81
+ export type DesktopTaskDefinition = {
82
+ intervalMs?: number;
83
+ runOnStart?: boolean;
84
+ retry?: DesktopTaskRetryOptions;
85
+ run: (ctx: DesktopTaskRunContext) => void | Promise<void>;
86
+ };
87
+
88
+ export type DesktopTaskSupervisor = {
89
+ register: (id: string, definition: DesktopTaskDefinition) => void;
90
+ every: (id: string, definition: DesktopTaskDefinition & { intervalMs: number }) => void;
91
+ submit: (id: string) => Promise<DesktopTaskStatus>;
92
+ status: (id: string) => DesktopTaskStatus | null;
93
+ list: () => DesktopTaskStatus[];
94
+ stop: () => Promise<void>;
95
+ };
96
+
97
+ export type DesktopLifecycleContext = {
98
+ app: DesktopAppConfig;
99
+ desktop: typeof desktop;
100
+ sql: DesktopSql;
101
+ logger: DesktopLogger;
102
+ signal: AbortSignal;
103
+ tasks: DesktopTaskSupervisor;
104
+ };
105
+
106
+ export type DesktopLifecycle = {
107
+ setup?: (ctx: DesktopLifecycleContext) => void | Promise<void>;
108
+ start?: (ctx: DesktopLifecycleContext) => void | Promise<void>;
109
+ stop?: (ctx: DesktopLifecycleContext) => void | Promise<void>;
110
+ };
111
+
112
+ export const desktopWindowDescriptorKind = "cloud-desktop-window" as const;
113
+ export const desktopWindowSearchParams = {
114
+ name: "__cloudDesktopWindow",
115
+ props: "__cloudDesktopWindowProps",
116
+ } as const;
117
+
118
+ export type DesktopWindowDescriptor = {
119
+ kind: typeof desktopWindowDescriptorKind;
120
+ name: string;
121
+ props: string;
122
+ };
123
+
124
+ export type DesktopWindowOpenOptions = {
125
+ title?: string;
126
+ width?: number;
127
+ height?: number;
128
+ x?: number;
129
+ y?: number;
130
+ activate?: boolean;
131
+ };
132
+
133
+ export type DesktopWindowOpenInput = {
134
+ descriptor: DesktopWindowDescriptor;
135
+ options?: DesktopWindowOpenOptions;
136
+ };
137
+
138
+ export type DesktopWindowIdInput = {
139
+ id: string;
140
+ };
141
+
142
+ export type DesktopWindowSetTitleInput = DesktopWindowIdInput & {
143
+ title: string;
144
+ };
145
+
146
+ export type DesktopWindowRefData = {
147
+ id: string;
148
+ };
149
+
150
+ export type DesktopWindowRef = DesktopWindowRefData & {
151
+ close: () => Promise<void>;
152
+ focus: () => Promise<void>;
153
+ setTitle: (title: string) => Promise<void>;
154
+ };
155
+
156
+ export type DesktopBridge = {
157
+ getEnvironment?: () => Promise<DesktopResult<DesktopEnvironment>>;
158
+ openFileDialog?: (options?: DesktopFileDialogOptions) => Promise<DesktopResult<DesktopFileDialogResult>>;
159
+ showMessage?: (options: DesktopMessageOptions) => Promise<DesktopResult<DesktopMessageResult>>;
160
+ showNotification?: (options: DesktopNotificationOptions) => Promise<DesktopResult<void>>;
161
+ clipboardWriteText?: (value: string) => Promise<DesktopResult<void>>;
162
+ clipboardReadText?: () => Promise<DesktopResult<string>>;
163
+ showContextMenu?: (items: DesktopContextMenuItem[]) => Promise<DesktopResult<void>>;
164
+ openExternal?: (url: string) => Promise<DesktopResult<boolean>>;
165
+ closeWindow?: () => Promise<DesktopResult<void>>;
166
+ minimizeWindow?: () => Promise<DesktopResult<void>>;
167
+ maximizeWindow?: () => Promise<DesktopResult<void>>;
168
+ getCurrentWindowDescriptor?: () => Promise<DesktopResult<DesktopWindowDescriptor | null>>;
169
+ openWindow?: (input: DesktopWindowOpenInput) => Promise<DesktopResult<DesktopWindowRefData>>;
170
+ closeWindowById?: (input: DesktopWindowIdInput) => Promise<DesktopResult<void>>;
171
+ focusWindow?: (input: DesktopWindowIdInput) => Promise<DesktopResult<void>>;
172
+ setWindowTitle?: (input: DesktopWindowSetTitleInput) => Promise<DesktopResult<void>>;
173
+ submitTask?: (id: string) => Promise<DesktopResult<DesktopTaskStatus>>;
174
+ getTaskStatus?: (id: string) => Promise<DesktopResult<DesktopTaskStatus | null>>;
175
+ listTasks?: () => Promise<DesktopResult<DesktopTaskStatus[]>>;
176
+ };
177
+
178
+ export type DesktopAppMenuItem =
179
+ | { type: "divider" }
180
+ | { role: string }
181
+ | { label: string; action?: string; onClick?: (ctx: DesktopActionContext) => void | Promise<void>; enabled?: boolean };
182
+
183
+ export type DesktopAppMenu = Array<{
184
+ label: string;
185
+ items: DesktopAppMenuItem[];
186
+ }>;
187
+
188
+ export type DesktopAppConfig = {
189
+ name: string;
190
+ identifier: string;
191
+ version?: string;
192
+ routing?: "path" | "hash" | "none";
193
+ window?: {
194
+ width?: number;
195
+ height?: number;
196
+ titleBar?: "default" | "hidden-inset" | "hidden" | "custom";
197
+ };
198
+ menu?: DesktopAppMenu;
199
+ lifecycle?: DesktopLifecycle;
200
+ };
201
+
202
+ export type DesktopActionContext = {
203
+ desktop: typeof desktop;
204
+ };
205
+
206
+ export type DesktopSql = (<Row = Record<string, unknown>>(strings: TemplateStringsArray, ...values: unknown[]) => Row[]) & {
207
+ transaction: <T>(fn: () => T) => T;
208
+ db: unknown;
209
+ };
210
+
211
+ const browserEnvironment: DesktopEnvironment = {
212
+ runtime: "browser",
213
+ platform: "browser",
214
+ windowControls: "browser",
215
+ supportsNativeDialogs: false,
216
+ supportsNativeMenus: false,
217
+ supportsContextMenus: false,
218
+ };
219
+
220
+ let bridgeOverride: DesktopBridge | null = null;
221
+
222
+ const hasWindow = () => typeof window !== "undefined";
223
+
224
+ const bridge = (): DesktopBridge | null => bridgeOverride ?? (hasWindow() ? (window.cloudDesktopRuntime ?? null) : null);
225
+
226
+ const unsupported = async <T>(label: string): Promise<DesktopResult<T>> => ({
227
+ ok: false,
228
+ error: `${label} requires a desktop runtime.`,
229
+ });
230
+
231
+ const unwrap = async <T>(result: Promise<DesktopResult<T>>): Promise<T> => {
232
+ const value = await result;
233
+ if (!value.ok) throw new Error(value.error);
234
+ return value.data;
235
+ };
236
+
237
+ const result = async <T>(fn: () => T | Promise<T>): Promise<DesktopResult<T>> => {
238
+ try {
239
+ return { ok: true, data: await fn() };
240
+ } catch (error) {
241
+ return { ok: false, error: errorMessage(error) };
242
+ }
243
+ };
244
+
245
+ const emitNavigation = () => {
246
+ if (!hasWindow()) return;
247
+ window.dispatchEvent(new CustomEvent("cloud-desktop:navigation"));
248
+ };
249
+
250
+ const isDesktopWindowDescriptor = (value: unknown): value is DesktopWindowDescriptor =>
251
+ Boolean(
252
+ value &&
253
+ typeof value === "object" &&
254
+ (value as { kind?: unknown }).kind === desktopWindowDescriptorKind &&
255
+ typeof (value as { name?: unknown }).name === "string" &&
256
+ typeof (value as { props?: unknown }).props === "string",
257
+ );
258
+
259
+ const descriptorUrl = (descriptor: DesktopWindowDescriptor): string => {
260
+ const url = hasWindow() ? new URL(window.location.href) : new URL("http://desktop.local/");
261
+ const params = new URLSearchParams(url.hash.startsWith("#") ? url.hash.slice(1) : "");
262
+ params.set(desktopWindowSearchParams.name, descriptor.name);
263
+ params.set(desktopWindowSearchParams.props, descriptor.props);
264
+ url.hash = params.toString();
265
+ return `${url.pathname}${url.search}${url.hash}`;
266
+ };
267
+
268
+ export const readDesktopWindowDescriptor = (): DesktopWindowDescriptor | null => {
269
+ if (!hasWindow()) return null;
270
+ const url = new URL(window.location.href);
271
+ const params = new URLSearchParams(url.hash.startsWith("#") ? url.hash.slice(1) : url.search);
272
+ const name = params.get(desktopWindowSearchParams.name);
273
+ const props = params.get(desktopWindowSearchParams.props);
274
+ return name && props ? { kind: desktopWindowDescriptorKind, name, props } : null;
275
+ };
276
+
277
+ const windowRef = (data: DesktopWindowRefData, browserWindow?: Window | null): DesktopWindowRef => ({
278
+ id: data.id,
279
+ close: () =>
280
+ browserWindow
281
+ ? Promise.resolve(browserWindow.close())
282
+ : unwrap(bridge()?.closeWindowById?.({ id: data.id }) ?? unsupported("Native windows")),
283
+ focus: () =>
284
+ browserWindow
285
+ ? Promise.resolve(browserWindow.focus())
286
+ : unwrap(bridge()?.focusWindow?.({ id: data.id }) ?? unsupported("Native windows")),
287
+ setTitle: (title) =>
288
+ browserWindow
289
+ ? Promise.resolve(undefined)
290
+ : unwrap(bridge()?.setWindowTitle?.({ id: data.id, title }) ?? unsupported("Native windows")),
291
+ });
292
+
293
+ const runtimeRequire = (): ((id: string) => unknown) | null => {
294
+ const importMetaRequire = (import.meta as unknown as { require?: (id: string) => unknown }).require;
295
+ if (importMetaRequire) return importMetaRequire;
296
+ try {
297
+ return Function("return typeof require === 'function' ? require : null")() as ((id: string) => unknown) | null;
298
+ } catch {
299
+ return null;
300
+ }
301
+ };
302
+
303
+ let sqlInstance: DesktopSql | null = null;
304
+
305
+ const getSql = (): DesktopSql => {
306
+ if (sqlInstance) return sqlInstance;
307
+ const req = runtimeRequire();
308
+ if (!req || typeof window !== "undefined") throw new Error("desktop.sql is only available in the Bun desktop process.");
309
+ const requireFn = req as (id: string) => unknown;
310
+
311
+ const { Database } = requireFn("bun:sqlite") as { Database: new (path: string) => any };
312
+ const { dirname, resolve } = requireFn("node:path") as typeof import("node:path");
313
+ const { mkdirSync } = requireFn("node:fs") as typeof import("node:fs");
314
+ const dbPath = process.env.CLOUD_DESKTOP_SQLITE_PATH ?? resolve(process.cwd(), ".local", "desktop.sqlite");
315
+ mkdirSync(dirname(dbPath), { recursive: true });
316
+ const db = new Database(dbPath);
317
+
318
+ const sql = ((strings: TemplateStringsArray, ...values: unknown[]) => {
319
+ const statement = strings.reduce((query, chunk, index) => `${query}${chunk}${index < values.length ? "?" : ""}`, "");
320
+ const prepared = db.query(statement);
321
+ const firstWord = statement.trimStart().split(/\s+/, 1)[0]?.toLowerCase();
322
+ if (firstWord === "select" || firstWord === "pragma" || firstWord === "with") return prepared.all(...values);
323
+ prepared.run(...values);
324
+ return [];
325
+ }) as DesktopSql;
326
+
327
+ sql.transaction = (fn) => db.transaction(fn)();
328
+ Object.defineProperty(sql, "db", { value: db, enumerable: true });
329
+ sqlInstance = sql;
330
+ return sql;
331
+ };
332
+
333
+ const sleep = (ms: number, signal: AbortSignal): Promise<void> =>
334
+ new Promise((resolve, reject) => {
335
+ if (signal.aborted) {
336
+ reject(new DOMException("Aborted", "AbortError"));
337
+ return;
338
+ }
339
+ const timer = setTimeout(resolve, ms);
340
+ signal.addEventListener(
341
+ "abort",
342
+ () => {
343
+ clearTimeout(timer);
344
+ reject(new DOMException("Aborted", "AbortError"));
345
+ },
346
+ { once: true },
347
+ );
348
+ });
349
+
350
+ const chainAbort = (source: AbortSignal, target: AbortController): (() => void) => {
351
+ if (source.aborted) {
352
+ target.abort();
353
+ return () => {};
354
+ }
355
+ const abort = () => target.abort();
356
+ source.addEventListener("abort", abort, { once: true });
357
+ return () => source.removeEventListener("abort", abort);
358
+ };
359
+
360
+ const errorMessage = (error: unknown): string => (error instanceof Error ? error.message : String(error));
361
+
362
+ const createConsoleLogger = (source: string): DesktopLogger => {
363
+ const write =
364
+ (level: "debug" | "info" | "warn" | "error") =>
365
+ (message: string, metadata?: Record<string, unknown>): void => {
366
+ const prefix = `[desktop:${source}] ${message}`;
367
+ if (metadata) console[level](prefix, metadata);
368
+ else console[level](prefix);
369
+ };
370
+ return {
371
+ debug: write("debug"),
372
+ info: write("info"),
373
+ warn: write("warn"),
374
+ error: write("error"),
375
+ };
376
+ };
377
+
378
+ const snapshotTaskStatus = (status: DesktopTaskStatus): DesktopTaskStatus => ({ ...status });
379
+
380
+ export const createDesktopTaskSupervisor = (options: {
381
+ app: DesktopAppConfig;
382
+ sql?: DesktopSql;
383
+ logger?: DesktopLogger;
384
+ signal?: AbortSignal;
385
+ }): DesktopTaskSupervisor => {
386
+ const parentSignal = options.signal ?? new AbortController().signal;
387
+ const sql = options.sql ?? getSql();
388
+ const logger = options.logger ?? createConsoleLogger(options.app.identifier);
389
+ let stopped = false;
390
+ type TaskRecord = {
391
+ definition: DesktopTaskDefinition;
392
+ status: DesktopTaskStatus;
393
+ timer: ReturnType<typeof setTimeout> | null;
394
+ runController: AbortController | null;
395
+ currentRun: Promise<DesktopTaskStatus> | null;
396
+ };
397
+ const records = new Map<string, TaskRecord>();
398
+
399
+ const scheduleNext = (id: string, record: TaskRecord): void => {
400
+ if (!record.definition.intervalMs || stopped || parentSignal.aborted || record.status.state === "stopped") return;
401
+ const dueAt = Date.now() + record.definition.intervalMs;
402
+ record.status.nextRunAt = new Date(dueAt).toISOString();
403
+ record.status.state = record.status.lastError ? "error" : "scheduled";
404
+ record.timer = setTimeout(() => {
405
+ record.timer = null;
406
+ void runTask(id, record).catch((error) => {
407
+ logger.error(`Task "${id}" failed`, { error: errorMessage(error) });
408
+ });
409
+ }, record.definition.intervalMs);
410
+ };
411
+
412
+ const runTask = async (id: string, record: TaskRecord): Promise<DesktopTaskStatus> => {
413
+ if (record.currentRun) return record.currentRun;
414
+ if (stopped || parentSignal.aborted) throw new Error("Desktop app is stopping.");
415
+ if (record.timer) {
416
+ clearTimeout(record.timer);
417
+ record.timer = null;
418
+ }
419
+
420
+ const run = async (): Promise<DesktopTaskStatus> => {
421
+ const controller = new AbortController();
422
+ const unchainAbort = chainAbort(parentSignal, controller);
423
+ record.runController = controller;
424
+ record.status.state = "running";
425
+ record.status.lastStartedAt = new Date().toISOString();
426
+ record.status.nextRunAt = undefined;
427
+
428
+ const retry = record.definition.retry;
429
+ const attempts = Math.max(1, retry?.attempts ?? 1);
430
+ const baseMs = Math.max(0, retry?.baseMs ?? 0);
431
+ const maxMs = Math.max(baseMs, retry?.maxMs ?? baseMs);
432
+ let lastError: unknown;
433
+
434
+ try {
435
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
436
+ if (controller.signal.aborted) throw new DOMException("Aborted", "AbortError");
437
+ try {
438
+ await record.definition.run({ id, app: options.app, sql, logger, signal: controller.signal });
439
+ record.status.runCount += 1;
440
+ record.status.failureCount = 0;
441
+ record.status.lastError = undefined;
442
+ record.status.lastFinishedAt = new Date().toISOString();
443
+ record.status.state = "idle";
444
+ return snapshotTaskStatus(record.status);
445
+ } catch (error) {
446
+ lastError = error;
447
+ if (controller.signal.aborted || attempt >= attempts) break;
448
+ const delay = Math.min(maxMs, baseMs * 2 ** (attempt - 1));
449
+ if (delay > 0) await sleep(delay, controller.signal);
450
+ }
451
+ }
452
+
453
+ record.status.failureCount += 1;
454
+ record.status.lastError = errorMessage(lastError);
455
+ record.status.lastFinishedAt = new Date().toISOString();
456
+ record.status.state = "error";
457
+ throw lastError instanceof Error ? lastError : new Error(errorMessage(lastError));
458
+ } finally {
459
+ unchainAbort();
460
+ record.runController = null;
461
+ record.currentRun = null;
462
+ if (!stopped && !parentSignal.aborted) scheduleNext(id, record);
463
+ }
464
+ };
465
+
466
+ record.currentRun = run();
467
+ return record.currentRun;
468
+ };
469
+
470
+ const register = (id: string, definition: DesktopTaskDefinition): void => {
471
+ if (stopped) throw new Error("Desktop task supervisor is stopped.");
472
+ const existing = records.get(id);
473
+ if (existing) {
474
+ if (existing.timer) clearTimeout(existing.timer);
475
+ existing.runController?.abort();
476
+ existing.definition = definition;
477
+ existing.status.state = "idle";
478
+ existing.status.nextRunAt = undefined;
479
+ if (definition.intervalMs) scheduleNext(id, existing);
480
+ if (definition.runOnStart) void runTask(id, existing).catch(() => {});
481
+ return;
482
+ }
483
+
484
+ const record: TaskRecord = {
485
+ definition,
486
+ timer: null,
487
+ runController: null,
488
+ currentRun: null,
489
+ status: {
490
+ id,
491
+ state: "idle",
492
+ runCount: 0,
493
+ failureCount: 0,
494
+ },
495
+ };
496
+ records.set(id, record);
497
+ if (definition.intervalMs) scheduleNext(id, record);
498
+ if (definition.runOnStart) void runTask(id, record).catch(() => {});
499
+ };
500
+
501
+ const supervisor: DesktopTaskSupervisor = {
502
+ register,
503
+ every: (id, definition) => register(id, definition),
504
+ submit: async (id) => {
505
+ if (stopped) throw new Error("Desktop task supervisor is stopped.");
506
+ const record = records.get(id);
507
+ if (!record) throw new Error(`Unknown desktop task "${id}".`);
508
+ return runTask(id, record);
509
+ },
510
+ status: (id) => {
511
+ const record = records.get(id);
512
+ return record ? snapshotTaskStatus(record.status) : null;
513
+ },
514
+ list: () => Array.from(records.values(), (record) => snapshotTaskStatus(record.status)),
515
+ stop: async () => {
516
+ stopped = true;
517
+ for (const record of records.values()) {
518
+ if (record.timer) clearTimeout(record.timer);
519
+ record.timer = null;
520
+ record.status.nextRunAt = undefined;
521
+ record.status.state = "stopped";
522
+ record.runController?.abort();
523
+ }
524
+ await Promise.allSettled(Array.from(records.values(), (record) => record.currentRun));
525
+ },
526
+ };
527
+
528
+ parentSignal.addEventListener("abort", () => void supervisor.stop(), { once: true });
529
+ return supervisor;
530
+ };
531
+
532
+ export type DesktopAppHandle = {
533
+ tasks: DesktopTaskSupervisor;
534
+ bridge: Pick<DesktopBridge, "submitTask" | "getTaskStatus" | "listTasks">;
535
+ signal: AbortSignal;
536
+ stop: () => Promise<void>;
537
+ };
538
+
539
+ export type StartDesktopAppOptions = {
540
+ sql?: DesktopSql;
541
+ logger?: DesktopLogger;
542
+ signal?: AbortSignal;
543
+ shutdownSignals?: boolean;
544
+ };
545
+
546
+ export const startDesktopApp = async <Config extends DesktopAppConfig>(
547
+ config: Config,
548
+ options: StartDesktopAppOptions = {},
549
+ ): Promise<DesktopAppHandle> => {
550
+ const controller = new AbortController();
551
+ const externalSignal = options.signal;
552
+ const unchainExternalAbort = externalSignal ? chainAbort(externalSignal, controller) : () => {};
553
+ const sql = options.sql ?? getSql();
554
+ const logger = options.logger ?? createConsoleLogger(config.identifier);
555
+ const tasks = createDesktopTaskSupervisor({ app: config, sql, logger, signal: controller.signal });
556
+ const ctx: DesktopLifecycleContext = { app: config, desktop, sql, logger, signal: controller.signal, tasks };
557
+ let stopped = false;
558
+
559
+ const stop = async (): Promise<void> => {
560
+ if (stopped) return;
561
+ stopped = true;
562
+ controller.abort();
563
+ try {
564
+ await config.lifecycle?.stop?.(ctx);
565
+ } finally {
566
+ await tasks.stop();
567
+ unchainExternalAbort();
568
+ }
569
+ };
570
+
571
+ if (options.shutdownSignals ?? true) {
572
+ const runtimeProcess = typeof process !== "undefined" ? process : null;
573
+ if (runtimeProcess?.on) {
574
+ const shutdown = () => {
575
+ void stop().then(() => runtimeProcess.exit(0));
576
+ };
577
+ runtimeProcess.on("SIGTERM", shutdown);
578
+ runtimeProcess.on("SIGINT", shutdown);
579
+ }
580
+ }
581
+
582
+ await config.lifecycle?.setup?.(ctx);
583
+ await config.lifecycle?.start?.(ctx);
584
+
585
+ return { tasks, bridge: createDesktopTaskBridge(tasks), signal: controller.signal, stop };
586
+ };
587
+
588
+ export const createDesktopTaskBridge = (
589
+ tasks: DesktopTaskSupervisor,
590
+ ): Pick<DesktopBridge, "submitTask" | "getTaskStatus" | "listTasks"> => ({
591
+ submitTask: (id) => result(() => tasks.submit(id)),
592
+ getTaskStatus: (id) => result(() => tasks.status(id)),
593
+ listTasks: () => result(() => tasks.list()),
594
+ });
595
+
596
+ export const defineDesktopApp = <Config extends DesktopAppConfig>(config: Config): Config => config;
597
+
598
+ export const installDesktopBridge = (nextBridge: DesktopBridge | null): void => {
599
+ bridgeOverride = nextBridge;
600
+ if (hasWindow()) window.cloudDesktopRuntime = nextBridge ?? undefined;
601
+ };
602
+
603
+ export const desktop = {
604
+ get env(): DesktopEnvironment {
605
+ return hasWindow() ? (window.cloudDesktopEnvironment ?? browserEnvironment) : browserEnvironment;
606
+ },
607
+
608
+ get sql(): DesktopSql {
609
+ return getSql();
610
+ },
611
+
612
+ navigate: (href: string, options: { replace?: boolean } = {}): void => {
613
+ if (!hasWindow()) return;
614
+ if (options.replace) window.history.replaceState(null, "", href);
615
+ else window.history.pushState(null, "", href);
616
+ emitNavigation();
617
+ },
618
+
619
+ back: (): void => {
620
+ if (hasWindow()) window.history.back();
621
+ },
622
+
623
+ forward: (): void => {
624
+ if (hasWindow()) window.history.forward();
625
+ },
626
+
627
+ environment: {
628
+ get: async (): Promise<DesktopEnvironment> => {
629
+ const result = await (bridge()?.getEnvironment?.() ?? Promise.resolve({ ok: true as const, data: browserEnvironment }));
630
+ if (result.ok && hasWindow()) window.cloudDesktopEnvironment = result.data;
631
+ if (!result.ok) throw new Error(result.error);
632
+ return result.data;
633
+ },
634
+ },
635
+
636
+ dialog: {
637
+ openFile: (options?: DesktopFileDialogOptions): Promise<DesktopFileDialogResult> =>
638
+ unwrap(bridge()?.openFileDialog?.(options) ?? unsupported("Native file dialogs")),
639
+ },
640
+
641
+ message: {
642
+ show: (options: DesktopMessageOptions): Promise<DesktopMessageResult> =>
643
+ unwrap(bridge()?.showMessage?.(options) ?? unsupported("Native message boxes")),
644
+ info: (message: string, options: Omit<DesktopMessageOptions, "message" | "type"> = {}): Promise<DesktopMessageResult> =>
645
+ desktop.message.show({ ...options, message, type: "info" }),
646
+ warning: (message: string, options: Omit<DesktopMessageOptions, "message" | "type"> = {}): Promise<DesktopMessageResult> =>
647
+ desktop.message.show({ ...options, message, type: "warning" }),
648
+ error: (message: string, options: Omit<DesktopMessageOptions, "message" | "type"> = {}): Promise<DesktopMessageResult> =>
649
+ desktop.message.show({ ...options, message, type: "error" }),
650
+ },
651
+
652
+ notification: {
653
+ show: (options: DesktopNotificationOptions): Promise<void> =>
654
+ unwrap(bridge()?.showNotification?.(options) ?? unsupported("Native notifications")),
655
+ },
656
+
657
+ clipboard: {
658
+ writeText: (value: string): Promise<void> => unwrap(bridge()?.clipboardWriteText?.(value) ?? unsupported("Native clipboard access")),
659
+ readText: (): Promise<string> => unwrap(bridge()?.clipboardReadText?.() ?? unsupported("Native clipboard access")),
660
+ },
661
+
662
+ contextMenu: {
663
+ show: (items: DesktopContextMenuItem[]): Promise<void> =>
664
+ unwrap(bridge()?.showContextMenu?.(items) ?? unsupported("Native context menus")),
665
+ },
666
+
667
+ external: {
668
+ open: (url: string): Promise<boolean> => unwrap(bridge()?.openExternal?.(url) ?? unsupported("Native external URL opening")),
669
+ },
670
+
671
+ window: {
672
+ close: (): Promise<void> => unwrap(bridge()?.closeWindow?.() ?? unsupported("Native window controls")),
673
+ minimize: (): Promise<void> => unwrap(bridge()?.minimizeWindow?.() ?? unsupported("Native window controls")),
674
+ maximize: (): Promise<void> => unwrap(bridge()?.maximizeWindow?.() ?? unsupported("Native window controls")),
675
+ current: async (): Promise<DesktopWindowDescriptor | null> => {
676
+ const fromUrl = readDesktopWindowDescriptor();
677
+ if (fromUrl) return fromUrl;
678
+ return unwrap(bridge()?.getCurrentWindowDescriptor?.() ?? Promise.resolve({ ok: true as const, data: null }));
679
+ },
680
+ open: async (view: unknown, options: DesktopWindowOpenOptions = {}): Promise<DesktopWindowRef> => {
681
+ if (!isDesktopWindowDescriptor(view)) throw new Error("desktop.window.open expects a desktop window component.");
682
+ const nativeOpen = bridge()?.openWindow;
683
+ if (nativeOpen) return windowRef(await unwrap(nativeOpen({ descriptor: view, options })));
684
+ if (!hasWindow()) return windowRef(await unwrap(unsupported<DesktopWindowRefData>("Native windows")));
685
+ const child = window.open(descriptorUrl(view), "_blank", `popup,width=${options.width ?? 900},height=${options.height ?? 720}`);
686
+ if (!child) throw new Error("The browser blocked the new window.");
687
+ return windowRef({ id: "browser" }, child);
688
+ },
689
+ },
690
+
691
+ tasks: {
692
+ submit: (id: string): Promise<DesktopTaskStatus> => unwrap(bridge()?.submitTask?.(id) ?? unsupported("Desktop background tasks")),
693
+ status: (id: string): Promise<DesktopTaskStatus | null> =>
694
+ unwrap(bridge()?.getTaskStatus?.(id) ?? unsupported("Desktop background tasks")),
695
+ list: (): Promise<DesktopTaskStatus[]> => unwrap(bridge()?.listTasks?.() ?? unsupported("Desktop background tasks")),
696
+ },
697
+ };
698
+
699
+ declare global {
700
+ interface Window {
701
+ cloudDesktopRuntime?: DesktopBridge;
702
+ cloudDesktopEnvironment?: DesktopEnvironment;
703
+ }
704
+ }