@deveye/types 0.15.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -6
- package/src/domain/device.ts +4 -29
- package/src/domain/featureRegistry.ts +40 -108
- package/src/domain/home.ts +40 -101
- package/src/domain/live.ts +36 -87
- package/src/domain/metrics.ts +10 -14
- package/src/domain/notifications.ts +39 -110
- package/src/domain/project.ts +3 -162
- package/src/domain/report.ts +46 -95
- package/src/domain/secrecy.ts +3 -5
- package/src/domain/sharing.ts +33 -70
- package/src/domain/syncProtocol.ts +5 -12
- package/src/domain/user.ts +8 -14
- package/src/domain/workspace.ts +0 -3
- package/src/domain/workspaceRole.ts +34 -82
- package/src/features/agent.ts +306 -0
- package/src/features/live.ts +17 -37
- package/src/features/notify.ts +19 -57
- package/src/features/registry.ts +7 -44
- package/src/features/secrecy.ts +4 -9
- package/src/features/sharing.ts +12 -26
- package/src/features/user.ts +6 -11
- package/src/features/workspace.ts +6 -11
- package/src/http/auth.ts +6 -13
- package/src/http/device.ts +13 -17
- package/src/http/status.ts +6 -10
- package/src/index.ts +29 -932
- package/src/protocol/agent.ts +40 -70
- package/src/protocol/envelope.ts +3 -10
- package/src/sdk/client-ambient.d.ts +244 -22
- package/src/sdk/client.ts +277 -17
- package/src/sdk/devb.ts +120 -0
- package/src/sdk/manifest.test.ts +96 -0
- package/src/sdk/manifest.ts +160 -25
- package/src/sdk/providers.ts +310 -5
- package/src/sdk/server.ts +483 -19
- package/src/sdk/testing.test.ts +62 -0
- package/src/sdk/testing.ts +416 -40
- package/src/utils/version.ts +5 -8
- package/src/domain/audience.ts +0 -549
- package/src/domain/backup.ts +0 -355
- package/src/domain/credential.ts +0 -55
- package/src/domain/database.ts +0 -467
- package/src/domain/deploy.ts +0 -231
- package/src/domain/finance.ts +0 -477
- package/src/domain/git.ts +0 -419
- package/src/domain/mail.ts +0 -394
- package/src/domain/note.ts +0 -202
- package/src/domain/password.ts +0 -36
- package/src/domain/projectBoard.ts +0 -130
- package/src/domain/projectChat.ts +0 -46
- package/src/domain/projectHistory.ts +0 -82
- package/src/domain/projectLink.ts +0 -87
- package/src/domain/projectPlan.ts +0 -68
- package/src/domain/sentinel.ts +0 -623
- package/src/domain/uptime.ts +0 -216
- package/src/features/audience.ts +0 -275
- package/src/features/backup.ts +0 -230
- package/src/features/database.ts +0 -461
- package/src/features/deploy.ts +0 -245
- package/src/features/device.ts +0 -292
- package/src/features/deviceFiles.ts +0 -83
- package/src/features/deviceLogs.ts +0 -36
- package/src/features/deviceTerminal.ts +0 -57
- package/src/features/finance.ts +0 -360
- package/src/features/git.ts +0 -368
- package/src/features/mail.ts +0 -374
- package/src/features/metrics.ts +0 -185
- package/src/features/note.ts +0 -189
- package/src/features/password.ts +0 -67
- package/src/features/project.ts +0 -709
- package/src/features/sentinel.ts +0 -233
- package/src/features/uptime.ts +0 -186
package/src/sdk/manifest.ts
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import type { ZodType } from 'zod';
|
|
2
|
+
import type { FeatureDescriptor } from '../domain/featureRegistry';
|
|
2
3
|
import {
|
|
3
4
|
EXTERNAL_FEATURE_ID_PATTERN,
|
|
4
5
|
isExternalFeatureId,
|
|
6
|
+
type ExternalFeatureId,
|
|
5
7
|
type FeatureId
|
|
6
8
|
} from '../domain/workspaceRole';
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* The feature manifest: everything DevEye needs to know about a feature,
|
|
10
|
-
* declared once.
|
|
11
|
-
*
|
|
12
|
-
* Historically this knowledge was scattered across ~14 registration points
|
|
13
|
-
* (descriptor, catalog entry, settings wiring tables, invalidation keys,
|
|
14
|
-
* teleport segments...). A module declares all of it here; the app's generated
|
|
15
|
-
* glue reads the manifest and wires every screen from it. Native features are
|
|
16
|
-
* being migrated onto the same contract (Weather first).
|
|
12
|
+
* declared once. The app's generated glue reads it and wires every screen
|
|
13
|
+
* (descriptor, catalog, settings tabs, invalidation keys, teleport segments).
|
|
17
14
|
*/
|
|
18
15
|
|
|
19
16
|
/**
|
|
@@ -76,23 +73,48 @@ export type NativeCapability =
|
|
|
76
73
|
| 'notify'
|
|
77
74
|
| 'mail.accounts'
|
|
78
75
|
| 'members.read'
|
|
76
|
+
/**
|
|
77
|
+
* List every workspace of this DevEye (id, name, kind, owner). A global
|
|
78
|
+
* administrator's surface only: the call refuses anyone else. What a
|
|
79
|
+
* fleet needs to attach a device to workspaces.
|
|
80
|
+
*/
|
|
81
|
+
| 'workspaces.read'
|
|
79
82
|
/** Read/authorize the workspace's devices. */
|
|
80
83
|
| 'devices.read'
|
|
81
84
|
/**
|
|
82
85
|
* The full agent-fleet sync transport (outbound requests, browser fan-out,
|
|
83
|
-
* per-socket subscriptions). Reserved for
|
|
84
|
-
*
|
|
86
|
+
* per-socket subscriptions). Reserved for native-id modules: the agent
|
|
87
|
+
* protocol is app infrastructure, not a third-party surface.
|
|
85
88
|
*/
|
|
86
|
-
| 'agents'
|
|
89
|
+
| 'agents'
|
|
90
|
+
/**
|
|
91
|
+
* Read the devices' telemetry (process instants, metric rows, evidence
|
|
92
|
+
* pinning). Reserved for native-id modules like `'agents'`: the metric
|
|
93
|
+
* store is app infrastructure.
|
|
94
|
+
*/
|
|
95
|
+
| 'telemetry.read'
|
|
96
|
+
/**
|
|
97
|
+
* Public HTTP routes: endpoints reachable WITHOUT a session (an analytics
|
|
98
|
+
* beacon), mounted on every listener the host exposes, with open CORS.
|
|
99
|
+
* Declared, because opening a door is the one thing a module must not do
|
|
100
|
+
* quietly. See `FeatureService.publicRoutes`.
|
|
101
|
+
*/
|
|
102
|
+
| 'routes.public';
|
|
87
103
|
|
|
88
104
|
/**
|
|
89
105
|
* Settings tabs the shell can render for you.
|
|
90
106
|
* - `'notifications'` and `'permissions'` are fully generic: DevEye renders
|
|
91
107
|
* them from the manifest alone, you write no component.
|
|
92
|
-
* - `'general'` and `'
|
|
93
|
-
* client entry (`settingsPanels`), keyed by
|
|
108
|
+
* - `'general'`, `'sources'`, `'sync'` and `'encryption'` need a panel
|
|
109
|
+
* component, provided by your client entry (`settingsPanels`), keyed by
|
|
110
|
+
* the tab id. `'sync'` (item scope only) is the cadence and maintenance of
|
|
111
|
+
* an item the module keeps fresh in the background (a mailbox).
|
|
112
|
+
* `'encryption'` (item scope only) is where an item chooses the form of
|
|
113
|
+
* its own data, when the feature leaves the choice: the shell names and
|
|
114
|
+
* places the tab, the module owns the choice.
|
|
94
115
|
*/
|
|
95
|
-
export type SettingsTab =
|
|
116
|
+
export type SettingsTab =
|
|
117
|
+
'general' | 'sources' | 'notifications' | 'permissions' | 'sync' | 'encryption';
|
|
96
118
|
|
|
97
119
|
/** A custom settings tab. Needs a matching panel in `settingsPanels`. */
|
|
98
120
|
export interface CustomTabRef {
|
|
@@ -125,6 +147,22 @@ export interface CrossTopicInvalidation {
|
|
|
125
147
|
/** Where a feature's card can live on the home grid. */
|
|
126
148
|
export type FeatureCategory = 'supervision' | 'security' | 'dev' | 'work' | 'daily' | 'analysis';
|
|
127
149
|
|
|
150
|
+
/**
|
|
151
|
+
* A data link from this feature to another one, as the "About" card of the
|
|
152
|
+
* home grid reads it (in both directions): a real coupling (an alert sender
|
|
153
|
+
* goes through a Mail account, a project points its repositories), not a
|
|
154
|
+
* thematic neighbourhood.
|
|
155
|
+
*/
|
|
156
|
+
export interface FeatureLink {
|
|
157
|
+
/** The feature linked to. Never your own id. */
|
|
158
|
+
to: FeatureId;
|
|
159
|
+
/** What the link allows, said from this feature's point of view. */
|
|
160
|
+
what: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Hard cap on `links`: the "About" card stays a card. */
|
|
164
|
+
export const MAX_FEATURE_LINKS = 6;
|
|
165
|
+
|
|
128
166
|
export interface FeatureManifest<Id extends FeatureId = FeatureId> {
|
|
129
167
|
/** External modules: `x-<slug>`. Native features keep their enum id. */
|
|
130
168
|
id: Id;
|
|
@@ -150,14 +188,20 @@ export interface FeatureManifest<Id extends FeatureId = FeatureId> {
|
|
|
150
188
|
sources?: { hint: string };
|
|
151
189
|
/**
|
|
152
190
|
* Whether an item can be projected into another workspace. Decided by
|
|
153
|
-
* encryption
|
|
154
|
-
*
|
|
191
|
+
* encryption: only the open tier is readable by the server alone. Anything
|
|
192
|
+
* but `'never'` commits the module to the sharing contract: a server entry
|
|
193
|
+
* with `items`, listings that read `ctx.sharing.scope()` and pick the
|
|
194
|
+
* cipher row by row, and `ctx.items.restrictions()` applied to what they
|
|
195
|
+
* return. External modules declare `'never'` for now.
|
|
155
196
|
*/
|
|
156
197
|
shareTier: 'open' | 'perItem' | 'never';
|
|
157
198
|
|
|
158
199
|
/** Card rendering on the home grid. `compact` halves the minimum height. */
|
|
159
200
|
tile?: { compact?: boolean };
|
|
160
201
|
|
|
202
|
+
/** See {@link FeatureLink}. At most {@link MAX_FEATURE_LINKS}. */
|
|
203
|
+
links?: readonly FeatureLink[];
|
|
204
|
+
|
|
161
205
|
/**
|
|
162
206
|
* The feature offers a compact TOPBAR widget (pinned top-right of the
|
|
163
207
|
* navbar). Declares only the DATA the picker shows; the component itself
|
|
@@ -184,6 +228,15 @@ export interface FeatureManifest<Id extends FeatureId = FeatureId> {
|
|
|
184
228
|
*/
|
|
185
229
|
invalidatedByTopic?: readonly string[];
|
|
186
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Secondary live topics of your own, beaten separately from your id so a
|
|
233
|
+
* frequent write does not refresh everything (a chat thread that must not
|
|
234
|
+
* re-fetch the board). Each names the keys it re-fetches (a subset of
|
|
235
|
+
* `resources`); its id starts with yours. A handler beats it with
|
|
236
|
+
* `mutates: ['<topic>']`, a service with `live.changed(ws, ['<topic>'])`.
|
|
237
|
+
*/
|
|
238
|
+
topics?: readonly { id: string; keys: readonly string[] }[];
|
|
239
|
+
|
|
187
240
|
/**
|
|
188
241
|
* Keys ALSO re-fetched when another feature's topic fires. The escape
|
|
189
242
|
* hatch for real data coupling (CloudSync's share rows carry device names:
|
|
@@ -200,13 +253,6 @@ export interface FeatureManifest<Id extends FeatureId = FeatureId> {
|
|
|
200
253
|
*/
|
|
201
254
|
commandPrefix?: string;
|
|
202
255
|
|
|
203
|
-
/**
|
|
204
|
-
* Builds the live/teleport segment for one item, e.g. `(id) => `job:${id}``.
|
|
205
|
-
* MUST return byte-for-byte what your view declares via `useLiveSegment('l1', ...)`:
|
|
206
|
-
* it is a rendezvous, not a convention. Requires `hasItems`.
|
|
207
|
-
*/
|
|
208
|
-
itemSegment?: (itemId: number | string) => string;
|
|
209
|
-
|
|
210
256
|
/** Settings tabs, per scope. Omit a scope to render no settings there. */
|
|
211
257
|
settings?: {
|
|
212
258
|
feature?: readonly (SettingsTab | CustomTabRef)[];
|
|
@@ -231,7 +277,6 @@ export function validateManifest(m: FeatureManifest): void {
|
|
|
231
277
|
if (!m.label.trim()) fail(m.id, 'empty label');
|
|
232
278
|
if (!m.description.trim()) fail(m.id, 'empty description');
|
|
233
279
|
if (m.hasItems && !m.itemNoun?.trim()) fail(m.id, 'hasItems requires itemNoun');
|
|
234
|
-
if (!m.hasItems && m.itemSegment) fail(m.id, 'itemSegment requires hasItems');
|
|
235
280
|
if (external && m.shareTier !== 'never') {
|
|
236
281
|
fail(m.id, "external modules must declare shareTier 'never' for now");
|
|
237
282
|
}
|
|
@@ -261,6 +306,27 @@ export function validateManifest(m: FeatureManifest): void {
|
|
|
261
306
|
fail(m.id, `invalidatedByTopic « ${key} » is not in resources`);
|
|
262
307
|
}
|
|
263
308
|
}
|
|
309
|
+
for (const topic of m.topics ?? []) {
|
|
310
|
+
if (
|
|
311
|
+
!/^[a-z][a-zA-Z0-9-]{1,31}$/.test(topic.id) ||
|
|
312
|
+
!topic.id.startsWith(m.id) ||
|
|
313
|
+
topic.id === m.id
|
|
314
|
+
) {
|
|
315
|
+
fail(m.id, `topic « ${topic.id} » must start with the feature id and differ from it`);
|
|
316
|
+
}
|
|
317
|
+
for (const key of topic.keys) {
|
|
318
|
+
if (!m.resources.includes(key)) {
|
|
319
|
+
fail(m.id, `topic « ${topic.id} » key « ${key} » is not in resources`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const links = m.links ?? [];
|
|
325
|
+
if (links.length > MAX_FEATURE_LINKS) fail(m.id, `more than ${MAX_FEATURE_LINKS} links`);
|
|
326
|
+
for (const link of links) {
|
|
327
|
+
if (link.to === m.id) fail(m.id, 'links must name ANOTHER feature');
|
|
328
|
+
if (!link.what.trim()) fail(m.id, `link to « ${link.to} »: empty what`);
|
|
329
|
+
}
|
|
264
330
|
|
|
265
331
|
const cross = m.alsoInvalidatedBy ?? [];
|
|
266
332
|
if (cross.length > 4) fail(m.id, 'more than 4 alsoInvalidatedBy entries');
|
|
@@ -273,8 +339,10 @@ export function validateManifest(m: FeatureManifest): void {
|
|
|
273
339
|
}
|
|
274
340
|
}
|
|
275
341
|
|
|
276
|
-
|
|
277
|
-
|
|
342
|
+
for (const reserved of ['agents', 'telemetry.read'] as const) {
|
|
343
|
+
if ((m.nativeCapabilities ?? []).includes(reserved) && external) {
|
|
344
|
+
fail(m.id, `capability '${reserved}' is reserved for native-id modules`);
|
|
345
|
+
}
|
|
278
346
|
}
|
|
279
347
|
|
|
280
348
|
const extras = m.extraPermissions ?? [];
|
|
@@ -313,6 +381,9 @@ export function validateManifest(m: FeatureManifest): void {
|
|
|
313
381
|
if (tab === 'sources' && scope === 'item') {
|
|
314
382
|
fail(m.id, 'sources is a feature-scope tab');
|
|
315
383
|
}
|
|
384
|
+
if (tab === 'encryption' && scope === 'feature') {
|
|
385
|
+
fail(m.id, 'encryption is an item-scope tab');
|
|
386
|
+
}
|
|
316
387
|
} else {
|
|
317
388
|
if (!CUSTOM_TAB_PATTERN.test(tab.id) || BUILTIN_TABS.includes(tab.id)) {
|
|
318
389
|
fail(m.id, `invalid custom tab id « ${tab.id} »`);
|
|
@@ -324,3 +395,67 @@ export function validateManifest(m: FeatureManifest): void {
|
|
|
324
395
|
}
|
|
325
396
|
}
|
|
326
397
|
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* The registry descriptor of an EXTERNAL module, read off its manifest: what
|
|
401
|
+
* the roles screen, the settings shell and the catalog need to know without
|
|
402
|
+
* opening the module. Natives keep their published descriptor; the app's
|
|
403
|
+
* server and client registries both project through here.
|
|
404
|
+
*/
|
|
405
|
+
export function externalDescriptorOf(
|
|
406
|
+
m: FeatureManifest
|
|
407
|
+
): FeatureDescriptor & { id: ExternalFeatureId } {
|
|
408
|
+
if (!isExternalFeatureId(m.id)) {
|
|
409
|
+
throw new Error(`externalDescriptorOf: « ${m.id} » is not an external id`);
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
id: m.id,
|
|
413
|
+
label: m.label,
|
|
414
|
+
description: m.description,
|
|
415
|
+
icon: m.icon,
|
|
416
|
+
notifies: m.notifies,
|
|
417
|
+
hasItems: m.hasItems,
|
|
418
|
+
itemNoun: m.itemNoun,
|
|
419
|
+
sources: m.sources,
|
|
420
|
+
shareTier: m.shareTier
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export interface ExtrasResolver {
|
|
425
|
+
canExtra(key: string): boolean;
|
|
426
|
+
extraValue(key: string): string;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* The runtime rules of extra permissions, shared by the app's request context
|
|
431
|
+
* and the test harness so the two can never drift:
|
|
432
|
+
* - a key the manifest does not declare, or of the other kind, yields
|
|
433
|
+
* `false` / `''`;
|
|
434
|
+
* - the workspace owner holds every toggle and gets `ownerValue` of every
|
|
435
|
+
* choice;
|
|
436
|
+
* - a member holds a toggle when the grant says `true`, and gets a choice's
|
|
437
|
+
* granted value when it is one of the options, `default` otherwise.
|
|
438
|
+
*/
|
|
439
|
+
export function resolveExtras(
|
|
440
|
+
specs: readonly ExtraPermissionSpec[] | undefined,
|
|
441
|
+
isOwner: boolean,
|
|
442
|
+
granted: Readonly<Record<string, boolean | string>>
|
|
443
|
+
): ExtrasResolver {
|
|
444
|
+
const byKey = new Map((specs ?? []).map((spec) => [spec.key, spec]));
|
|
445
|
+
return {
|
|
446
|
+
canExtra(key) {
|
|
447
|
+
const spec = byKey.get(key);
|
|
448
|
+
if (!spec || spec.type !== 'toggle') return false;
|
|
449
|
+
return isOwner || granted[key] === true;
|
|
450
|
+
},
|
|
451
|
+
extraValue(key) {
|
|
452
|
+
const spec = byKey.get(key);
|
|
453
|
+
if (!spec || spec.type !== 'choice') return '';
|
|
454
|
+
if (isOwner) return spec.ownerValue;
|
|
455
|
+
const value = granted[key];
|
|
456
|
+
return typeof value === 'string' && spec.options.some((o) => o.value === value)
|
|
457
|
+
? value
|
|
458
|
+
: spec.default;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}
|
package/src/sdk/providers.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Provider contracts: the inversion for
|
|
3
|
-
*
|
|
2
|
+
* Provider contracts: the inversion for code that needs a MODULE's data,
|
|
3
|
+
* whether it is the app or another module.
|
|
4
4
|
*
|
|
5
5
|
* A module exposes named contracts on its service (`FeatureService.providers`);
|
|
6
6
|
* the app looks them up at call time (`moduleProvider(key)` in its SDK
|
|
7
|
-
* assembly) and degrades cleanly when
|
|
8
|
-
*
|
|
9
|
-
* while neither may import the
|
|
7
|
+
* assembly, `ctx.providers.get(key)` from a module) and degrades cleanly when
|
|
8
|
+
* the module is absent. The contract types live here, in the published
|
|
9
|
+
* package, because both sides must agree on them while neither may import the
|
|
10
|
+
* other. Every provider is offered by a module's service: the app offers none
|
|
11
|
+
* itself.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
/** Key under `FeatureService.providers` for the CloudSync backup source. */
|
|
@@ -46,3 +48,306 @@ export interface CloudSyncBackupProvider {
|
|
|
46
48
|
/** Decrypted plaintext stream of one blob. */
|
|
47
49
|
openBlob(shareId: number, hash: string): Promise<AsyncIterable<Uint8Array>>;
|
|
48
50
|
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Key of the database access the Backup module consumes: a logical dump goes
|
|
54
|
+
* through the SAME access as the monitoring (SSH tunnel or SOCKS proxy
|
|
55
|
+
* included), and only the Databases feature knows how to decrypt a connection.
|
|
56
|
+
*/
|
|
57
|
+
export const DATABASE_BACKUP_PROVIDER = 'database.backup' as const;
|
|
58
|
+
|
|
59
|
+
/** A database of the workspace, as the source picker lists it. */
|
|
60
|
+
export interface DatabaseBackupCandidate {
|
|
61
|
+
id: number;
|
|
62
|
+
name: string;
|
|
63
|
+
engine: 'mysql' | 'postgres';
|
|
64
|
+
host: string;
|
|
65
|
+
database: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* An OPEN access to a database: what a dump tool needs, reachable from the
|
|
70
|
+
* server (the tunnel's local listener when there is one). `close()` releases
|
|
71
|
+
* the tunnel; call it whatever happens, a forgotten tunnel keeps an SSH
|
|
72
|
+
* session and a listener alive.
|
|
73
|
+
*/
|
|
74
|
+
export interface DatabaseBackupAccess {
|
|
75
|
+
engine: 'mysql' | 'postgres';
|
|
76
|
+
host: string;
|
|
77
|
+
port: number;
|
|
78
|
+
database: string;
|
|
79
|
+
username: string;
|
|
80
|
+
password: string | null;
|
|
81
|
+
close(): Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface DatabaseBackupProvider {
|
|
85
|
+
/** The databases of the workspace, its own only (a projection is not a source). */
|
|
86
|
+
listDatabases(workspaceId: number): Promise<readonly DatabaseBackupCandidate[]>;
|
|
87
|
+
findDatabase(databaseId: number, workspaceId: number): Promise<DatabaseBackupCandidate | null>;
|
|
88
|
+
/** null when the database is unknown to this workspace. */
|
|
89
|
+
openAccess(databaseId: number, workspaceId: number): Promise<DatabaseBackupAccess | null>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Key under `FeatureService.providers` for the Databases items the Projects
|
|
94
|
+
* feature links to: same shape and reason as `UPTIME_ITEMS_PROVIDER`
|
|
95
|
+
* (`project_database_links` is Projects' table).
|
|
96
|
+
*/
|
|
97
|
+
export const DATABASE_ITEMS_PROVIDER = 'database.items' as const;
|
|
98
|
+
|
|
99
|
+
export interface DatabaseItemsProvider {
|
|
100
|
+
/** Does this database live in this workspace? Its home only, never a projection. */
|
|
101
|
+
exists(databaseId: number, workspaceId: number): Promise<boolean>;
|
|
102
|
+
/**
|
|
103
|
+
* The item's display name under the OPEN cipher of `workspaceId`, its home;
|
|
104
|
+
* `null` when it is gone or unreadable. What a window onto a projected
|
|
105
|
+
* project shows for a link it cannot open: a name, never an id.
|
|
106
|
+
*/
|
|
107
|
+
labelOf(databaseId: number, workspaceId: number): Promise<string | null>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Key under `FeatureService.providers` for the workspace's outgoing mail: the
|
|
112
|
+
* transport the app's notification channels of kind `email` send through.
|
|
113
|
+
* Offered by the Mail module; absent, an email channel cannot be readied and
|
|
114
|
+
* the settings screen says so. Senders are the module's OPEN-tier, enabled
|
|
115
|
+
* accounts: a guarded mailbox needs a session unlock no background job has.
|
|
116
|
+
*/
|
|
117
|
+
export const MAIL_TRANSPORT_PROVIDER = 'mail.transport' as const;
|
|
118
|
+
|
|
119
|
+
/** A mailbox able to send without anyone unlocking anything. */
|
|
120
|
+
export interface MailSender {
|
|
121
|
+
id: number;
|
|
122
|
+
label: string;
|
|
123
|
+
address: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface MailTransportProvider {
|
|
127
|
+
/** The workspace's ready senders (open tier, enabled). */
|
|
128
|
+
listSenders(workspaceId: number): Promise<readonly MailSender[]>;
|
|
129
|
+
/** Is this account a ready sender of this workspace right now? */
|
|
130
|
+
isReady(accountId: number, workspaceId: number): Promise<boolean>;
|
|
131
|
+
/**
|
|
132
|
+
* Sends one plain-text message from this account. Resolves `true` when the
|
|
133
|
+
* provider accepted it; `false` (never a throw) when the account is not a
|
|
134
|
+
* ready sender or the send failed, the failure logged by the module.
|
|
135
|
+
*/
|
|
136
|
+
send(
|
|
137
|
+
accountId: number,
|
|
138
|
+
workspaceId: number,
|
|
139
|
+
message: { to: string; subject: string; text: string }
|
|
140
|
+
): Promise<boolean>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Key under `FeatureClient.providers` for the Mail pieces the app's settings
|
|
145
|
+
* shell composes: the ready senders an email channel picks from, and the
|
|
146
|
+
* feature's own account dialog (the "+" of the channel form). The contract
|
|
147
|
+
* types live in `@deveye/types/sdk/client`.
|
|
148
|
+
*/
|
|
149
|
+
export const MAIL_CLIENT_PROVIDER = 'mail.client' as const;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Key under `FeatureService.providers` for the Audience sites the app's
|
|
153
|
+
* Projects feature links to: same shape and same reason as
|
|
154
|
+
* `UPTIME_ITEMS_PROVIDER` (`project_audience_links` is Projects' table).
|
|
155
|
+
*/
|
|
156
|
+
export const AUDIENCE_ITEMS_PROVIDER = 'audience.items' as const;
|
|
157
|
+
|
|
158
|
+
export interface AudienceItemsProvider {
|
|
159
|
+
/** Does this site live in this workspace? Its home only, never a projection. */
|
|
160
|
+
exists(siteId: number, workspaceId: number): Promise<boolean>;
|
|
161
|
+
/**
|
|
162
|
+
* The item's display name under the OPEN cipher of `workspaceId`, its home;
|
|
163
|
+
* `null` when it is gone or unreadable. What a window onto a projected
|
|
164
|
+
* project shows for a link it cannot open: a name, never an id.
|
|
165
|
+
*/
|
|
166
|
+
labelOf(siteId: number, workspaceId: number): Promise<string | null>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Key under `FeatureClient.providers` for the Audience pieces the app's
|
|
171
|
+
* Projects screens compose: the list of the workspace's sites, a linked site
|
|
172
|
+
* shown in full inside a project's tab, and the feature's own site dialog.
|
|
173
|
+
* The contract types live in `@deveye/types/sdk/client`.
|
|
174
|
+
*/
|
|
175
|
+
export const AUDIENCE_CLIENT_PROVIDER = 'audience.client' as const;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Key under `FeatureService.providers` for the Git repositories the app's
|
|
179
|
+
* Projects feature links to: same shape and same reason as
|
|
180
|
+
* `UPTIME_ITEMS_PROVIDER` (`project_repo_links` is Projects' table).
|
|
181
|
+
*/
|
|
182
|
+
export const GIT_ITEMS_PROVIDER = 'git.items' as const;
|
|
183
|
+
|
|
184
|
+
export interface GitItemsProvider {
|
|
185
|
+
/** Does this repository live in this workspace? Its home only, never a projection. */
|
|
186
|
+
exists(repoId: number, workspaceId: number): Promise<boolean>;
|
|
187
|
+
/**
|
|
188
|
+
* The item's display name under the OPEN cipher of `workspaceId`, its home;
|
|
189
|
+
* `null` when it is gone or unreadable. What a window onto a projected
|
|
190
|
+
* project shows for a link it cannot open: a name, never an id.
|
|
191
|
+
*/
|
|
192
|
+
labelOf(repoId: number, workspaceId: number): Promise<string | null>;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Key under `FeatureClient.providers` for the Git pieces the app's Projects
|
|
197
|
+
* screens compose: the list of the workspace's repositories, a linked
|
|
198
|
+
* repository shown in full inside a project's tab, and the feature's own
|
|
199
|
+
* repository dialog. The contract types live in `@deveye/types/sdk/client`.
|
|
200
|
+
*/
|
|
201
|
+
export const GIT_CLIENT_PROVIDER = 'git.client' as const;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Key under `FeatureService.providers` for the Deploy targets the app's
|
|
205
|
+
* Projects feature links to: same shape and same reason as
|
|
206
|
+
* `UPTIME_ITEMS_PROVIDER` (`project_deploy_links` is Projects' table).
|
|
207
|
+
*/
|
|
208
|
+
export const DEPLOY_ITEMS_PROVIDER = 'deploy.items' as const;
|
|
209
|
+
|
|
210
|
+
export interface DeployItemsProvider {
|
|
211
|
+
/** Does this target live in this workspace? Its home only, never a projection. */
|
|
212
|
+
exists(targetId: number, workspaceId: number): Promise<boolean>;
|
|
213
|
+
/**
|
|
214
|
+
* The item's display name under the OPEN cipher of `workspaceId`, its home;
|
|
215
|
+
* `null` when it is gone or unreadable. What a window onto a projected
|
|
216
|
+
* project shows for a link it cannot open: a name, never an id.
|
|
217
|
+
*/
|
|
218
|
+
labelOf(targetId: number, workspaceId: number): Promise<string | null>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Key under `FeatureClient.providers` for the Deploy pieces the app's
|
|
223
|
+
* Projects screens compose: the list of the workspace's targets, a linked
|
|
224
|
+
* target shown in full inside a project's tab, and the feature's own target
|
|
225
|
+
* dialog. The contract types live in `@deveye/types/sdk/client`.
|
|
226
|
+
*/
|
|
227
|
+
export const DEPLOY_CLIENT_PROVIDER = 'deploy.client' as const;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Key under `FeatureClient.providers` for the Databases pieces the app's
|
|
231
|
+
* Projects screens compose: the list of the workspace's databases, a linked
|
|
232
|
+
* database shown in full inside a project's tab, and the feature's own
|
|
233
|
+
* database dialog (declaring a database from a project goes through the real
|
|
234
|
+
* form, never a reduced copy). The contract types live in
|
|
235
|
+
* `@deveye/types/sdk/client` (they are React components).
|
|
236
|
+
*/
|
|
237
|
+
export const DATABASE_CLIENT_PROVIDER = 'database.client' as const;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Key of what Projects knows about the items of OTHER features: the projects
|
|
241
|
+
* of the workspace that link them (a module's list shows how many projects use
|
|
242
|
+
* each item, its detail lists them by title), without reading Projects'
|
|
243
|
+
* tables. Keyed by the linked feature's id so every linkable feature reads the
|
|
244
|
+
* same contract.
|
|
245
|
+
*/
|
|
246
|
+
export const PROJECTS_USAGE_PROVIDER = 'projects.usage' as const;
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* A project that links an item. Open tier only: a guarded project cannot link
|
|
250
|
+
* a workspace item (its link row is plain, the item lives at the open tier),
|
|
251
|
+
* so every title here is readable without a session.
|
|
252
|
+
*/
|
|
253
|
+
export interface ProjectUsage {
|
|
254
|
+
projectId: number;
|
|
255
|
+
title: string;
|
|
256
|
+
status: 'draft' | 'active' | 'paused' | 'done';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface ProjectsUsageProvider {
|
|
260
|
+
/** The workspace's projects linking this item of this feature, in Projects' display order. */
|
|
261
|
+
usageOf(feature: string, itemId: number, workspaceId: number): Promise<readonly ProjectUsage[]>;
|
|
262
|
+
/** How many projects of the workspace link each item of this feature (absent = zero). */
|
|
263
|
+
countByItem(feature: string, workspaceId: number): Promise<ReadonlyMap<number, number>>;
|
|
264
|
+
/**
|
|
265
|
+
* Writes one line in a project's timeline (a deployment triggered from a
|
|
266
|
+
* project's tab). Open tier only: an event aimed at a guarded project is
|
|
267
|
+
* dropped silently. Rejects when the write fails; a lost timeline line
|
|
268
|
+
* never turns a deployment into an error.
|
|
269
|
+
*/
|
|
270
|
+
recordEvent(
|
|
271
|
+
projectId: number,
|
|
272
|
+
workspaceId: number,
|
|
273
|
+
event: { kind: string; label: string; actorUserId: number | null }
|
|
274
|
+
): Promise<void>;
|
|
275
|
+
/**
|
|
276
|
+
* Reports a version on the projects linking this item that asked to
|
|
277
|
+
* follow it (Projects' `versionSource`, `'github_release'` for a git
|
|
278
|
+
* repository): the field then belongs to the item, and the project's
|
|
279
|
+
* screen shows it read-only. All linked projects of the workspace, open
|
|
280
|
+
* tier only; the others are left untouched.
|
|
281
|
+
*/
|
|
282
|
+
applyVersion(
|
|
283
|
+
feature: string,
|
|
284
|
+
itemId: number,
|
|
285
|
+
workspaceId: number,
|
|
286
|
+
version: string
|
|
287
|
+
): Promise<void>;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Key under `FeatureService.providers` for the Uptime items the app's Projects
|
|
292
|
+
* feature links to. Projects stores only identifiers; before linking one it
|
|
293
|
+
* asks the module whether the service exists in the workspace, so a foreign
|
|
294
|
+
* id can neither be linked nor leak its existence.
|
|
295
|
+
*/
|
|
296
|
+
export const UPTIME_ITEMS_PROVIDER = 'uptime.items' as const;
|
|
297
|
+
|
|
298
|
+
export interface UptimeItemsProvider {
|
|
299
|
+
/** Does this service live in this workspace? Its home only, never a projection. */
|
|
300
|
+
exists(serviceId: number, workspaceId: number): Promise<boolean>;
|
|
301
|
+
/**
|
|
302
|
+
* The item's display name under the OPEN cipher of `workspaceId`, its home;
|
|
303
|
+
* `null` when it is gone or unreadable. What a window onto a projected
|
|
304
|
+
* project shows for a link it cannot open: a name, never an id.
|
|
305
|
+
*/
|
|
306
|
+
labelOf(serviceId: number, workspaceId: number): Promise<string | null>;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Key under `FeatureClient.providers` for the Uptime pieces the app's Projects
|
|
311
|
+
* screens compose: the availability strip and ratios of a linked service, and
|
|
312
|
+
* the feature's own service dialog (declaring a service from a project goes
|
|
313
|
+
* through the real form, never a reduced copy). The contract types live in
|
|
314
|
+
* `@deveye/types/sdk/client` (they are React components).
|
|
315
|
+
*/
|
|
316
|
+
export const UPTIME_CLIENT_PROVIDER = 'uptime.client' as const;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The client twin of the Devices module (`DevicesClientProvider`,
|
|
320
|
+
* `sdk/client.ts`): the workspace's devices as a store, one device's panel
|
|
321
|
+
* and tile. The app's home and topbar compose them; without the module they
|
|
322
|
+
* render nothing device-related.
|
|
323
|
+
*/
|
|
324
|
+
export const DEVICES_CLIENT_PROVIDER = 'devices.client' as const;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Key under `FeatureService.providers` for what Sentinel contributes to the
|
|
328
|
+
* collection config the app pushes to an agent (`agent.config`): whether the
|
|
329
|
+
* security probes run, and at which cadence. Absent module: the app pushes the
|
|
330
|
+
* probes off.
|
|
331
|
+
*/
|
|
332
|
+
export const SENTINEL_AGENT_CONFIG_PROVIDER = 'sentinel.agentConfig' as const;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Cadence du manifeste de persistance quand rien n'est réglé, en minutes.
|
|
336
|
+
* Vit ici et non dans le domaine du module parce que l'app l'applique
|
|
337
|
+
* elle-même : sans module installé, ou sans ligne de config pour l'appareil,
|
|
338
|
+
* la config poussée à l'agent porte ce défaut (sondes éteintes).
|
|
339
|
+
*/
|
|
340
|
+
export const DEFAULT_SENTINEL_INTEGRITY_MINUTES = 360;
|
|
341
|
+
|
|
342
|
+
export interface SentinelAgentConfig {
|
|
343
|
+
enabled: boolean;
|
|
344
|
+
/** Cadence of the persistence manifest, in minutes. */
|
|
345
|
+
integrityMinutes: number;
|
|
346
|
+
/** Whether the agent reads the authentication journal. */
|
|
347
|
+
authEvents: boolean;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export interface SentinelAgentConfigProvider {
|
|
351
|
+
/** The device's contribution, or null when Sentinel knows nothing about it (probes off). */
|
|
352
|
+
configFor(deviceId: string): Promise<SentinelAgentConfig | null>;
|
|
353
|
+
}
|