ldap-rest 1.1.0 → 1.2.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 (97) hide show
  1. package/bin/index.mjs +8 -7
  2. package/dist/abstract/ldapFlat.js +1 -1
  3. package/dist/abstract/plugin.js +1 -1
  4. package/dist/abstract/twakePlugin.js +1 -1
  5. package/dist/bin/index.js +1 -1
  6. package/dist/config/args.js +1 -1
  7. package/dist/config/schema.js +1 -1
  8. package/dist/lib/changeContext.js +1 -1
  9. package/dist/lib/entitySchema/validator.js +1 -0
  10. package/dist/lib/expressFormatedResponses.js +1 -1
  11. package/dist/lib/health.js +1 -0
  12. package/dist/lib/instanceProviders.js +1 -1
  13. package/dist/lib/ldapActions.js +1 -1
  14. package/dist/lib/metrics.js +1 -0
  15. package/dist/lib/requestContext.js +1 -1
  16. package/dist/lib/shutdown.js +1 -0
  17. package/dist/lib/staticFiles.js +1 -0
  18. package/dist/lib/storage/store.js +1 -1
  19. package/dist/lib/utils.js +1 -1
  20. package/dist/logger/winston.js +1 -1
  21. package/dist/plugins/auth/crowdsec.js +1 -1
  22. package/dist/plugins/auth/openidconnect.js +1 -1
  23. package/dist/plugins/demo/helloworld.js +1 -1
  24. package/dist/plugins/ldap/bulkImport.js +1 -1
  25. package/dist/plugins/ldap/flatGeneric.js +1 -1
  26. package/dist/plugins/ldap/groups.js +1 -1
  27. package/dist/plugins/ldap/organizations.js +1 -1
  28. package/dist/plugins/metrics.js +1 -0
  29. package/dist/plugins/rabbitmq.js +1 -1
  30. package/dist/plugins/scim/scim.js +1 -1
  31. package/dist/plugins/static.js +1 -1
  32. package/dist/plugins/storage/file.js +1 -1
  33. package/dist/plugins/storage/postgres.js +1 -1
  34. package/dist/plugins/storage/valkey.js +1 -1
  35. package/dist/plugins/storage.js +1 -1
  36. package/dist/plugins/twake/calendar.js +1 -1
  37. package/dist/plugins/twake/clouderyProvision.js +1 -1
  38. package/dist/plugins/twake/cozyProvision.js +1 -1
  39. package/dist/plugins/twake/drive.js +1 -1
  40. package/dist/plugins/twake/james.js +1 -1
  41. package/dist/plugins/twake/migrationProxy.js +1 -0
  42. package/dist/plugins/weblogs.js +1 -1
  43. package/dist/src/abstract/ldapFlat.d.ts +3 -13
  44. package/dist/src/abstract/plugin.d.ts +20 -0
  45. package/dist/src/abstract/twakePlugin.d.ts +23 -1
  46. package/dist/src/bin/index.d.ts +46 -1
  47. package/dist/src/config/args.d.ts +12 -1
  48. package/dist/src/lib/changeContext.d.ts +11 -0
  49. package/dist/src/lib/entitySchema/validator.d.ts +64 -0
  50. package/dist/src/lib/expressFormatedResponses.d.ts +19 -0
  51. package/dist/src/lib/health.d.ts +63 -0
  52. package/dist/src/lib/instanceProviders.d.ts +4 -0
  53. package/dist/src/lib/ldapActions.d.ts +28 -0
  54. package/dist/src/lib/metrics.d.ts +44 -0
  55. package/dist/src/lib/requestContext.d.ts +40 -0
  56. package/dist/src/lib/shutdown.d.ts +20 -0
  57. package/dist/src/lib/staticFiles.d.ts +33 -0
  58. package/dist/src/lib/storage/store.d.ts +8 -0
  59. package/dist/src/lib/utils.d.ts +9 -0
  60. package/dist/src/logger/winston.d.ts +4 -0
  61. package/dist/src/plugins/ldap/groups.d.ts +18 -3
  62. package/dist/src/plugins/ldap/organizations.d.ts +3 -5
  63. package/dist/src/plugins/metrics.d.ts +17 -0
  64. package/dist/src/plugins/rabbitmq.d.ts +37 -1
  65. package/dist/src/plugins/static.d.ts +12 -0
  66. package/dist/src/plugins/storage/file.d.ts +4 -0
  67. package/dist/src/plugins/storage/postgres.d.ts +7 -14
  68. package/dist/src/plugins/storage/valkey.d.ts +2 -15
  69. package/dist/src/plugins/storage.d.ts +8 -0
  70. package/dist/src/plugins/twake/drive.d.ts +8 -0
  71. package/dist/src/plugins/twake/james.d.ts +2 -0
  72. package/dist/src/plugins/twake/migrationProxy.d.ts +71 -0
  73. package/docs/client-development/api/reference.md +5 -9
  74. package/docs/client-development/api/rest-api.md +4 -10
  75. package/docs/client-development/schemas/README.md +60 -21
  76. package/docs/plugin-development/README.md +15 -13
  77. package/docs/plugin-development/dependencies.md +8 -0
  78. package/docs/plugin-development/hooks.md +1 -1
  79. package/docs/usage/README.md +1 -0
  80. package/docs/usage/configuration.md +49 -11
  81. package/docs/usage/operations.md +193 -0
  82. package/docs/usage/plugins/README.md +2 -0
  83. package/docs/usage/plugins/auth/authz-per-branch.md +2 -1
  84. package/docs/usage/plugins/integrations/README.md +8 -4
  85. package/docs/usage/plugins/integrations/mail-migration.md +102 -0
  86. package/docs/usage/plugins/integrations/scim.md +4 -2
  87. package/docs/usage/plugins/ldap/bulk-import.md +3 -1
  88. package/docs/usage/plugins/ldap/flat-generic.md +5 -2
  89. package/docs/usage/plugins/ldap/on-change.md +2 -1
  90. package/docs/usage/plugins/ldap/organizations.md +7 -7
  91. package/docs/usage/plugins/utilities/metrics.md +21 -0
  92. package/docs/usage/plugins/utilities/static.md +16 -8
  93. package/docs/usage/plugins/utilities/storage.md +7 -0
  94. package/docs/usage/plugins/utilities/weblogs.md +51 -30
  95. package/docs/usage/upgrading.md +71 -0
  96. package/package.json +11 -2
  97. package/static/schemas/example/README.md +1 -0
@@ -0,0 +1,64 @@
1
+ import type { AttributesList, AttributeValue, ModifyRequest } from '../ldapActions';
2
+ import type { Schema } from '../../config/schema';
3
+ export interface EntityValidatorDeps {
4
+ /**
5
+ * Tells whether a DN names an entry. Asked as the server: a reference to
6
+ * an entry the caller cannot read is still a valid reference.
7
+ */
8
+ entryExists: (dn: string) => Promise<boolean>;
9
+ /** Asked whether a `generated` attribute will be filled after validation */
10
+ loadedPlugins: Record<string, unknown>;
11
+ /** Names the entity in the messages */
12
+ singularName: string;
13
+ }
14
+ /**
15
+ * Enforces an entity schema on what a client sends: the entry of a creation
16
+ * and the parts of a change. Every refusal is a `BadRequestError`.
17
+ *
18
+ * See `docs/client-development/schemas/README.md` for the rules.
19
+ */
20
+ export declare class EntityValidator {
21
+ private readonly schema;
22
+ private readonly deps;
23
+ constructor(schema: Schema | undefined, deps: EntityValidatorDeps);
24
+ /**
25
+ * Check a new entry, then convert it in place for the directory.
26
+ *
27
+ * @param entry entry as the client sent it, `objectClass` included
28
+ */
29
+ validateNew(entry: AttributesList): Promise<void>;
30
+ /**
31
+ * Check the parts of a change, then convert them in place. No value under
32
+ * a name is the empty list for the directory layer, which clears the
33
+ * attribute in a `replace` and refuses it in an `add`.
34
+ *
35
+ * @param changes change as the client sent it
36
+ */
37
+ validateChanges(changes: ModifyRequest): Promise<void>;
38
+ /**
39
+ * Check one value against its attribute's rules: its shape, then the
40
+ * entries a pointer names, then the pattern.
41
+ *
42
+ * @param field attribute name, as the client wrote it
43
+ * @param value value sent; no value clears the attribute
44
+ * @returns the value in the shape of the type: one value for a
45
+ * single-valued attribute, a list for an `array`
46
+ */
47
+ validateValue(field: string, value: AttributeValue | null | undefined): Promise<AttributeValue | null | undefined>;
48
+ /**
49
+ * The schema's definition of an attribute, under any case.
50
+ *
51
+ * @throws BadRequestError for a name a strict schema does not declare
52
+ */
53
+ private attribute;
54
+ }
55
+ /**
56
+ * Write every fixed attribute with a default as the schema says, whatever
57
+ * the request carried. `objectClass` is left to the entity's configuration.
58
+ * Run on the entry an authorization judges, so that it judges the entry
59
+ * written.
60
+ *
61
+ * @param schema entity schema, nothing to apply without one
62
+ * @param entry entry about to be created, changed in place
63
+ */
64
+ export declare function applyFixedDefaults(schema: Schema | undefined, entry: AttributesList): void;
@@ -13,6 +13,25 @@ import type winston from 'winston';
13
13
  export declare const setLogger: (logger: winston.Logger | undefined) => void;
14
14
  /** @returns the installed logger, or undefined before any `DM` exists */
15
15
  export declare const getLogger: () => winston.Logger | undefined;
16
+ /**
17
+ * Whether `serverError` names the failure in its answer: the `--debug` of
18
+ * the server, installed as its logger is.
19
+ *
20
+ * @param debug true to name the failure in the response
21
+ */
22
+ export declare const setDebug: (debug: boolean) => void;
23
+ /**
24
+ * The body of a 5xx answer, for the error middleware of `DM` and
25
+ * `serverError` alike. The failure is for the logs: the message names it
26
+ * only under `--debug`.
27
+ *
28
+ * @param err what failed
29
+ * @param debug whether to name the failure; `serverError`'s setting by default
30
+ */
31
+ export declare const serverErrorBody: (err: unknown, debug?: boolean) => {
32
+ error: string;
33
+ message: string;
34
+ };
16
35
  export declare const tryMethod: (res: Response, method: Function, ...args: unknown[]) => Promise<void>;
17
36
  export declare const tryMethodData: (res: Response, method: Function, ...args: unknown[]) => Promise<void>;
18
37
  export declare const ok: (res: Response, data?: object) => void;
@@ -0,0 +1,63 @@
1
+ import type { Express } from 'express';
2
+ import type { MaybePromise } from '../hooks';
3
+ /** What a plugin answers about the resources it depends on */
4
+ export type HealthStatus = 'ok' | 'degraded';
5
+ /** The paths of the two probes, served before authentication */
6
+ export declare const HEALTH_PATHS: readonly ["/healthz", "/readyz"];
7
+ /** How long one readiness answer serves */
8
+ export declare const PROBE_TTL_MS = 5000;
9
+ /**
10
+ * How long a readiness answer may take, the directory and every plugin
11
+ * asked at once: under the 3 s a kubelet waits, so that a hanging resource
12
+ * is reported, not timed out
13
+ */
14
+ export declare const PROBE_TIMEOUT_MS = 2000;
15
+ /** What the readiness probe reads of the server */
16
+ export interface Probed {
17
+ /** Every plugin loaded and the composition checked */
18
+ readonly composed: boolean;
19
+ /** Told to stop: what is in flight finishes, nothing new is taken */
20
+ readonly stopping: boolean;
21
+ readonly ldap: {
22
+ ping: (timeoutMs: number) => Promise<void>;
23
+ };
24
+ readonly loadedPlugins: {
25
+ [name: string]: {
26
+ health?: () => MaybePromise<HealthStatus>;
27
+ };
28
+ };
29
+ }
30
+ export interface Readiness {
31
+ /** `starting`, `stopping` and `unavailable` answer 503, the two others 200 */
32
+ status: 'starting' | 'stopping' | 'ok' | 'degraded' | 'unavailable';
33
+ ldap?: 'ok' | 'failed';
34
+ /** The plugins implementing `health()`; `failed` when it threw or hung */
35
+ plugins?: {
36
+ [name: string]: HealthStatus | 'failed';
37
+ };
38
+ }
39
+ /**
40
+ * Tell whether the server can serve: loaded, with a directory that answers.
41
+ * A plugin's resource being down degrades the answer without failing it,
42
+ * so that one broken outbound service does not take every replica out of
43
+ * rotation.
44
+ *
45
+ * One answer serves `PROBE_TTL_MS`, and the probes arriving while it is
46
+ * made share it: a probe every few seconds from every replica, or a flood
47
+ * on an unauthenticated route, must not become a load on the directory nor
48
+ * on what the plugins ask.
49
+ *
50
+ * @param server what is probed
51
+ * @param now the moment, for the answer's age
52
+ * @param timeoutMs how long the whole answer may take
53
+ */
54
+ export declare function readiness(server: Probed, now?: number, timeoutMs?: number): Promise<Readiness>;
55
+ /**
56
+ * Serve `GET /healthz`, which answers as long as the process serves, and
57
+ * `GET /readyz`, which answers 200 when `readiness` says the server can
58
+ * serve. Both are outside the API prefix and carry no identity.
59
+ *
60
+ * @param app the application, before its authentication
61
+ * @param server what the readiness probe reads
62
+ */
63
+ export declare function mountHealth(app: Express, server: Probed): void;
@@ -69,6 +69,8 @@ export interface InstanceProvider {
69
69
  declare abstract class HttpProvider {
70
70
  protected readonly domain: string;
71
71
  protected readonly timeout: number;
72
+ /** The `target` label of this provider's calls in the outbound metrics */
73
+ protected abstract readonly target: string;
72
74
  constructor(domain: string, timeout: number);
73
75
  address(request: InstanceRequest): string;
74
76
  protected call(url: string, init?: RequestInit): Promise<Response>;
@@ -79,6 +81,7 @@ export declare class ClouderyProvider extends HttpProvider implements InstancePr
79
81
  private readonly url;
80
82
  private readonly token;
81
83
  private readonly offer;
84
+ protected readonly target = "cloudery";
82
85
  constructor(url: string, token: string, domain: string, offer: string, timeout: number);
83
86
  private headers;
84
87
  create(request: InstanceRequest): Promise<undefined>;
@@ -97,6 +100,7 @@ export declare class CozyStackProvider extends HttpProvider implements InstanceP
97
100
  private readonly url;
98
101
  private readonly context;
99
102
  private readonly apps;
103
+ protected readonly target = "cozy-stack";
100
104
  private readonly auth;
101
105
  constructor(url: string, user: string, passphrase: string, domain: string, context: string, apps: string, timeout: number);
102
106
  private headers;
@@ -126,11 +126,39 @@ declare class ldapActions {
126
126
  * Uses a flag to prevent concurrent cleanup operations
127
127
  */
128
128
  private cleanupExpiredConnections;
129
+ /**
130
+ * Ask the directory for its root DSE on a connection of its own, bound
131
+ * as the server and closed after: the readiness probe. Neither the pool
132
+ * nor the search cache nor the hooks answer for a directory that does
133
+ * not, and a directory that stops answering holds nothing of the pool.
134
+ * The URLs are tried as `createConnection` tries them.
135
+ *
136
+ * @param timeoutMs how long the connection, the bind and the search may
137
+ * each take; the client closes the socket when it passes
138
+ * @throws when no URL answers in time
139
+ */
140
+ ping(timeoutMs: number): Promise<void>;
141
+ /** Set by `close`: no connection is handed out any more */
142
+ private closed;
143
+ /**
144
+ * Close every pooled connection, the ones in use included: an operation
145
+ * still running on one is rejected by the client, and a waiter is refused
146
+ * rather than kept.
147
+ */
148
+ close(): Promise<void>;
129
149
  /**
130
150
  * Acquire a connection from the pool or create a new one
131
151
  * Optimized for O(1) lookup using separate available connections queue
132
152
  */
133
153
  private acquireConnection;
154
+ /** The pool as it is, for the metrics */
155
+ poolStats(): {
156
+ busy: number;
157
+ idle: number;
158
+ waiting: number;
159
+ max: number;
160
+ };
161
+ private takeConnection;
134
162
  /**
135
163
  * Release a connection back to the pool
136
164
  * If there are waiting requests, hand off the connection directly
@@ -0,0 +1,44 @@
1
+ /**
2
+ * What the server measures of itself, for `core/metrics` to expose; see
3
+ * docs/usage/operations.md#metrics
4
+ * @module lib/metrics
5
+ */
6
+ import type { Request } from 'express';
7
+ import { Counter, Histogram, Registry } from 'prom-client';
8
+ /** The instruments below register here; `core/metrics` adds its own */
9
+ export declare const registry: Registry<"text/plain; version=0.0.4; charset=utf-8">;
10
+ export declare const httpRequestDuration: Histogram<"status" | "method" | "route">;
11
+ export type LdapOperation = 'bind' | 'search' | 'add' | 'modify' | 'rename' | 'delete';
12
+ export declare const ldapOperationDuration: Histogram<"operation" | "result">;
13
+ export declare const ldapPoolWait: Histogram<string>;
14
+ export declare const ldapSearchCache: Counter<"result">;
15
+ export declare const hookFailures: Counter<"plugin" | "hook">;
16
+ export declare const outboundRequestDuration: Histogram<"status" | "target">;
17
+ export declare const unhandledRejections: Counter<string>;
18
+ /**
19
+ * The `result` label of a directory call that threw: a refusal is the
20
+ * directory answering, by its result code (32 for an entry that is not
21
+ * there); `error` is for not answering.
22
+ */
23
+ export declare const ldapResult: (err: unknown) => string;
24
+ /** @returns the seconds since `started`, a `process.hrtime.bigint()` */
25
+ export declare const secondsSince: (started: bigint) => number;
26
+ /**
27
+ * Time a directory call into the operation histogram, whatever becomes of
28
+ * it.
29
+ */
30
+ export declare function timedLdap<T>(operation: LdapOperation, call: () => Promise<T>): Promise<T>;
31
+ /**
32
+ * Time a call to another service into the outbound histogram, to its
33
+ * answer's headers: the body is read by the caller, at its own pace.
34
+ */
35
+ export declare function timedOutbound<T extends {
36
+ status: number;
37
+ }>(target: string, call: () => Promise<T>): Promise<T>;
38
+ /**
39
+ * The template of the route that answered, never the URL: a label per URL
40
+ * would grow without bound. See docs/usage/operations.md#metrics.
41
+ *
42
+ * @returns the template, `none` when no route matched
43
+ */
44
+ export declare const routeTemplate: (req: Request) => string;
@@ -21,3 +21,43 @@ export declare function outsideRequest<T>(fn: () => T): T;
21
21
  * @returns the request, or undefined outside any, or as the server
22
22
  */
23
23
  export declare function currentRequest(): Request | undefined;
24
+ /** What a log line carries beside what it says */
25
+ export interface LogContext {
26
+ /** The request being served */
27
+ requestId?: string;
28
+ /** The plugin whose hook is running */
29
+ plugin?: string;
30
+ /** That hook */
31
+ op?: string;
32
+ }
33
+ /**
34
+ * Run something that outlives the request being served, a connection
35
+ * opened for the pool or a client kept for the process: its operations are
36
+ * judged by nobody, and what it logs later is nobody's request.
37
+ *
38
+ * @param fn what to run
39
+ * @returns what it returns
40
+ */
41
+ export declare function outsideAnyRequest<T>(fn: () => T): T;
42
+ /**
43
+ * Serve a request with its log context: what runs inside, synchronously or
44
+ * not, logs with it.
45
+ *
46
+ * @param context what the lines carry
47
+ * @param fn what serves the request
48
+ * @returns what `fn` returns
49
+ */
50
+ export declare function runWithLogContext<T>(context: LogContext, fn: () => T): T;
51
+ /**
52
+ * Run a hook: what it logs names its plugin and itself, under the request
53
+ * being served; a hook no plugin registered names none, even inside
54
+ * another's.
55
+ *
56
+ * @param plugin the plugin's instance name, none for an unregistered hook
57
+ * @param op the hook
58
+ * @param fn the hook's run
59
+ * @returns what `fn` returns
60
+ */
61
+ export declare function runAsPlugin<T>(plugin: string | undefined, op: string | undefined, fn: () => T): T;
62
+ /** The log context of what is running, if any: request, plugin, hook */
63
+ export declare function currentLogContext(): LogContext | undefined;
@@ -0,0 +1,20 @@
1
+ export type ShutdownCallback = () => void | Promise<void>;
2
+ /**
3
+ * How long the shutdown callbacks get before the signal is raised again.
4
+ * The binary sets it from `--shutdown-timeout`.
5
+ */
6
+ export declare const shutdownTimeout: {
7
+ ms: number;
8
+ };
9
+ /**
10
+ * Run a callback when the process is told to stop. The first registration
11
+ * installs the signal listeners; a callback registered while the shutdown
12
+ * runs is awaited with the others.
13
+ *
14
+ * Once every callback has settled, or the timeout has passed, the signal is
15
+ * raised again for its default action, unless another listener is there to
16
+ * decide.
17
+ *
18
+ * @param cb what to run, awaited when it answers a promise
19
+ */
20
+ export declare function registerProcessShutdown(cb: ShutdownCallback): void;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Where the static plugins serve a schema file, for another plugin to
3
+ * announce it
4
+ * @module lib/staticFiles
5
+ */
6
+ import type { DM } from '../bin';
7
+ /** A static plugin, as another plugin asks it where it serves a file */
8
+ export interface StaticFiles {
9
+ schemaUrlOf(file: string): string | undefined;
10
+ servedPath(): string;
11
+ }
12
+ /** Told by its methods, not its class: another copy of ldap-rest has its own */
13
+ export declare const isStaticFiles: (plugin: unknown) => plugin is StaticFiles;
14
+ /**
15
+ * The static plugins loaded, whatever their class or name: one loaded under
16
+ * another name, or with a name or a directory of its own, serves files
17
+ * elsewhere or not at all. The one registered as `static` comes first; one
18
+ * whose path an earlier plugin took, lies under it or holds it, is left
19
+ * out, the earlier one's routes answering first.
20
+ *
21
+ * @param server the server whose plugins are asked
22
+ * @returns the plugins, in the order to ask them
23
+ */
24
+ export declare function staticPlugins(server: Pick<DM, 'loadedPlugins'>): StaticFiles[];
25
+ /**
26
+ * URL a static plugin serves a schema file at, merged, asked of
27
+ * {@link staticPlugins} in their order
28
+ *
29
+ * @param server the server whose plugins are asked
30
+ * @param file the file, as the plugin announcing it reads it
31
+ * @returns its URL, or undefined when no static plugin serves it merged
32
+ */
33
+ export declare function staticSchemaUrl(server: Pick<DM, 'loadedPlugins'>, file: string): string | undefined;
@@ -26,6 +26,7 @@
26
26
  * @group Libraries
27
27
  */
28
28
  import type winston from 'winston';
29
+ import type { HealthStatus } from '../health';
29
30
  /** What a backend keeps: a value, and when it stops counting. */
30
31
  export interface StoredRecord {
31
32
  value: string;
@@ -45,6 +46,11 @@ export default abstract class Store {
45
46
  protected abstract drop(key: string): Promise<void>;
46
47
  /** Drop every expired record. Returns how many went. */
47
48
  abstract sweep(): Promise<number>;
49
+ /**
50
+ * Whether the backend answers, asked by the readiness probe; a backend
51
+ * with a resource of its own asks it and throws when it does not answer.
52
+ */
53
+ health(): Promise<HealthStatus>;
48
54
  /**
49
55
  * The key a backend actually sees, namespaced so two consumers cannot
50
56
  * collide.
@@ -72,6 +78,8 @@ export default abstract class Store {
72
78
  * operator reads it, instead of the first request that needs the store.
73
79
  */
74
80
  open(): Promise<void>;
81
+ /** Set by `close`: a backend opens nothing past it */
82
+ protected closed: boolean;
75
83
  /** Stop sweeping and let go of any connection. */
76
84
  close(): Promise<void>;
77
85
  /**
@@ -22,6 +22,15 @@ export declare function getCompiledRegex(pattern: string, flags?: string): RegEx
22
22
  * @returns The escaped string safe for use in RegExp
23
23
  */
24
24
  export declare function escapeRegex(str: string): string;
25
+ /**
26
+ * Run a hook under its plugin's name and its own, for the lines it logs:
27
+ * what `launchHooks` does, for a caller walking the hooks itself.
28
+ *
29
+ * @param hook the registered function
30
+ * @param fn its run
31
+ * @returns what `fn` returns
32
+ */
33
+ export declare const runAsHookOwner: <T>(hook: Function, fn: () => T) => T;
25
34
  export declare const launchHooks: (hooks: Function[] | undefined, ...args: unknown[]) => Promise<void>;
26
35
  /**
27
36
  * Record the plugin a hook function belongs to.
@@ -5,4 +5,8 @@ declare module 'winston' {
5
5
  notice: winston.LeveledLogMethod;
6
6
  }
7
7
  }
8
+ /**
9
+ * @throws when `--logger` names no known logger, or `--logger file` has no
10
+ * file: never a file nobody asked for
11
+ */
8
12
  export declare const buildLogger: (config: Config) => winston.Logger;
@@ -14,6 +14,7 @@ import type { Hooks } from '../../hooks';
14
14
  import type ldapActions from '../../lib/ldapActions';
15
15
  import type { AttributesList, AttributeValue, LdapList, ModifyRequest } from '../../lib/ldapActions';
16
16
  import type { Schema } from '../../config/schema';
17
+ import { EntityValidator } from '../../lib/entitySchema/validator';
17
18
  export interface postAdd {
18
19
  cn?: string;
19
20
  [key: string]: AttributeValue | undefined;
@@ -308,9 +309,23 @@ export default class LdapGroups extends DmPlugin {
308
309
  * @returns nothing, throw if error
309
310
  */
310
311
  validateMembers(dn: string, members: string[], req?: Request): Promise<void>;
311
- validateNewGroup(dn: string, entry: AttributesList): Promise<boolean>;
312
- validateChanges(dn: string, changes: ModifyRequest): Promise<boolean>;
313
- _validateOneChange(field: string, value: AttributeValue | null): Promise<boolean>;
312
+ /**
313
+ * Check a new group's entry, after the `ldapgroupvalidatenew` hooks, and
314
+ * convert it in place.
315
+ *
316
+ * @returns the entry the hooks handed on; `addGroup` writes its
317
+ * attributes over the entry it judged, the members aside
318
+ */
319
+ validateNewGroup(dn: string, entry: AttributesList): Promise<AttributesList>;
320
+ /**
321
+ * Check a change, after the `ldapgroupvalidatechanges` hooks, and convert
322
+ * it in place.
323
+ *
324
+ * @returns the change the hooks handed on
325
+ */
326
+ validateChanges(dn: string, changes: ModifyRequest): Promise<ModifyRequest>;
327
+ /** The schema's rules; the pointer lookups run as the server */
328
+ validator(): EntityValidator;
314
329
  /**
315
330
  * Provide configuration for config API
316
331
  */
@@ -2,7 +2,7 @@ import type { Express, Request, Response } from 'express';
2
2
  import DmPlugin, { type Role } from '../../abstract/plugin';
3
3
  import { DM } from '../../bin';
4
4
  import { Hooks } from '../../hooks';
5
- import { AttributesList, AttributeValue, ModifyRequest } from '../../lib/ldapActions';
5
+ import { AttributesList, ModifyRequest } from '../../lib/ldapActions';
6
6
  import type { Schema } from '../../config/schema';
7
7
  /**
8
8
  * Shared OpenAPI schemas surfaced by this plugin. Picked up by
@@ -231,10 +231,8 @@ export default class LdapOrganizations extends DmPlugin {
231
231
  private linkedEntities;
232
232
  addOrganization(dn: string, entry: AttributesList, req?: Request): Promise<boolean>;
233
233
  modifyOrganization(dn: string, changes: ModifyRequest, req?: Request): Promise<boolean>;
234
- validateNewOrganization(dn: string, entry: AttributesList): Promise<boolean>;
235
- invalidValueMessage(field: string): string;
236
- validateChanges(dn: string, changes: ModifyRequest): Promise<boolean>;
237
- _validateOneChange(field: string, value: AttributeValue | null): Promise<boolean>;
234
+ /** The schema's rules; the pointer lookups run as the server */
235
+ private validator;
238
236
  /**
239
237
  * Tell whether a DN names an existing entry.
240
238
  *
@@ -0,0 +1,17 @@
1
+ import type { Express } from 'express';
2
+ import DmPlugin from '../abstract/plugin';
3
+ import type { DM } from '../bin';
4
+ export default class Metrics extends DmPlugin {
5
+ name: string;
6
+ /** The gauges read at scrape time and the process metrics */
7
+ private readonly own;
8
+ private readonly exposed;
9
+ private listener?;
10
+ constructor(server: DM);
11
+ /** Open the listener; the server does not start on a port it cannot take */
12
+ api(_app: Express): Promise<void>;
13
+ /** @returns the port the metrics are served on, once `api` opened it */
14
+ port(): number | undefined;
15
+ private answer;
16
+ close(): Promise<void>;
17
+ }
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import DmPlugin from '../abstract/plugin';
21
21
  import type { DM } from '../bin';
22
+ import type { HealthStatus } from '../lib/health';
22
23
  /** JSON-serialisable message payload. */
23
24
  export type RabbitMessage = Record<string, unknown>;
24
25
  /** Async handler invoked for each consumed message. */
@@ -46,6 +47,18 @@ interface RabbitClient {
46
47
  subscribe(exchange: string, routingKey: string, queue: string, handler: RabbitMessageHandler, options?: SubscribeOptions): Promise<void>;
47
48
  unsubscribe(queue: string): Promise<void>;
48
49
  close(clearSubscriptions?: boolean): Promise<void>;
50
+ isConnected(): boolean;
51
+ }
52
+ /** The logging contract of `@linagora/rabbitmq-client`: message first, then a context */
53
+ interface ClientLogger {
54
+ debug(message: string, context?: unknown): void;
55
+ info(message: string, context?: unknown): void;
56
+ warn(message: string, context?: unknown): void;
57
+ error(message: string, context?: unknown): void;
58
+ }
59
+ interface RabbitClientOptions {
60
+ url: string;
61
+ logger: ClientLogger;
49
62
  }
50
63
  export default class RabbitMq extends DmPlugin {
51
64
  name: string;
@@ -59,6 +72,13 @@ export default class RabbitMq extends DmPlugin {
59
72
  * the broker is reachable — the connection is still lazy.
60
73
  */
61
74
  isAvailable(): boolean;
75
+ /**
76
+ * Degraded when a broker is configured and no connection to it stands:
77
+ * the state as it is, nothing opened for the probe, which would run the
78
+ * client's connection attempts on every probe while the broker is away.
79
+ * A plugin without a broker has nothing to report.
80
+ */
81
+ health(): HealthStatus;
62
82
  /**
63
83
  * Publish a JSON message to a topic exchange. No-op (returns silently) when
64
84
  * no broker is configured or the client is unavailable.
@@ -89,6 +109,22 @@ export default class RabbitMq extends DmPlugin {
89
109
  * retry policy in `getClient()` can be tested without a live broker.
90
110
  */
91
111
  protected connect(): Promise<RabbitClient>;
92
- private registerShutdown;
112
+ /**
113
+ * Build and initialise a client of the given class: a test hands in a stub
114
+ * to see what the client is built with.
115
+ */
116
+ protected open(Client: new (options: RabbitClientOptions) => unknown): Promise<RabbitClient>;
117
+ /**
118
+ * The client's lines go through the plugin's logger, under its name, so
119
+ * they follow `--logger` and `--log-level`; without one the client writes
120
+ * to the console. An `Error` in the context is written as text: the JSON
121
+ * format keeps only its enumerable fields, which for a channel error is
122
+ * the code without the broker's message.
123
+ */
124
+ private clientLogger;
125
+ /** Set by `close`: a publish of what outlives the drain opens nothing */
126
+ private closed;
127
+ /** Close the broker connection; nothing opens one after */
128
+ close(): Promise<void>;
93
129
  }
94
130
  export {};
@@ -65,6 +65,18 @@ export default class Static extends DmPlugin {
65
65
  * @returns its URL, or undefined when this plugin does not serve it
66
66
  */
67
67
  urlOf(file: string): string | undefined;
68
+ /**
69
+ * The URL its schema routes serve a schema file at, merged with what it
70
+ * extends and its placeholders replaced, for another plugin to announce
71
+ * it: one of the configuration's schema files it serves, or a file its
72
+ * `schemas/` directory reaches, through a link included. One that
73
+ * express.static would send as it stands, outside `schemas/` or not named
74
+ * `.json`, has none.
75
+ *
76
+ * @param file the schema file, as the other plugin reads it
77
+ * @returns its URL, or undefined when its schema routes do not serve it
78
+ */
79
+ schemaUrlOf(file: string): string | undefined;
68
80
  /** The path it serves under, which an earlier plugin may have taken */
69
81
  servedPath(): string;
70
82
  /**
@@ -1,5 +1,6 @@
1
1
  import type winston from 'winston';
2
2
  import Store, { type StoredRecord } from '../../lib/storage/store';
3
+ import type { HealthStatus } from '../../lib/health';
3
4
  export default class FileStore extends Store {
4
5
  name: string;
5
6
  private dir;
@@ -8,6 +9,9 @@ export default class FileStore extends Store {
8
9
  constructor(logger: winston.Logger, dir: string, abandonedAfterSeconds: number);
9
10
  /** The directory is created on demand, readable by this user alone. */
10
11
  ready(): Promise<void>;
12
+ open(): Promise<void>;
13
+ /** A directory this user may write a record in: entered and written */
14
+ health(): Promise<HealthStatus>;
11
15
  private path;
12
16
  /** `<deadline> <key>\n<value>` — the deadline first, so a short read still has it. */
13
17
  private static encode;
@@ -1,19 +1,6 @@
1
- /**
2
- * @module plugins/storage/postgres
3
- *
4
- * A keyed store kept in one PostgreSQL table.
5
- *
6
- * `pg` is an optional dependency, loaded when this backend is chosen: a
7
- * deployment that keeps its records elsewhere never needs it installed.
8
- *
9
- * The key is kept verbatim, split into the consumer's namespace and its own
10
- * key: a `text` column has no reason to hash it the way a DN or a file name
11
- * must, so a `SELECT` says what a record is about.
12
- *
13
- * @group Plugins
14
- */
15
1
  import type winston from 'winston';
16
2
  import Store, { type StoredRecord } from '../../lib/storage/store';
3
+ import { type HealthStatus } from '../../lib/health';
17
4
  type Pg = typeof import('pg');
18
5
  export default class PostgresStore extends Store {
19
6
  name: string;
@@ -26,6 +13,12 @@ export default class PostgresStore extends Store {
26
13
  /** Split out so a test can stand in for a missing package. */
27
14
  protected driver(): Promise<Pg>;
28
15
  open(): Promise<void>;
16
+ /**
17
+ * On a client of its own, closed after, with the budget as its timeouts:
18
+ * a query the pool never got an answer to would hold one of its
19
+ * connections for as long as the socket lives
20
+ */
21
+ health(): Promise<HealthStatus>;
29
22
  close(): Promise<void>;
30
23
  /**
31
24
  * The pool, once the table is known to be there. A failure is not kept:
@@ -1,20 +1,6 @@
1
- /**
2
- * @module plugins/storage/valkey
3
- *
4
- * A keyed store kept in Valkey, or Redis.
5
- *
6
- * `iovalkey` is an optional dependency, loaded when this backend is chosen: a
7
- * deployment that keeps its records elsewhere never needs it installed.
8
- *
9
- * The only backend that expires its own records, so it has nothing to sweep.
10
- * The value still carries its deadline, in the shape the LDAP store uses:
11
- * reads judge expiry by it, as with every other backend, and the TTL only
12
- * reclaims the memory.
13
- *
14
- * @group Plugins
15
- */
16
1
  import type winston from 'winston';
17
2
  import Store, { type StoredRecord } from '../../lib/storage/store';
3
+ import type { HealthStatus } from '../../lib/health';
18
4
  type IoValkey = typeof import('iovalkey');
19
5
  export default class ValkeyStore extends Store {
20
6
  name: string;
@@ -27,6 +13,7 @@ export default class ValkeyStore extends Store {
27
13
  /** Split out so a test can stand in for a missing package. */
28
14
  protected driver(): Promise<IoValkey>;
29
15
  open(): Promise<void>;
16
+ health(): Promise<HealthStatus>;
30
17
  close(): Promise<void>;
31
18
  /**
32
19
  * The client, once connected. A failed first connection is not kept: the