@deveye/types 0.15.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -0
  3. package/package.json +68 -0
  4. package/src/domain/audience.ts +549 -0
  5. package/src/domain/backup.ts +355 -0
  6. package/src/domain/credential.ts +55 -0
  7. package/src/domain/database.ts +467 -0
  8. package/src/domain/deploy.ts +231 -0
  9. package/src/domain/device.ts +172 -0
  10. package/src/domain/deviceFiles.ts +84 -0
  11. package/src/domain/deviceLogs.ts +82 -0
  12. package/src/domain/featureRegistry.ts +392 -0
  13. package/src/domain/finance.ts +477 -0
  14. package/src/domain/git.ts +419 -0
  15. package/src/domain/home.ts +314 -0
  16. package/src/domain/live.ts +272 -0
  17. package/src/domain/logs.ts +117 -0
  18. package/src/domain/mail.ts +394 -0
  19. package/src/domain/metrics.ts +127 -0
  20. package/src/domain/note.ts +202 -0
  21. package/src/domain/notifications.ts +268 -0
  22. package/src/domain/packages.ts +35 -0
  23. package/src/domain/password.ts +36 -0
  24. package/src/domain/presence.ts +21 -0
  25. package/src/domain/project.ts +168 -0
  26. package/src/domain/projectBoard.ts +130 -0
  27. package/src/domain/projectChat.ts +46 -0
  28. package/src/domain/projectHistory.ts +82 -0
  29. package/src/domain/projectLink.ts +87 -0
  30. package/src/domain/projectPlan.ts +68 -0
  31. package/src/domain/report.ts +492 -0
  32. package/src/domain/role.ts +8 -0
  33. package/src/domain/secrecy.ts +66 -0
  34. package/src/domain/sentinel.ts +623 -0
  35. package/src/domain/sharing.ts +186 -0
  36. package/src/domain/syncProtocol.ts +116 -0
  37. package/src/domain/twoFactor.ts +40 -0
  38. package/src/domain/uptime.ts +216 -0
  39. package/src/domain/user.ts +141 -0
  40. package/src/domain/workspace.ts +56 -0
  41. package/src/domain/workspaceRole.ts +251 -0
  42. package/src/features/admin.ts +112 -0
  43. package/src/features/audience.ts +275 -0
  44. package/src/features/backup.ts +230 -0
  45. package/src/features/database.ts +461 -0
  46. package/src/features/deploy.ts +245 -0
  47. package/src/features/device.ts +292 -0
  48. package/src/features/deviceFiles.ts +83 -0
  49. package/src/features/deviceLogs.ts +36 -0
  50. package/src/features/deviceTerminal.ts +57 -0
  51. package/src/features/finance.ts +360 -0
  52. package/src/features/git.ts +368 -0
  53. package/src/features/home.ts +32 -0
  54. package/src/features/live.ts +113 -0
  55. package/src/features/logs.ts +86 -0
  56. package/src/features/mail.ts +374 -0
  57. package/src/features/metrics.ts +185 -0
  58. package/src/features/note.ts +189 -0
  59. package/src/features/notify.ts +164 -0
  60. package/src/features/password.ts +67 -0
  61. package/src/features/project.ts +709 -0
  62. package/src/features/registry.ts +103 -0
  63. package/src/features/secrecy.ts +120 -0
  64. package/src/features/sentinel.ts +233 -0
  65. package/src/features/sharing.ts +79 -0
  66. package/src/features/twoFactor.ts +47 -0
  67. package/src/features/uptime.ts +186 -0
  68. package/src/features/user.ts +91 -0
  69. package/src/features/workspace.ts +200 -0
  70. package/src/http/auth.ts +94 -0
  71. package/src/http/device.ts +222 -0
  72. package/src/http/status.ts +45 -0
  73. package/src/index.ts +1700 -0
  74. package/src/protocol/agent.ts +1171 -0
  75. package/src/protocol/envelope.ts +46 -0
  76. package/src/protocol/error.ts +27 -0
  77. package/src/protocol/result.ts +17 -0
  78. package/src/protocol/version.ts +6 -0
  79. package/src/sdk/client-ambient.d.ts +238 -0
  80. package/src/sdk/client.ts +66 -0
  81. package/src/sdk/ids.ts +25 -0
  82. package/src/sdk/index.ts +11 -0
  83. package/src/sdk/manifest.ts +326 -0
  84. package/src/sdk/providers.ts +48 -0
  85. package/src/sdk/server.ts +378 -0
  86. package/src/sdk/testing.ts +179 -0
  87. package/src/utils/version.ts +28 -0
@@ -0,0 +1,46 @@
1
+ import { z } from 'zod';
2
+ import { ProtocolErrorSchema } from './error';
3
+
4
+ /**
5
+ * WebSocket envelope shared between client and server.
6
+ *
7
+ * Client sends `ClientMessage` with `command`, `requestId` and `payload`.
8
+ * Server replies with `ServerMessage` whose `requestId` matches; payload is a
9
+ * `Result`-shaped object (success or protocol error). The server may also push
10
+ * unsolicited events with no `requestId`.
11
+ */
12
+
13
+ export const clientMessageSchema = z.object({
14
+ requestId: z.string().min(1),
15
+ command: z.string().min(1),
16
+ /**
17
+ * Espace de travail visé par la commande. Absent → l'espace personnel de
18
+ * l'appelant.
19
+ *
20
+ * Il vit sur l'enveloppe, et non dans le `payload` de chaque commande, pour
21
+ * trois raisons : aucun schéma d'entrée ne porte de `workspaceId`, aucun site
22
+ * d'appel client ne le passe à la main (`ws.send` l'estampille), et le
23
+ * serveur n'a qu'un seul point de résolution et d'autorisation. Le tenir dans
24
+ * un état de session côté serveur serait plus fragile : la socket se
25
+ * reconnecte seule (backoff, retour de focus) et une commande émise avant la
26
+ * ré-activation viserait le mauvais espace — ici chaque message se décrit
27
+ * lui-même.
28
+ */
29
+ workspaceId: z.number().int().positive().optional(),
30
+ payload: z.unknown()
31
+ });
32
+
33
+ export type ClientMessage = z.infer<typeof clientMessageSchema>;
34
+
35
+ export const serverMessageSchema = z.object({
36
+ requestId: z.string().min(1).optional(),
37
+ command: z.string().min(1),
38
+ payload: z.discriminatedUnion('ok', [
39
+ z.object({ ok: z.literal(true), data: z.unknown() }),
40
+ z.object({ ok: z.literal(false), error: ProtocolErrorSchema })
41
+ ])
42
+ });
43
+
44
+ export type ServerMessage = z.infer<typeof serverMessageSchema>;
45
+
46
+ export type ConnectionState = 'idle' | 'connecting' | 'open' | 'closing' | 'closed' | 'error';
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+
3
+ export const ErrorCodeSchema = z.enum([
4
+ 'auth_required',
5
+ 'auth_invalid',
6
+ 'auth_expired',
7
+ 'forbidden',
8
+ 'not_found',
9
+ 'conflict',
10
+ 'validation',
11
+ 'rate_limited',
12
+ 'internal',
13
+ 'unsupported_version',
14
+ // Password-based encryption is enabled but the session has not yet been
15
+ // unlocked with the user's password. The client should prompt for it.
16
+ 'locked'
17
+ ]);
18
+
19
+ export type ErrorCode = z.infer<typeof ErrorCodeSchema>;
20
+
21
+ export const ProtocolErrorSchema = z.object({
22
+ code: ErrorCodeSchema,
23
+ message: z.string(),
24
+ details: z.unknown().optional()
25
+ });
26
+
27
+ export type ProtocolError = z.infer<typeof ProtocolErrorSchema>;
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ import { ProtocolErrorSchema, type ErrorCode, type ProtocolError } from './error';
3
+
4
+ export type Result<T, E = ProtocolError> = { ok: true; data: T } | { ok: false; error: E };
5
+
6
+ export const ok = <T>(data: T): Result<T> => ({ ok: true, data });
7
+
8
+ export const err = (code: ErrorCode, message: string, details?: unknown): Result<never> => ({
9
+ ok: false,
10
+ error: { code, message, ...(details !== undefined ? { details } : {}) }
11
+ });
12
+
13
+ export const resultSchema = <T extends z.ZodTypeAny>(data: T) =>
14
+ z.discriminatedUnion('ok', [
15
+ z.object({ ok: z.literal(true), data }),
16
+ z.object({ ok: z.literal(false), error: ProtocolErrorSchema })
17
+ ]);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Protocol version negotiated between client and server.
3
+ * Bump on any breaking change to wire format, feature schemas or error codes.
4
+ */
5
+ export const PROTOCOL_VERSION = '0.3.0' as const;
6
+ export type ProtocolVersion = typeof PROTOCOL_VERSION;
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Ambient declaration of `deveye-sdk-client` — the runtime barrel the APP
3
+ * provides when a feature module is compiled in (resolved by alias inside
4
+ * DevEye). The module itself is app code and is published nowhere; this file
5
+ * is its typed portrait, so a module repo can `tsc --noEmit` standalone.
6
+ *
7
+ * How it is consumed:
8
+ * - a module repo (the template) pulls it into its standalone typecheck via
9
+ * one tsconfig `include` entry pointing at this file in node_modules;
10
+ * - DevEye itself verifies MECHANICALLY that the real barrel honours this
11
+ * declaration (`client/npm run check:sdk`): a drift breaks the app's CI,
12
+ * never a third-party build.
13
+ *
14
+ * Extend it when the app's stable surface grows — same gesture as before,
15
+ * one file instead of one per module repo.
16
+ */
17
+ declare module 'deveye-sdk-client' {
18
+ import type {
19
+ ButtonHTMLAttributes,
20
+ ChangeEvent,
21
+ ComponentType,
22
+ CSSProperties,
23
+ InputHTMLAttributes,
24
+ ReactNode,
25
+ SelectHTMLAttributes
26
+ } from 'react';
27
+ import type { z, ZodType } from 'zod';
28
+ import type { FeatureAccess, FeatureId, WorkspaceCapability } from 'deveye-types';
29
+ import type { FeatureManifest } from 'deveye-types/sdk';
30
+
31
+ // ── UI kit ─────────────────────────────────────────────────────────────
32
+ export const Button: ComponentType<
33
+ ButtonHTMLAttributes<HTMLButtonElement> & {
34
+ variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
35
+ icon?: string;
36
+ }
37
+ >;
38
+ export const TextInput: ComponentType<
39
+ InputHTMLAttributes<HTMLInputElement> & { enableShowHideButton?: boolean }
40
+ >;
41
+ export const SelectInput: ComponentType<SelectHTMLAttributes<HTMLSelectElement>>;
42
+ export const Checkbox: ComponentType<{
43
+ checked: boolean;
44
+ /** Receives the state AFTER the click — no event to unwrap. */
45
+ onChange: (checked: boolean) => void;
46
+ children?: ReactNode;
47
+ disabled?: boolean;
48
+ /** Required when there is no visible label. */
49
+ 'aria-label'?: string;
50
+ }>;
51
+ export const Switch: ComponentType<{
52
+ checked: boolean;
53
+ onChange: (checked: boolean) => void;
54
+ label?: string;
55
+ hint?: string;
56
+ disabled?: boolean;
57
+ 'aria-label'?: string;
58
+ }>;
59
+ export function SegmentedControl<T extends string>(props: {
60
+ value: T;
61
+ options: readonly { value: T; label: string; title?: string }[];
62
+ onChange: (value: T) => void;
63
+ className?: string;
64
+ 'aria-label'?: string;
65
+ disabled?: boolean;
66
+ }): ReactNode;
67
+ export const Dialog: ComponentType<{
68
+ open: boolean;
69
+ onClose: () => void;
70
+ title?: string;
71
+ description?: ReactNode;
72
+ children?: ReactNode;
73
+ footer?: ReactNode;
74
+ /** Extra action in the top-right corner, left of the close button. */
75
+ headerAction?: ReactNode;
76
+ width?: number;
77
+ onSubmit?: () => void;
78
+ fill?: boolean;
79
+ holdSecrecy?: boolean;
80
+ }>;
81
+ export const StatusBadge: ComponentType<{
82
+ tone?: 'online' | 'offline' | 'success' | 'warning' | 'danger' | 'accent' | 'neutral';
83
+ /** Show the leading status dot (default true). */
84
+ dot?: boolean;
85
+ children: ReactNode;
86
+ className?: string;
87
+ }>;
88
+ /** One confirmation dialog for the whole app — see ConfirmDialog. */
89
+ export interface ConfirmRequest {
90
+ title: string;
91
+ description?: ReactNode;
92
+ confirmLabel?: string;
93
+ /** `primary` for a reversible action; `danger` (default) otherwise. */
94
+ tone?: 'danger' | 'primary';
95
+ onConfirm: () => void;
96
+ }
97
+ export const ConfirmDialog: ComponentType<{
98
+ /** The pending request, or `null` when nothing awaits confirmation. */
99
+ request: ConfirmRequest | null;
100
+ onClose: () => void;
101
+ busy?: boolean;
102
+ }>;
103
+ export const FeatureSettingsButton: ComponentType<{
104
+ scope:
105
+ | { kind: 'feature'; feature: FeatureId }
106
+ | { kind: 'item'; feature: FeatureId; itemId: number; itemLabel: string };
107
+ variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
108
+ label?: string;
109
+ }>;
110
+ /** The canonical settings row classes (channelRow, field, sectionHint...). */
111
+ export const settingsStyles: Readonly<Record<string, string>>;
112
+
113
+ // ── Server push events ─────────────────────────────────────────────────
114
+ /** Typed push subscription: filters `event`, safeParses, drops mismatches. */
115
+ export function onServerEvent<T>(
116
+ event: string,
117
+ schema: ZodType<T>,
118
+ cb: (payload: T) => void
119
+ ): () => void;
120
+ /** Fires now if the socket is open, then on every reopen (resubscribe primitive). */
121
+ export function onSocketOpen(cb: () => void): () => void;
122
+ /** Whether the socket is open right now (to tell a real error from an outage). */
123
+ export function isSocketOpen(): boolean;
124
+
125
+ // ── Data ───────────────────────────────────────────────────────────────
126
+ /**
127
+ * A resource key an EXTERNAL module may own: `x-<slug>.<name>`, as listed
128
+ * in its manifest `resources`. The app's bus is typed on the closed union
129
+ * of native keys plus this shape — an arbitrary string is refused there,
130
+ * so it is refused here too.
131
+ */
132
+ export type ExternalResourceKey = `x-${string}.${string}`;
133
+ export function useResource<T>(
134
+ key: ExternalResourceKey,
135
+ load: () => Promise<T>,
136
+ fallback: string,
137
+ deps?: readonly unknown[]
138
+ ): { data: T | null; error: string | null; loading: boolean; reload: () => void };
139
+ export function invalidate(...keys: ExternalResourceKey[]): void;
140
+ export function useResourceVersion(key: ExternalResourceKey): number;
141
+ /** Imperative subscription to one resource key's invalidations. Returns the unsubscribe. */
142
+ export function onResourceChange(key: ExternalResourceKey, cb: () => void): () => void;
143
+ export function humanizeError(error: unknown, fallback: string): string;
144
+ export function featureApi<const M extends FeatureManifest>(
145
+ manifest: M
146
+ ): {
147
+ send<N extends M['commands'][number]['command']>(
148
+ name: N,
149
+ input: z.input<Extract<M['commands'][number], { command: N }>['input'] & ZodType>,
150
+ /** `timeoutMs` stretches the wait for a command that queries a slow third party. */
151
+ opts?: { timeoutMs?: number }
152
+ ): Promise<z.output<Extract<M['commands'][number], { command: N }>['output'] & ZodType>>;
153
+ };
154
+
155
+ // ── Password-based encryption (secrecy) ────────────────────────────────
156
+ // The surface any feature needs when one of its commands can answer
157
+ // `locked` (contracts stored with `'private'` encryption server-side).
158
+ export interface SecrecyState {
159
+ /** True when password-based encryption is enabled for the account. */
160
+ enabled: boolean;
161
+ /** True while the session holds the unlocked key. */
162
+ unlocked: boolean;
163
+ }
164
+ /** Live lock state of the session (shared with the topbar widget and the global prompt). */
165
+ export function useSecrecy(): SecrecyState;
166
+ /** Resolves once the session is unlocked, opening the global prompt if needed. Rejects on cancel. */
167
+ export function ensureSecrecyUnlocked(): Promise<void>;
168
+ /** Runs `run`; on a `locked` error, opens the unlock prompt and retries once. */
169
+ export function withSecrecy<T>(run: () => Promise<T>): Promise<T>;
170
+
171
+ // ── Live ───────────────────────────────────────────────────────────────
172
+ export type LiveSegmentKind = 'view' | 'l1' | 'l2' | 'l3' | 'l4';
173
+ export function useLiveSegment(
174
+ kind: LiveSegmentKind,
175
+ value: string | null
176
+ ): { value: string | null } | null;
177
+ export function useLiveOutline(kind: LiveSegmentKind, value: string | null): LiveOutlineProps;
178
+ export function useLiveOutlines(
179
+ kind: LiveSegmentKind
180
+ ): (value: string | null) => LiveOutlineProps;
181
+ export function useLiveItemTarget(
182
+ kind: LiveSegmentKind,
183
+ value: string | null,
184
+ ready: boolean,
185
+ onTarget: (value: string | null) => void
186
+ ): void;
187
+ export function useTypers(): { connId: string; userId: number }[];
188
+ export function useTypingSignal(): { onInput: () => void; stop: () => void };
189
+
190
+ // ── Shared helpers ─────────────────────────────────────────────────────
191
+ /** Byte size with French units (o / Ko / Mo / Go). */
192
+ export function formatBytesFr(bytes: number): string;
193
+ /** Folder picker over an enrolled device's filesystem (shared with Backup). */
194
+ export const DeviceFolderPicker: ComponentType<{
195
+ open: boolean;
196
+ deviceId: string;
197
+ deviceName: string;
198
+ onClose: () => void;
199
+ /** Called with the absolute path of the folder picked on the device. */
200
+ onPick: (path: string) => void;
201
+ /** What the folder is for, said by the caller. */
202
+ description?: string;
203
+ }>;
204
+ /** The workspace's enrolled devices, live (a polled store, not a bare list). */
205
+ export function useDevices(): {
206
+ devices: { id: string; name: string; online: boolean }[];
207
+ loading: boolean;
208
+ error: string | null;
209
+ refresh: () => Promise<void>;
210
+ };
211
+ export interface LiveOutlineProps {
212
+ 'data-live-peer'?: true;
213
+ style?: CSSProperties;
214
+ }
215
+
216
+ // ── Rights and workspace ───────────────────────────────────────────────
217
+ export function useWorkspacePermissions(): {
218
+ isOwner: boolean;
219
+ can: (c: WorkspaceCapability) => boolean;
220
+ canFeature: (f: FeatureId, level?: FeatureAccess) => boolean;
221
+ canChannels: (f: FeatureId) => boolean;
222
+ canExtra: (f: FeatureId, key: string) => boolean;
223
+ extraValue: (
224
+ f: FeatureId,
225
+ key: string,
226
+ spec: { default: string; ownerValue: string }
227
+ ) => string;
228
+ };
229
+ export function useActiveWorkspace(): {
230
+ id: number;
231
+ kind: 'personal' | 'shared';
232
+ name: string;
233
+ } | null;
234
+ export function useFeatureLifecycle(hooks: { onUnmount?: () => void }): void;
235
+
236
+ // Change events re-exported for convenience in handlers.
237
+ export type InputChange = ChangeEvent<HTMLInputElement>;
238
+ }
@@ -0,0 +1,66 @@
1
+ import type { ComponentType } from 'react';
2
+
3
+ /**
4
+ * Client-side SDK contracts: what your package's `./client` entry exports and
5
+ * what your components receive.
6
+ *
7
+ * Your client code imports its runtime helpers (UI kit, `useResource`,
8
+ * `featureApi`, live hooks, permissions) from `deveye-sdk-client`, which the
9
+ * app provides when your module is compiled in. This file only carries the
10
+ * shapes both sides must agree on.
11
+ */
12
+
13
+ /** The scope a settings panel is opened for. */
14
+ export type SdkSettingsScope =
15
+ { kind: 'feature' } | { kind: 'item'; itemId: number; itemLabel: string };
16
+
17
+ export interface SettingsPanelProps {
18
+ scope: SdkSettingsScope;
19
+ /** Caller has `write` on the feature. Render read-only when false. */
20
+ canWrite: boolean;
21
+ }
22
+
23
+ export interface FeatureViewProps {
24
+ /** Closes the full view (the popup over the grid). */
25
+ closeFeature(): void;
26
+ }
27
+
28
+ /**
29
+ * Your package's `./client` export.
30
+ */
31
+ export interface FeatureClient {
32
+ /**
33
+ * The card body on the home grid. NO props, like every native widget:
34
+ * data comes from `useResource`, so the card and the full view share one
35
+ * cache and refresh together.
36
+ */
37
+ Widget: ComponentType;
38
+ /** The full view, opened when the card expands. */
39
+ Full: ComponentType<FeatureViewProps>;
40
+ /**
41
+ * Panels for the manifest's settings tabs that need one: `'general'`,
42
+ * `'sources'`, and any custom tab id. Generic tabs (`'notifications'`,
43
+ * `'permissions'`) need no panel.
44
+ */
45
+ settingsPanels?: Readonly<Record<string, ComponentType<SettingsPanelProps>>>;
46
+ /**
47
+ * The compact topbar widget declared by `manifest.topbarWidget`.
48
+ *
49
+ * Rendered with NO props, on purpose: that is the security contract. The
50
+ * host hands the component nothing (no user, no stores, no other feature's
51
+ * state); everything it shows must come through YOUR feature's commands
52
+ * (`featureApi`), which the server authorizes against the caller's grants
53
+ * like any other call. The host also only mounts it for members whose role
54
+ * grants your feature. Freedom inside the box, nothing outside it.
55
+ */
56
+ TopbarWidget?: ComponentType;
57
+ /**
58
+ * Minutes the full view stays mounted (state preserved) after closing.
59
+ * `0` = unmount immediately; omit = mounted forever.
60
+ */
61
+ cacheDurationMinutes?: number;
62
+ /** Warm the full view at idle after load, when the card is placed. */
63
+ preload?: boolean;
64
+ /** Hold the password-encryption unlock alive while the full view is open. */
65
+ holdSecrecy?: boolean;
66
+ }
package/src/sdk/ids.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Feature ids, re-exported for SDK consumers.
3
+ *
4
+ * DevEye has two id families sharing one namespace:
5
+ * - the sixteen **native** feature ids, a closed enum (`WorkspaceFeatureId`);
6
+ * - **external** module ids, `x-<slug>` strings matching
7
+ * {@link EXTERNAL_FEATURE_ID_PATTERN}.
8
+ *
9
+ * The `x-` prefix guarantees, by construction, that an external id can never
10
+ * collide with a native feature, a reserved live topic, or a device UUID in a
11
+ * home layout. Your module's id is also its command prefix (`x-crypto.list`)
12
+ * and its live topic.
13
+ */
14
+ export {
15
+ EXTERNAL_FEATURE_ID_PATTERN,
16
+ externalFeatureIdSchema,
17
+ featureIdSchema,
18
+ isExternalFeatureId,
19
+ workspaceFeatureIdSchema,
20
+ type ExternalFeatureId,
21
+ type FeatureId,
22
+ type WorkspaceFeatureId
23
+ } from '../domain/workspaceRole';
24
+
25
+ export { featureAccessSchema, type FeatureAccess } from '../domain/workspaceRole';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The DevEye feature SDK, shared surface: ids and the manifest contract.
3
+ *
4
+ * Side-specific surfaces live in their own entries so neither bundle drags the
5
+ * other's world in: `deveye-types/sdk/server` (handlers, storage, facade),
6
+ * `deveye-types/sdk/client` (component contracts), `deveye-types/sdk/testing`
7
+ * (handler test harness).
8
+ */
9
+ export * from './ids';
10
+ export * from './manifest';
11
+ export * from './providers';