@synoikia/core 0.1.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/LICENSE +21 -0
- package/dist/app.d.ts +79 -0
- package/dist/app.js +155 -0
- package/dist/app.js.map +1 -0
- package/dist/approvals/links.d.ts +23 -0
- package/dist/approvals/links.js +49 -0
- package/dist/approvals/links.js.map +1 -0
- package/dist/approvals/service.d.ts +110 -0
- package/dist/approvals/service.js +182 -0
- package/dist/approvals/service.js.map +1 -0
- package/dist/audit-query.d.ts +52 -0
- package/dist/audit-query.js +116 -0
- package/dist/audit-query.js.map +1 -0
- package/dist/audit.d.ts +5 -0
- package/dist/audit.js +8 -0
- package/dist/audit.js.map +1 -0
- package/dist/auth/mcp-auth.d.ts +50 -0
- package/dist/auth/mcp-auth.js +160 -0
- package/dist/auth/mcp-auth.js.map +1 -0
- package/dist/auth/mcp-tokens.d.ts +57 -0
- package/dist/auth/mcp-tokens.js +143 -0
- package/dist/auth/mcp-tokens.js.map +1 -0
- package/dist/auth/oauth.d.ts +188 -0
- package/dist/auth/oauth.js +470 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/oidc.d.ts +110 -0
- package/dist/auth/oidc.js +221 -0
- package/dist/auth/oidc.js.map +1 -0
- package/dist/auth/sessions.d.ts +34 -0
- package/dist/auth/sessions.js +83 -0
- package/dist/auth/sessions.js.map +1 -0
- package/dist/auth/throttle.d.ts +31 -0
- package/dist/auth/throttle.js +52 -0
- package/dist/auth/throttle.js.map +1 -0
- package/dist/auth/tokens.d.ts +7 -0
- package/dist/auth/tokens.js +40 -0
- package/dist/auth/tokens.js.map +1 -0
- package/dist/auth/totp.d.ts +14 -0
- package/dist/auth/totp.js +81 -0
- package/dist/auth/totp.js.map +1 -0
- package/dist/auth/users.d.ts +70 -0
- package/dist/auth/users.js +296 -0
- package/dist/auth/users.js.map +1 -0
- package/dist/catalog/groups.d.ts +102 -0
- package/dist/catalog/groups.js +363 -0
- package/dist/catalog/groups.js.map +1 -0
- package/dist/catalog/registry.d.ts +25 -0
- package/dist/catalog/registry.js +80 -0
- package/dist/catalog/registry.js.map +1 -0
- package/dist/catalog/rules.d.ts +128 -0
- package/dist/catalog/rules.js +178 -0
- package/dist/catalog/rules.js.map +1 -0
- package/dist/catalog/sync.d.ts +29 -0
- package/dist/catalog/sync.js +222 -0
- package/dist/catalog/sync.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +83 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/env.d.ts +40 -0
- package/dist/config/env.js +71 -0
- package/dist/config/env.js.map +1 -0
- package/dist/crypto/index.d.ts +28 -0
- package/dist/crypto/index.js +88 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/crypto/rotate.d.ts +15 -0
- package/dist/crypto/rotate.js +61 -0
- package/dist/crypto/rotate.js.map +1 -0
- package/dist/db/index.d.ts +17 -0
- package/dist/db/index.js +29 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +4389 -0
- package/dist/db/schema.js +372 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/endpoints/slug.d.ts +3 -0
- package/dist/endpoints/slug.js +7 -0
- package/dist/endpoints/slug.js.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +29 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +43 -0
- package/dist/events.js +16 -0
- package/dist/events.js.map +1 -0
- package/dist/gate/access.d.ts +47 -0
- package/dist/gate/access.js +48 -0
- package/dist/gate/access.js.map +1 -0
- package/dist/gate/attestation.d.ts +23 -0
- package/dist/gate/attestation.js +33 -0
- package/dist/gate/attestation.js.map +1 -0
- package/dist/gate/canonical.d.ts +3 -0
- package/dist/gate/canonical.js +11 -0
- package/dist/gate/canonical.js.map +1 -0
- package/dist/gate/match.d.ts +67 -0
- package/dist/gate/match.js +133 -0
- package/dist/gate/match.js.map +1 -0
- package/dist/gate/pipeline.d.ts +61 -0
- package/dist/gate/pipeline.js +284 -0
- package/dist/gate/pipeline.js.map +1 -0
- package/dist/gate/preapproval.d.ts +24 -0
- package/dist/gate/preapproval.js +48 -0
- package/dist/gate/preapproval.js.map +1 -0
- package/dist/gate/rate-limit.d.ts +35 -0
- package/dist/gate/rate-limit.js +82 -0
- package/dist/gate/rate-limit.js.map +1 -0
- package/dist/gate/redact.d.ts +23 -0
- package/dist/gate/redact.js +87 -0
- package/dist/gate/redact.js.map +1 -0
- package/dist/http/admin/auth.d.ts +33 -0
- package/dist/http/admin/auth.js +338 -0
- package/dist/http/admin/auth.js.map +1 -0
- package/dist/http/admin/instances.d.ts +5 -0
- package/dist/http/admin/instances.js +193 -0
- package/dist/http/admin/instances.js.map +1 -0
- package/dist/http/admin/system.d.ts +5 -0
- package/dist/http/admin/system.js +225 -0
- package/dist/http/admin/system.js.map +1 -0
- package/dist/http/admin-app.d.ts +9 -0
- package/dist/http/admin-app.js +51 -0
- package/dist/http/admin-app.js.map +1 -0
- package/dist/http/common.d.ts +32 -0
- package/dist/http/common.js +80 -0
- package/dist/http/common.js.map +1 -0
- package/dist/http/mcp/approval-routes.d.ts +11 -0
- package/dist/http/mcp/approval-routes.js +117 -0
- package/dist/http/mcp/approval-routes.js.map +1 -0
- package/dist/http/mcp/endpoint.d.ts +32 -0
- package/dist/http/mcp/endpoint.js +293 -0
- package/dist/http/mcp/endpoint.js.map +1 -0
- package/dist/http/mcp/oauth-routes.d.ts +29 -0
- package/dist/http/mcp/oauth-routes.js +465 -0
- package/dist/http/mcp/oauth-routes.js.map +1 -0
- package/dist/http/mcp/pages.d.ts +63 -0
- package/dist/http/mcp/pages.js +200 -0
- package/dist/http/mcp/pages.js.map +1 -0
- package/dist/http/mcp-app.d.ts +7 -0
- package/dist/http/mcp-app.js +30 -0
- package/dist/http/mcp-app.js.map +1 -0
- package/dist/instances/connection.d.ts +37 -0
- package/dist/instances/connection.js +109 -0
- package/dist/instances/connection.js.map +1 -0
- package/dist/instances/manager.d.ts +313 -0
- package/dist/instances/manager.js +600 -0
- package/dist/instances/manager.js.map +1 -0
- package/dist/instances/settings.d.ts +42 -0
- package/dist/instances/settings.js +57 -0
- package/dist/instances/settings.js.map +1 -0
- package/dist/lenient.d.ts +16 -0
- package/dist/lenient.js +51 -0
- package/dist/lenient.js.map +1 -0
- package/dist/lock.d.ts +28 -0
- package/dist/lock.js +77 -0
- package/dist/lock.js.map +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +38 -0
- package/dist/main.js.map +1 -0
- package/dist/maintenance.d.ts +10 -0
- package/dist/maintenance.js +48 -0
- package/dist/maintenance.js.map +1 -0
- package/dist/notify/service.d.ts +149 -0
- package/dist/notify/service.js +358 -0
- package/dist/notify/service.js.map +1 -0
- package/dist/plugins/discovery.d.ts +39 -0
- package/dist/plugins/discovery.js +157 -0
- package/dist/plugins/discovery.js.map +1 -0
- package/dist/plugins/minisign.d.ts +25 -0
- package/dist/plugins/minisign.js +49 -0
- package/dist/plugins/minisign.js.map +1 -0
- package/dist/plugins/process.d.ts +55 -0
- package/dist/plugins/process.js +162 -0
- package/dist/plugins/process.js.map +1 -0
- package/dist/plugins/repos.d.ts +198 -0
- package/dist/plugins/repos.js +612 -0
- package/dist/plugins/repos.js.map +1 -0
- package/dist/plugins/supervisor.d.ts +50 -0
- package/dist/plugins/supervisor.js +125 -0
- package/dist/plugins/supervisor.js.map +1 -0
- package/dist/runtime/index.d.ts +28 -0
- package/dist/runtime/index.js +248 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/sandbox/index.d.ts +53 -0
- package/dist/sandbox/index.js +197 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.js +36 -0
- package/dist/server.js.map +1 -0
- package/dist/settings.d.ts +43 -0
- package/dist/settings.js +99 -0
- package/dist/settings.js.map +1 -0
- package/dist/testing/index.d.ts +73 -0
- package/dist/testing/index.js +174 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/drizzle/0000_init.sql +334 -0
- package/drizzle/0001_totp_last_step.sql +1 -0
- package/drizzle/0002_access_levels.sql +16 -0
- package/drizzle/0003_drop_exclusion_flags.sql +2 -0
- package/drizzle/0004_grant_instance_ids.sql +1 -0
- package/drizzle/0005_rule_strict_miss.sql +1 -0
- package/drizzle/0006_attestation_waived.sql +1 -0
- package/drizzle/meta/0000_snapshot.json +2318 -0
- package/drizzle/meta/0001_snapshot.json +2325 -0
- package/drizzle/meta/0002_snapshot.json +2348 -0
- package/drizzle/meta/0003_snapshot.json +2332 -0
- package/drizzle/meta/0004_snapshot.json +2339 -0
- package/drizzle/meta/0005_snapshot.json +2346 -0
- package/drizzle/meta/0006_snapshot.json +2354 -0
- package/drizzle/meta/_journal.json +55 -0
- package/package.json +58 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Per-instance runtime settings (design §8.2 "Instance Settings"), stored as JSON on `plugin_instances.settings`. */
|
|
3
|
+
export declare const InstanceSettingsSchema: z.ZodPrefault<z.ZodObject<{
|
|
4
|
+
approvalTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
5
|
+
formElicitationApprovals: z.ZodDefault<z.ZodEnum<{
|
|
6
|
+
off: "off";
|
|
7
|
+
writes: "writes";
|
|
8
|
+
}>>;
|
|
9
|
+
executePerMinute: z.ZodDefault<z.ZodNumber>;
|
|
10
|
+
writesPerMinute: z.ZodDefault<z.ZodNumber>;
|
|
11
|
+
sandbox: z.ZodPrefault<z.ZodObject<{
|
|
12
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
13
|
+
memoryMb: z.ZodDefault<z.ZodNumber>;
|
|
14
|
+
maxResultBytes: z.ZodDefault<z.ZodNumber>;
|
|
15
|
+
}, z.core.$strip>>;
|
|
16
|
+
extraRedactKeys: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
17
|
+
syncMaxAgeMs: z.ZodDefault<z.ZodNumber>;
|
|
18
|
+
memoryMb: z.ZodDefault<z.ZodNumber>;
|
|
19
|
+
}, z.core.$strip>>;
|
|
20
|
+
export type InstanceSettings = z.infer<typeof InstanceSettingsSchema>;
|
|
21
|
+
export declare function parseInstanceSettings(raw: unknown): InstanceSettings;
|
|
22
|
+
/** Stored instance settings, read leniently (a field a newer schema rejects falls back to its default). */
|
|
23
|
+
export declare function readInstanceSettings(raw: unknown, label?: string): InstanceSettings;
|
|
24
|
+
/** For the startup normalization: the stored settings with just the rejected fields removed. */
|
|
25
|
+
export declare function cleanInstanceSettings(raw: unknown, label: string): {
|
|
26
|
+
value: {
|
|
27
|
+
approvalTimeoutMs: number;
|
|
28
|
+
formElicitationApprovals: "off" | "writes";
|
|
29
|
+
executePerMinute: number;
|
|
30
|
+
writesPerMinute: number;
|
|
31
|
+
sandbox: {
|
|
32
|
+
timeoutMs: number;
|
|
33
|
+
memoryMb: number;
|
|
34
|
+
maxResultBytes: number;
|
|
35
|
+
};
|
|
36
|
+
extraRedactKeys: string[];
|
|
37
|
+
syncMaxAgeMs: number;
|
|
38
|
+
memoryMb: number;
|
|
39
|
+
};
|
|
40
|
+
cleaned: unknown;
|
|
41
|
+
dropped: string[];
|
|
42
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { cleanStored, parseLeniently } from '../lenient.js';
|
|
3
|
+
/** Per-instance runtime settings (design §8.2 "Instance Settings"), stored as JSON on `plugin_instances.settings`. */
|
|
4
|
+
export const InstanceSettingsSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
/** Unanswered approvals are auto-denied after this long (TN §3.3). */
|
|
7
|
+
approvalTimeoutMs: z
|
|
8
|
+
.number()
|
|
9
|
+
.int()
|
|
10
|
+
.min(10_000)
|
|
11
|
+
.max(24 * 60 * 60_000)
|
|
12
|
+
.default(15 * 60_000),
|
|
13
|
+
/**
|
|
14
|
+
* Let MCP clients that only support form prompts approve plain writes (never locked or
|
|
15
|
+
* typed-confirmation operations). Off by default: any client on this endpoint could then approve
|
|
16
|
+
* its own writes. URL prompts (a signed-in human on the approval page) always work.
|
|
17
|
+
*/
|
|
18
|
+
formElicitationApprovals: z.enum(['off', 'writes']).default('off'),
|
|
19
|
+
/** `execute` and `search` runs per minute, per principal (design §5.2). */
|
|
20
|
+
executePerMinute: z.number().int().min(1).max(10_000).default(30),
|
|
21
|
+
writesPerMinute: z.number().int().min(1).max(10_000).default(10),
|
|
22
|
+
sandbox: z
|
|
23
|
+
.object({
|
|
24
|
+
timeoutMs: z.number().int().min(100).max(120_000).default(10_000),
|
|
25
|
+
memoryMb: z.number().int().min(8).max(1024).default(64),
|
|
26
|
+
maxResultBytes: z
|
|
27
|
+
.number()
|
|
28
|
+
.int()
|
|
29
|
+
.min(1024)
|
|
30
|
+
.max(4 * 1024 * 1024)
|
|
31
|
+
.default(64 * 1024),
|
|
32
|
+
})
|
|
33
|
+
.prefault({}),
|
|
34
|
+
extraRedactKeys: z.array(z.string().min(1)).default([]),
|
|
35
|
+
/** Re-sync the catalog on session start when the last sync is older than this (TN §5). */
|
|
36
|
+
syncMaxAgeMs: z
|
|
37
|
+
.number()
|
|
38
|
+
.int()
|
|
39
|
+
.min(60_000)
|
|
40
|
+
.max(7 * 24 * 60 * 60_000)
|
|
41
|
+
.default(60 * 60_000),
|
|
42
|
+
/** Plugin child heap limit (design §4.4). */
|
|
43
|
+
memoryMb: z.number().int().min(64).max(4096).default(256),
|
|
44
|
+
})
|
|
45
|
+
.prefault({});
|
|
46
|
+
export function parseInstanceSettings(raw) {
|
|
47
|
+
return InstanceSettingsSchema.parse(raw ?? {});
|
|
48
|
+
}
|
|
49
|
+
/** Stored instance settings, read leniently (a field a newer schema rejects falls back to its default). */
|
|
50
|
+
export function readInstanceSettings(raw, label = 'instance settings') {
|
|
51
|
+
return parseLeniently(InstanceSettingsSchema, raw, label);
|
|
52
|
+
}
|
|
53
|
+
/** For the startup normalization: the stored settings with just the rejected fields removed. */
|
|
54
|
+
export function cleanInstanceSettings(raw, label) {
|
|
55
|
+
return cleanStored(InstanceSettingsSchema, raw, label);
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/instances/settings.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Parses stored JSON (settings written by an earlier release) without letting one field the current
|
|
4
|
+
* schema no longer accepts take the whole feature down: each invalid field is dropped, so its default
|
|
5
|
+
* applies, and a warning names it. Input from requests is still validated strictly elsewhere.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseLeniently<S extends z.ZodType>(schema: S, raw: unknown, label: string, warn?: (message: string) => void): z.infer<S>;
|
|
8
|
+
/**
|
|
9
|
+
* The lenient parse, plus the stored value with just the rejected fields removed (defaults are not
|
|
10
|
+
* written in, so a later release's new defaults still apply to fields nobody set).
|
|
11
|
+
*/
|
|
12
|
+
export declare function cleanStored<S extends z.ZodType>(schema: S, raw: unknown, label: string, warn?: (message: string) => void): {
|
|
13
|
+
value: z.infer<S>;
|
|
14
|
+
cleaned: unknown;
|
|
15
|
+
dropped: string[];
|
|
16
|
+
};
|
package/dist/lenient.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses stored JSON (settings written by an earlier release) without letting one field the current
|
|
3
|
+
* schema no longer accepts take the whole feature down: each invalid field is dropped, so its default
|
|
4
|
+
* applies, and a warning names it. Input from requests is still validated strictly elsewhere.
|
|
5
|
+
*/
|
|
6
|
+
export function parseLeniently(schema, raw, label, warn = (m) => console.warn(`WARN ${m}`)) {
|
|
7
|
+
return cleanStored(schema, raw, label, warn).value;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The lenient parse, plus the stored value with just the rejected fields removed (defaults are not
|
|
11
|
+
* written in, so a later release's new defaults still apply to fields nobody set).
|
|
12
|
+
*/
|
|
13
|
+
export function cleanStored(schema, raw, label, warn = (m) => console.warn(`WARN ${m}`)) {
|
|
14
|
+
const cleaned = raw === null || raw === undefined ? {} : structuredClone(raw);
|
|
15
|
+
const dropped = [];
|
|
16
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
17
|
+
const parsed = schema.safeParse(cleaned);
|
|
18
|
+
if (parsed.success)
|
|
19
|
+
return { value: parsed.data, cleaned, dropped };
|
|
20
|
+
let progress = false;
|
|
21
|
+
for (const issue of parsed.error.issues) {
|
|
22
|
+
const path = issue.path.filter((p) => typeof p !== 'symbol');
|
|
23
|
+
if (path.length === 0 || !removeAt(cleaned, path))
|
|
24
|
+
continue;
|
|
25
|
+
warn(`${label}: ignoring stored ${path.join('.')} (${issue.message}); using the default`);
|
|
26
|
+
dropped.push(path.join('.'));
|
|
27
|
+
progress = true;
|
|
28
|
+
}
|
|
29
|
+
if (!progress)
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
warn(`${label}: stored value is unusable; using defaults`);
|
|
33
|
+
return { value: schema.parse({}), cleaned: {}, dropped: [...dropped, '*'] };
|
|
34
|
+
}
|
|
35
|
+
function removeAt(root, path) {
|
|
36
|
+
let node = root;
|
|
37
|
+
for (const key of path.slice(0, -1)) {
|
|
38
|
+
if (!node || typeof node !== 'object')
|
|
39
|
+
return false;
|
|
40
|
+
node = node[key];
|
|
41
|
+
}
|
|
42
|
+
const last = path.at(-1);
|
|
43
|
+
if (!node || typeof node !== 'object' || !(last in node))
|
|
44
|
+
return false;
|
|
45
|
+
if (Array.isArray(node) && typeof last === 'number')
|
|
46
|
+
node.splice(last, 1);
|
|
47
|
+
else
|
|
48
|
+
delete node[last];
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=lenient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lenient.js","sourceRoot":"","sources":["../src/lenient.ts"],"names":[],"mappings":"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"}
|
package/dist/lock.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A running server holds `DATA_DIR/server.lock` (pid and host). Offline operator commands such as
|
|
3
|
+
* `rotate-master-key` refuse to run while it is held: a live server keeps the old master key in memory
|
|
4
|
+
* and would write secrets under it after the rotation (design §7.1).
|
|
5
|
+
*/
|
|
6
|
+
export declare const LOCK_FILENAME = "server.lock";
|
|
7
|
+
interface LockInfo {
|
|
8
|
+
pid: number;
|
|
9
|
+
host: string;
|
|
10
|
+
startedAt: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The live holder of the lock, or null if it is free or stale. A lock from another host (another
|
|
14
|
+
* container on the same volume) can't be checked from here, so it counts as held: only its pid on
|
|
15
|
+
* this host can be proven dead.
|
|
16
|
+
*/
|
|
17
|
+
export declare function lockHolder(dataDir: string): LockInfo | null;
|
|
18
|
+
/**
|
|
19
|
+
* Takes the lock for this process; throws if another live server on this host holds it. A lock left
|
|
20
|
+
* by another host is taken over with a warning: a recreated container gets a new hostname, and a
|
|
21
|
+
* server that refused to start there would never start again. Returns the release.
|
|
22
|
+
*/
|
|
23
|
+
export declare function acquireServerLock(dataDir: string, warn?: (message: string) => void): () => void;
|
|
24
|
+
/** For offline commands: throws while a server may be running on this data directory, unless forced. */
|
|
25
|
+
export declare function assertServerStopped(dataDir: string, opts?: {
|
|
26
|
+
force?: boolean;
|
|
27
|
+
}): void;
|
|
28
|
+
export {};
|
package/dist/lock.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* A running server holds `DATA_DIR/server.lock` (pid and host). Offline operator commands such as
|
|
6
|
+
* `rotate-master-key` refuse to run while it is held: a live server keeps the old master key in memory
|
|
7
|
+
* and would write secrets under it after the rotation (design §7.1).
|
|
8
|
+
*/
|
|
9
|
+
export const LOCK_FILENAME = 'server.lock';
|
|
10
|
+
const lockPath = (dataDir) => path.join(dataDir, LOCK_FILENAME);
|
|
11
|
+
const alive = (pid) => {
|
|
12
|
+
try {
|
|
13
|
+
process.kill(pid, 0);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
return err.code === 'EPERM';
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
function readLock(dataDir) {
|
|
21
|
+
try {
|
|
22
|
+
const info = JSON.parse(readFileSync(lockPath(dataDir), 'utf8'));
|
|
23
|
+
return typeof info?.pid === 'number' && typeof info.host === 'string' ? info : null;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The live holder of the lock, or null if it is free or stale. A lock from another host (another
|
|
31
|
+
* container on the same volume) can't be checked from here, so it counts as held: only its pid on
|
|
32
|
+
* this host can be proven dead.
|
|
33
|
+
*/
|
|
34
|
+
export function lockHolder(dataDir) {
|
|
35
|
+
const info = readLock(dataDir);
|
|
36
|
+
if (!info || info.pid === process.pid)
|
|
37
|
+
return null;
|
|
38
|
+
if (info.host !== hostname())
|
|
39
|
+
return info;
|
|
40
|
+
return alive(info.pid) ? info : null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Takes the lock for this process; throws if another live server on this host holds it. A lock left
|
|
44
|
+
* by another host is taken over with a warning: a recreated container gets a new hostname, and a
|
|
45
|
+
* server that refused to start there would never start again. Returns the release.
|
|
46
|
+
*/
|
|
47
|
+
export function acquireServerLock(dataDir, warn = (m) => console.warn(`WARN ${m}`)) {
|
|
48
|
+
const previous = readLock(dataDir);
|
|
49
|
+
if (previous && previous.pid !== process.pid) {
|
|
50
|
+
if (previous.host === hostname()) {
|
|
51
|
+
if (alive(previous.pid))
|
|
52
|
+
throw new Error(`Another server (pid ${previous.pid}) is using ${dataDir}`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
warn(`${lockPath(dataDir)} was held by pid ${previous.pid} on host ${previous.host}; taking it over. ` +
|
|
56
|
+
'Two servers must never share a data directory.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const info = { pid: process.pid, host: hostname(), startedAt: new Date().toISOString() };
|
|
60
|
+
writeFileSync(lockPath(dataDir), `${JSON.stringify(info)}\n`, { mode: 0o600 });
|
|
61
|
+
return () => {
|
|
62
|
+
if (readLock(dataDir)?.pid === process.pid && readLock(dataDir)?.host === hostname())
|
|
63
|
+
rmSync(lockPath(dataDir), { force: true });
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** For offline commands: throws while a server may be running on this data directory, unless forced. */
|
|
67
|
+
export function assertServerStopped(dataDir, opts = {}) {
|
|
68
|
+
if (opts.force)
|
|
69
|
+
return;
|
|
70
|
+
const holder = lockHolder(dataDir);
|
|
71
|
+
if (!holder)
|
|
72
|
+
return;
|
|
73
|
+
const where = holder.host === hostname() ? `pid ${holder.pid}` : `pid ${holder.pid} on host ${holder.host}`;
|
|
74
|
+
throw new Error(`The server may be running (${where}, since ${holder.startedAt}); stop it first. ` +
|
|
75
|
+
`If it is really stopped, run again with --force (or delete ${lockPath(dataDir)}).`);
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=lock.js.map
|
package/dist/lock.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../src/lock.ts"],"names":[],"mappings":"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"}
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { createAppContext } from './app.js';
|
|
3
|
+
import { loadConfig } from './config/env.js';
|
|
4
|
+
import { acquireServerLock } from './lock.js';
|
|
5
|
+
import { startServers } from './server.js';
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
mkdirSync(config.DATA_DIR, { recursive: true });
|
|
8
|
+
const releaseLock = acquireServerLock(config.DATA_DIR);
|
|
9
|
+
process.on('exit', releaseLock);
|
|
10
|
+
const ctx = await createAppContext(config);
|
|
11
|
+
for (const warning of ctx.warnings)
|
|
12
|
+
console.warn(`WARN ${warning}`);
|
|
13
|
+
if (ctx.users.count() === 0)
|
|
14
|
+
console.log('No users yet: open the admin portal to create the first account.');
|
|
15
|
+
await ctx.start();
|
|
16
|
+
const servers = await startServers(ctx);
|
|
17
|
+
console.log(`MCP listener on ${config.MCP_HOST}:${servers.mcp.port}`);
|
|
18
|
+
console.log(`Admin listener on ${config.ADMIN_HOST}:${servers.admin.port}`);
|
|
19
|
+
let stopping = false;
|
|
20
|
+
const shutdown = (signal) => {
|
|
21
|
+
if (stopping)
|
|
22
|
+
return;
|
|
23
|
+
stopping = true;
|
|
24
|
+
console.log(`${signal} received, shutting down`);
|
|
25
|
+
// Drain first (approvals, MCP sessions, SSE), then close the listeners with a grace period, then stop
|
|
26
|
+
// plugins and the database; nothing here waits on a client that keeps its connection open.
|
|
27
|
+
ctx
|
|
28
|
+
.drain()
|
|
29
|
+
.then(() => servers.close())
|
|
30
|
+
.then(() => ctx.stop())
|
|
31
|
+
.then(() => process.exit(0), (err) => {
|
|
32
|
+
console.error(err);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
37
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
38
|
+
//# sourceMappingURL=main.js.map
|
package/dist/main.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;AAC5B,SAAS,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAChD,MAAM,WAAW,GAAG,iBAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AACvD,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAChC,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,MAAM,CAAC,CAAC;AAC3C,KAAK,MAAM,OAAO,IAAI,GAAG,CAAC,QAAQ;IAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAC;AACpE,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC;IAAE,OAAO,CAAC,GAAG,CAAC,kEAAkE,CAAC,CAAC;AAC7G,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;AAElB,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,CAAC;AACxC,OAAO,CAAC,GAAG,CAAC,mBAAmB,MAAM,CAAC,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;AACtE,OAAO,CAAC,GAAG,CAAC,qBAAqB,MAAM,CAAC,UAAU,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;AAE5E,IAAI,QAAQ,GAAG,KAAK,CAAC;AACrB,MAAM,QAAQ,GAAG,CAAC,MAAc,EAAE,EAAE;IAClC,IAAI,QAAQ;QAAE,OAAO;IACrB,QAAQ,GAAG,IAAI,CAAC;IAChB,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,0BAA0B,CAAC,CAAC;IACjD,sGAAsG;IACtG,2FAA2F;IAC3F,GAAG;SACA,KAAK,EAAE;SACP,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;SAC3B,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;SACtB,IAAI,CACH,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EACrB,CAAC,GAAY,EAAE,EAAE;QACf,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CACF,CAAC;AACN,CAAC,CAAC;AACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AAC/C,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AppContext } from './app.js';
|
|
2
|
+
export interface HousekeepingResult {
|
|
3
|
+
sessions: number;
|
|
4
|
+
oauth: number;
|
|
5
|
+
approvalLinks: number;
|
|
6
|
+
preApprovalHits: number;
|
|
7
|
+
approvals: number;
|
|
8
|
+
audit: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function runHousekeeping(ctx: AppContext): HousekeepingResult;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { and, lt, max, notInArray } from 'drizzle-orm';
|
|
2
|
+
import { writeAudit } from './audit.js';
|
|
3
|
+
import { auditLog, pendingApprovals, preApprovalHits, preApprovalRules } from './db/schema.js';
|
|
4
|
+
import { getSettings } from './settings.js';
|
|
5
|
+
/**
|
|
6
|
+
* Housekeeping (design §10): expired sessions, OAuth codes/tokens and approval links, rate-limit hits
|
|
7
|
+
* older than any rule's window, decided approvals past a week, and — only when an admin configured a
|
|
8
|
+
* retention — old audit rows. Runs hourly; every step is idempotent.
|
|
9
|
+
*/
|
|
10
|
+
const DAY = 24 * 60 * 60_000;
|
|
11
|
+
export function runHousekeeping(ctx) {
|
|
12
|
+
const now = ctx.now();
|
|
13
|
+
// Keep hits for the longest rule window (default 1 h), so rate limits stay exact.
|
|
14
|
+
const longest = ctx.db
|
|
15
|
+
.select({ w: max(preApprovalRules.windowSeconds) })
|
|
16
|
+
.from(preApprovalRules)
|
|
17
|
+
.get()?.w ?? 3600;
|
|
18
|
+
const hitCutoff = new Date(now.getTime() - Math.max(longest, 3600) * 1000);
|
|
19
|
+
const hits = ctx.db.delete(preApprovalHits).where(lt(preApprovalHits.occurredAt, hitCutoff)).run().changes;
|
|
20
|
+
// Links cascade with their approval; approvals must outlive any link, and stay visible for a week.
|
|
21
|
+
const approvals = ctx.db
|
|
22
|
+
.delete(pendingApprovals)
|
|
23
|
+
.where(and(lt(pendingApprovals.requestedAt, new Date(now.getTime() - 7 * DAY)), notInArray(pendingApprovals.status, ['pending'])))
|
|
24
|
+
.run().changes;
|
|
25
|
+
let audit = 0;
|
|
26
|
+
const { retentionDays } = getSettings(ctx.db, 'audit');
|
|
27
|
+
if (retentionDays) {
|
|
28
|
+
const cutoff = new Date(now.getTime() - retentionDays * DAY);
|
|
29
|
+
audit = ctx.db.delete(auditLog).where(lt(auditLog.at, cutoff)).run().changes;
|
|
30
|
+
if (audit > 0) {
|
|
31
|
+
writeAudit(ctx.db, {
|
|
32
|
+
kind: 'config',
|
|
33
|
+
decision: 'audit_purged',
|
|
34
|
+
actorKind: 'system',
|
|
35
|
+
detail: { rows: audit, before: cutoff.toISOString(), retentionDays },
|
|
36
|
+
}, now);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
sessions: ctx.sessions.purgeExpired(),
|
|
41
|
+
oauth: ctx.oauth.purgeExpired(),
|
|
42
|
+
approvalLinks: ctx.links.purgeExpired(),
|
|
43
|
+
preApprovalHits: hits,
|
|
44
|
+
approvals,
|
|
45
|
+
audit,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=maintenance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"maintenance.js","sourceRoot":"","sources":["../src/maintenance.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC/F,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C;;;;GAIG;AAEH,MAAM,GAAG,GAAG,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC;AAW7B,MAAM,UAAU,eAAe,CAAC,GAAe;IAC7C,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC;IAEtB,kFAAkF;IAClF,MAAM,OAAO,GACX,GAAG,CAAC,EAAE;SACH,MAAM,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,gBAAgB,CAAC,aAAa,CAAC,EAAE,CAAC;SAClD,IAAI,CAAC,gBAAgB,CAAC;SACtB,GAAG,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC;IACtB,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3E,MAAM,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,eAAe,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC;IAE3G,mGAAmG;IACnG,MAAM,SAAS,GAAG,GAAG,CAAC,EAAE;SACrB,MAAM,CAAC,gBAAgB,CAAC;SACxB,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,gBAAgB,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,EACnE,UAAU,CAAC,gBAAgB,CAAC,MAAM,EAAE,CAAC,SAAS,CAAC,CAAC,CACjD,CACF;SACA,GAAG,EAAE,CAAC,OAAO,CAAC;IAEjB,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,MAAM,EAAE,aAAa,EAAE,GAAG,WAAW,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IACvD,IAAI,aAAa,EAAE,CAAC;QAClB,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,aAAa,GAAG,GAAG,CAAC,CAAC;QAC7D,KAAK,GAAG,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC;QAC7E,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACd,UAAU,CACR,GAAG,CAAC,EAAE,EACN;gBACE,IAAI,EAAE,QAAQ;gBACd,QAAQ,EAAE,cAAc;gBACxB,SAAS,EAAE,QAAQ;gBACnB,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,EAAE,aAAa,EAAE;aACrE,EACD,GAAG,CACJ,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,YAAY,EAAE;QACrC,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,YAAY,EAAE;QAC/B,aAAa,EAAE,GAAG,CAAC,KAAK,CAAC,YAAY,EAAE;QACvC,eAAe,EAAE,IAAI;QACrB,SAAS;QACT,KAAK;KACN,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { SecretBox } from '../crypto/index.js';
|
|
3
|
+
import type { Db } from '../db/index.js';
|
|
4
|
+
import type { CoreEvents } from '../events.js';
|
|
5
|
+
/**
|
|
6
|
+
* Notification channels (design §9): ntfy and signed webhooks, subscribed to core events and
|
|
7
|
+
* optionally filtered by instance. Informational only: approvals happen in the MCP client (§5.3).
|
|
8
|
+
* Payloads carry summaries, never raw params or secrets.
|
|
9
|
+
*/
|
|
10
|
+
export declare const NOTIFY_EVENTS: readonly ["instance.error", "instance.recovered", "plugin.crashed", "sync.failed", "sync.pending_review", "auth.lockout"];
|
|
11
|
+
export type NotifyEvent = (typeof NOTIFY_EVENTS)[number];
|
|
12
|
+
export declare const ChannelInputSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
13
|
+
kind: z.ZodLiteral<"ntfy">;
|
|
14
|
+
name: z.ZodString;
|
|
15
|
+
config: z.ZodObject<{
|
|
16
|
+
server: z.ZodDefault<z.ZodURL>;
|
|
17
|
+
topic: z.ZodString;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
secrets: z.ZodDefault<z.ZodObject<{
|
|
20
|
+
token: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
21
|
+
}, z.core.$strip>>;
|
|
22
|
+
events: z.ZodArray<z.ZodEnum<{
|
|
23
|
+
"plugin.crashed": "plugin.crashed";
|
|
24
|
+
"sync.failed": "sync.failed";
|
|
25
|
+
"auth.lockout": "auth.lockout";
|
|
26
|
+
"instance.error": "instance.error";
|
|
27
|
+
"instance.recovered": "instance.recovered";
|
|
28
|
+
"sync.pending_review": "sync.pending_review";
|
|
29
|
+
}>>;
|
|
30
|
+
instanceFilter: z.ZodDefault<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
31
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
32
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
33
|
+
kind: z.ZodLiteral<"webhook">;
|
|
34
|
+
name: z.ZodString;
|
|
35
|
+
config: z.ZodObject<{
|
|
36
|
+
url: z.ZodURL;
|
|
37
|
+
}, z.core.$strip>;
|
|
38
|
+
secrets: z.ZodDefault<z.ZodObject<{
|
|
39
|
+
hmacSecret: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
40
|
+
headers: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodString>>>;
|
|
41
|
+
}, z.core.$strip>>;
|
|
42
|
+
events: z.ZodArray<z.ZodEnum<{
|
|
43
|
+
"plugin.crashed": "plugin.crashed";
|
|
44
|
+
"sync.failed": "sync.failed";
|
|
45
|
+
"auth.lockout": "auth.lockout";
|
|
46
|
+
"instance.error": "instance.error";
|
|
47
|
+
"instance.recovered": "instance.recovered";
|
|
48
|
+
"sync.pending_review": "sync.pending_review";
|
|
49
|
+
}>>;
|
|
50
|
+
instanceFilter: z.ZodDefault<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
51
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
52
|
+
}, z.core.$strip>], "kind">;
|
|
53
|
+
export interface Notification {
|
|
54
|
+
event: NotifyEvent;
|
|
55
|
+
instance?: {
|
|
56
|
+
id: string;
|
|
57
|
+
slug: string;
|
|
58
|
+
};
|
|
59
|
+
title: string;
|
|
60
|
+
message: string;
|
|
61
|
+
data: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
export type FetchLike = (url: string, init: {
|
|
64
|
+
method: string;
|
|
65
|
+
headers: Record<string, string>;
|
|
66
|
+
body: string;
|
|
67
|
+
signal?: AbortSignal;
|
|
68
|
+
}) => Promise<{
|
|
69
|
+
ok: boolean;
|
|
70
|
+
status: number;
|
|
71
|
+
}>;
|
|
72
|
+
export declare class NotifierService {
|
|
73
|
+
private readonly db;
|
|
74
|
+
private readonly box;
|
|
75
|
+
private readonly opts;
|
|
76
|
+
private readonly lastStatus;
|
|
77
|
+
constructor(db: Db, box: SecretBox, opts?: {
|
|
78
|
+
fetch?: FetchLike;
|
|
79
|
+
retryDelaysMs?: number[];
|
|
80
|
+
now?: () => Date;
|
|
81
|
+
});
|
|
82
|
+
private secretsOf;
|
|
83
|
+
private publicRow;
|
|
84
|
+
list(): {
|
|
85
|
+
events: ("plugin.crashed" | "sync.failed" | "auth.lockout" | "instance.error" | "instance.recovered" | "sync.pending_review")[];
|
|
86
|
+
secretsEnc: undefined;
|
|
87
|
+
secrets: {
|
|
88
|
+
hmacSecret?: {
|
|
89
|
+
set: boolean;
|
|
90
|
+
} | undefined;
|
|
91
|
+
headers?: {
|
|
92
|
+
set: boolean;
|
|
93
|
+
} | undefined;
|
|
94
|
+
token?: {
|
|
95
|
+
set: boolean;
|
|
96
|
+
} | undefined;
|
|
97
|
+
};
|
|
98
|
+
id: string;
|
|
99
|
+
name: string;
|
|
100
|
+
kind: "ntfy" | "webhook";
|
|
101
|
+
enabled: boolean;
|
|
102
|
+
config: unknown;
|
|
103
|
+
instanceFilter: string[] | null;
|
|
104
|
+
lastSentAt: Date | null;
|
|
105
|
+
lastError: string | null;
|
|
106
|
+
}[];
|
|
107
|
+
private mergeSecrets;
|
|
108
|
+
save(raw: unknown, opts?: {
|
|
109
|
+
id?: string;
|
|
110
|
+
actor?: {
|
|
111
|
+
userId?: string;
|
|
112
|
+
};
|
|
113
|
+
}): {
|
|
114
|
+
events: ("plugin.crashed" | "sync.failed" | "auth.lockout" | "instance.error" | "instance.recovered" | "sync.pending_review")[];
|
|
115
|
+
secretsEnc: undefined;
|
|
116
|
+
secrets: {
|
|
117
|
+
hmacSecret?: {
|
|
118
|
+
set: boolean;
|
|
119
|
+
} | undefined;
|
|
120
|
+
headers?: {
|
|
121
|
+
set: boolean;
|
|
122
|
+
} | undefined;
|
|
123
|
+
token?: {
|
|
124
|
+
set: boolean;
|
|
125
|
+
} | undefined;
|
|
126
|
+
};
|
|
127
|
+
id: string;
|
|
128
|
+
name: string;
|
|
129
|
+
kind: "ntfy" | "webhook";
|
|
130
|
+
enabled: boolean;
|
|
131
|
+
config: unknown;
|
|
132
|
+
instanceFilter: string[] | null;
|
|
133
|
+
lastSentAt: Date | null;
|
|
134
|
+
lastError: string | null;
|
|
135
|
+
};
|
|
136
|
+
remove(id: string, actor?: {
|
|
137
|
+
userId?: string;
|
|
138
|
+
}): void;
|
|
139
|
+
/** Sends a test message to one channel, bypassing its event filter. */
|
|
140
|
+
test(id: string): Promise<{
|
|
141
|
+
ok: boolean;
|
|
142
|
+
error?: string;
|
|
143
|
+
}>;
|
|
144
|
+
/** Wires channels to the event bus. Returns an unsubscribe function. */
|
|
145
|
+
subscribe(events: CoreEvents): () => void;
|
|
146
|
+
/** Fans a notification out to every enabled channel subscribed to its event (and instance). */
|
|
147
|
+
dispatch(n: Notification): Promise<void>;
|
|
148
|
+
private deliver;
|
|
149
|
+
}
|