@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.
Files changed (209) hide show
  1. package/LICENSE +21 -0
  2. package/dist/app.d.ts +79 -0
  3. package/dist/app.js +155 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/approvals/links.d.ts +23 -0
  6. package/dist/approvals/links.js +49 -0
  7. package/dist/approvals/links.js.map +1 -0
  8. package/dist/approvals/service.d.ts +110 -0
  9. package/dist/approvals/service.js +182 -0
  10. package/dist/approvals/service.js.map +1 -0
  11. package/dist/audit-query.d.ts +52 -0
  12. package/dist/audit-query.js +116 -0
  13. package/dist/audit-query.js.map +1 -0
  14. package/dist/audit.d.ts +5 -0
  15. package/dist/audit.js +8 -0
  16. package/dist/audit.js.map +1 -0
  17. package/dist/auth/mcp-auth.d.ts +50 -0
  18. package/dist/auth/mcp-auth.js +160 -0
  19. package/dist/auth/mcp-auth.js.map +1 -0
  20. package/dist/auth/mcp-tokens.d.ts +57 -0
  21. package/dist/auth/mcp-tokens.js +143 -0
  22. package/dist/auth/mcp-tokens.js.map +1 -0
  23. package/dist/auth/oauth.d.ts +188 -0
  24. package/dist/auth/oauth.js +470 -0
  25. package/dist/auth/oauth.js.map +1 -0
  26. package/dist/auth/oidc.d.ts +110 -0
  27. package/dist/auth/oidc.js +221 -0
  28. package/dist/auth/oidc.js.map +1 -0
  29. package/dist/auth/sessions.d.ts +34 -0
  30. package/dist/auth/sessions.js +83 -0
  31. package/dist/auth/sessions.js.map +1 -0
  32. package/dist/auth/throttle.d.ts +31 -0
  33. package/dist/auth/throttle.js +52 -0
  34. package/dist/auth/throttle.js.map +1 -0
  35. package/dist/auth/tokens.d.ts +7 -0
  36. package/dist/auth/tokens.js +40 -0
  37. package/dist/auth/tokens.js.map +1 -0
  38. package/dist/auth/totp.d.ts +14 -0
  39. package/dist/auth/totp.js +81 -0
  40. package/dist/auth/totp.js.map +1 -0
  41. package/dist/auth/users.d.ts +70 -0
  42. package/dist/auth/users.js +296 -0
  43. package/dist/auth/users.js.map +1 -0
  44. package/dist/catalog/groups.d.ts +102 -0
  45. package/dist/catalog/groups.js +363 -0
  46. package/dist/catalog/groups.js.map +1 -0
  47. package/dist/catalog/registry.d.ts +25 -0
  48. package/dist/catalog/registry.js +80 -0
  49. package/dist/catalog/registry.js.map +1 -0
  50. package/dist/catalog/rules.d.ts +128 -0
  51. package/dist/catalog/rules.js +178 -0
  52. package/dist/catalog/rules.js.map +1 -0
  53. package/dist/catalog/sync.d.ts +29 -0
  54. package/dist/catalog/sync.js +222 -0
  55. package/dist/catalog/sync.js.map +1 -0
  56. package/dist/cli.d.ts +3 -0
  57. package/dist/cli.js +83 -0
  58. package/dist/cli.js.map +1 -0
  59. package/dist/config/env.d.ts +40 -0
  60. package/dist/config/env.js +71 -0
  61. package/dist/config/env.js.map +1 -0
  62. package/dist/crypto/index.d.ts +28 -0
  63. package/dist/crypto/index.js +88 -0
  64. package/dist/crypto/index.js.map +1 -0
  65. package/dist/crypto/rotate.d.ts +15 -0
  66. package/dist/crypto/rotate.js +61 -0
  67. package/dist/crypto/rotate.js.map +1 -0
  68. package/dist/db/index.d.ts +17 -0
  69. package/dist/db/index.js +29 -0
  70. package/dist/db/index.js.map +1 -0
  71. package/dist/db/schema.d.ts +4389 -0
  72. package/dist/db/schema.js +372 -0
  73. package/dist/db/schema.js.map +1 -0
  74. package/dist/endpoints/slug.d.ts +3 -0
  75. package/dist/endpoints/slug.js +7 -0
  76. package/dist/endpoints/slug.js.map +1 -0
  77. package/dist/errors.d.ts +16 -0
  78. package/dist/errors.js +29 -0
  79. package/dist/errors.js.map +1 -0
  80. package/dist/events.d.ts +43 -0
  81. package/dist/events.js +16 -0
  82. package/dist/events.js.map +1 -0
  83. package/dist/gate/access.d.ts +47 -0
  84. package/dist/gate/access.js +48 -0
  85. package/dist/gate/access.js.map +1 -0
  86. package/dist/gate/attestation.d.ts +23 -0
  87. package/dist/gate/attestation.js +33 -0
  88. package/dist/gate/attestation.js.map +1 -0
  89. package/dist/gate/canonical.d.ts +3 -0
  90. package/dist/gate/canonical.js +11 -0
  91. package/dist/gate/canonical.js.map +1 -0
  92. package/dist/gate/match.d.ts +67 -0
  93. package/dist/gate/match.js +133 -0
  94. package/dist/gate/match.js.map +1 -0
  95. package/dist/gate/pipeline.d.ts +61 -0
  96. package/dist/gate/pipeline.js +284 -0
  97. package/dist/gate/pipeline.js.map +1 -0
  98. package/dist/gate/preapproval.d.ts +24 -0
  99. package/dist/gate/preapproval.js +48 -0
  100. package/dist/gate/preapproval.js.map +1 -0
  101. package/dist/gate/rate-limit.d.ts +35 -0
  102. package/dist/gate/rate-limit.js +82 -0
  103. package/dist/gate/rate-limit.js.map +1 -0
  104. package/dist/gate/redact.d.ts +23 -0
  105. package/dist/gate/redact.js +87 -0
  106. package/dist/gate/redact.js.map +1 -0
  107. package/dist/http/admin/auth.d.ts +33 -0
  108. package/dist/http/admin/auth.js +338 -0
  109. package/dist/http/admin/auth.js.map +1 -0
  110. package/dist/http/admin/instances.d.ts +5 -0
  111. package/dist/http/admin/instances.js +193 -0
  112. package/dist/http/admin/instances.js.map +1 -0
  113. package/dist/http/admin/system.d.ts +5 -0
  114. package/dist/http/admin/system.js +225 -0
  115. package/dist/http/admin/system.js.map +1 -0
  116. package/dist/http/admin-app.d.ts +9 -0
  117. package/dist/http/admin-app.js +51 -0
  118. package/dist/http/admin-app.js.map +1 -0
  119. package/dist/http/common.d.ts +32 -0
  120. package/dist/http/common.js +80 -0
  121. package/dist/http/common.js.map +1 -0
  122. package/dist/http/mcp/approval-routes.d.ts +11 -0
  123. package/dist/http/mcp/approval-routes.js +117 -0
  124. package/dist/http/mcp/approval-routes.js.map +1 -0
  125. package/dist/http/mcp/endpoint.d.ts +32 -0
  126. package/dist/http/mcp/endpoint.js +293 -0
  127. package/dist/http/mcp/endpoint.js.map +1 -0
  128. package/dist/http/mcp/oauth-routes.d.ts +29 -0
  129. package/dist/http/mcp/oauth-routes.js +465 -0
  130. package/dist/http/mcp/oauth-routes.js.map +1 -0
  131. package/dist/http/mcp/pages.d.ts +63 -0
  132. package/dist/http/mcp/pages.js +200 -0
  133. package/dist/http/mcp/pages.js.map +1 -0
  134. package/dist/http/mcp-app.d.ts +7 -0
  135. package/dist/http/mcp-app.js +30 -0
  136. package/dist/http/mcp-app.js.map +1 -0
  137. package/dist/instances/connection.d.ts +37 -0
  138. package/dist/instances/connection.js +109 -0
  139. package/dist/instances/connection.js.map +1 -0
  140. package/dist/instances/manager.d.ts +313 -0
  141. package/dist/instances/manager.js +600 -0
  142. package/dist/instances/manager.js.map +1 -0
  143. package/dist/instances/settings.d.ts +42 -0
  144. package/dist/instances/settings.js +57 -0
  145. package/dist/instances/settings.js.map +1 -0
  146. package/dist/lenient.d.ts +16 -0
  147. package/dist/lenient.js +51 -0
  148. package/dist/lenient.js.map +1 -0
  149. package/dist/lock.d.ts +28 -0
  150. package/dist/lock.js +77 -0
  151. package/dist/lock.js.map +1 -0
  152. package/dist/main.d.ts +1 -0
  153. package/dist/main.js +38 -0
  154. package/dist/main.js.map +1 -0
  155. package/dist/maintenance.d.ts +10 -0
  156. package/dist/maintenance.js +48 -0
  157. package/dist/maintenance.js.map +1 -0
  158. package/dist/notify/service.d.ts +149 -0
  159. package/dist/notify/service.js +358 -0
  160. package/dist/notify/service.js.map +1 -0
  161. package/dist/plugins/discovery.d.ts +39 -0
  162. package/dist/plugins/discovery.js +157 -0
  163. package/dist/plugins/discovery.js.map +1 -0
  164. package/dist/plugins/minisign.d.ts +25 -0
  165. package/dist/plugins/minisign.js +49 -0
  166. package/dist/plugins/minisign.js.map +1 -0
  167. package/dist/plugins/process.d.ts +55 -0
  168. package/dist/plugins/process.js +162 -0
  169. package/dist/plugins/process.js.map +1 -0
  170. package/dist/plugins/repos.d.ts +198 -0
  171. package/dist/plugins/repos.js +612 -0
  172. package/dist/plugins/repos.js.map +1 -0
  173. package/dist/plugins/supervisor.d.ts +50 -0
  174. package/dist/plugins/supervisor.js +125 -0
  175. package/dist/plugins/supervisor.js.map +1 -0
  176. package/dist/runtime/index.d.ts +28 -0
  177. package/dist/runtime/index.js +248 -0
  178. package/dist/runtime/index.js.map +1 -0
  179. package/dist/sandbox/index.d.ts +53 -0
  180. package/dist/sandbox/index.js +197 -0
  181. package/dist/sandbox/index.js.map +1 -0
  182. package/dist/server.d.ts +16 -0
  183. package/dist/server.js +36 -0
  184. package/dist/server.js.map +1 -0
  185. package/dist/settings.d.ts +43 -0
  186. package/dist/settings.js +99 -0
  187. package/dist/settings.js.map +1 -0
  188. package/dist/testing/index.d.ts +73 -0
  189. package/dist/testing/index.js +174 -0
  190. package/dist/testing/index.js.map +1 -0
  191. package/dist/version.d.ts +2 -0
  192. package/dist/version.js +4 -0
  193. package/dist/version.js.map +1 -0
  194. package/drizzle/0000_init.sql +334 -0
  195. package/drizzle/0001_totp_last_step.sql +1 -0
  196. package/drizzle/0002_access_levels.sql +16 -0
  197. package/drizzle/0003_drop_exclusion_flags.sql +2 -0
  198. package/drizzle/0004_grant_instance_ids.sql +1 -0
  199. package/drizzle/0005_rule_strict_miss.sql +1 -0
  200. package/drizzle/0006_attestation_waived.sql +1 -0
  201. package/drizzle/meta/0000_snapshot.json +2318 -0
  202. package/drizzle/meta/0001_snapshot.json +2325 -0
  203. package/drizzle/meta/0002_snapshot.json +2348 -0
  204. package/drizzle/meta/0003_snapshot.json +2332 -0
  205. package/drizzle/meta/0004_snapshot.json +2339 -0
  206. package/drizzle/meta/0005_snapshot.json +2346 -0
  207. package/drizzle/meta/0006_snapshot.json +2354 -0
  208. package/drizzle/meta/_journal.json +55 -0
  209. package/package.json +58 -0
@@ -0,0 +1,197 @@
1
+ import ivm from 'isolated-vm';
2
+ export const DEFAULT_LIMITS = {
3
+ timeoutMs: 10_000,
4
+ memoryMb: 64,
5
+ maxResultBytes: 64 * 1024,
6
+ maxLogBytes: 16 * 1024,
7
+ };
8
+ /** Thrown by a binding to surface a structured error (`err.code`) inside the sandbox. */
9
+ export class BindingError extends Error {
10
+ code;
11
+ constructor(code, message) {
12
+ super(message);
13
+ this.code = code;
14
+ this.name = 'BindingError';
15
+ }
16
+ }
17
+ const RESERVED = new Set(['console', 'globalThis', '__syn']);
18
+ const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
19
+ /** Wall-clock budget that can be paused; fires `onExpire` once when it runs out. */
20
+ class Budget {
21
+ onExpire;
22
+ remaining;
23
+ startedAt = 0;
24
+ paused = 0;
25
+ timer;
26
+ expired = false;
27
+ constructor(totalMs, onExpire) {
28
+ this.onExpire = onExpire;
29
+ this.remaining = totalMs;
30
+ }
31
+ start() {
32
+ this.startedAt = Date.now();
33
+ this.timer = setTimeout(() => {
34
+ this.expired = true;
35
+ this.onExpire();
36
+ }, this.remaining);
37
+ }
38
+ pause() {
39
+ if (this.paused++ > 0 || this.expired)
40
+ return;
41
+ clearTimeout(this.timer);
42
+ this.remaining -= Date.now() - this.startedAt;
43
+ }
44
+ resume() {
45
+ if (--this.paused > 0 || this.expired)
46
+ return;
47
+ this.start();
48
+ }
49
+ remainingMs() {
50
+ return this.paused > 0 ? this.remaining : Math.max(0, this.remaining - (Date.now() - this.startedAt));
51
+ }
52
+ stop() {
53
+ clearTimeout(this.timer);
54
+ }
55
+ }
56
+ // Runs inside the isolate, as its own script before the user's. Everything it holds on to — the host
57
+ // references, the wrapper — lives in the closure, so user code (a separate script) can never name it.
58
+ // It installs `console` and one frozen object per binding namespace, and removes `__syn` from the global.
59
+ const prelude = (spec) => `(() => {
60
+ const call = globalThis.__syn.call;
61
+ const log = globalThis.__syn.log;
62
+ delete globalThis.__syn;
63
+ const stringify = JSON.stringify;
64
+ const parse = JSON.parse;
65
+ globalThis.console = Object.freeze({
66
+ log: (...a) => log.applySync(undefined, [stringify(a.map((x) => x === undefined ? null : x))]),
67
+ });
68
+ const bind = (ns, fn) => async (...args) => {
69
+ const raw = await call.apply(undefined, [ns, fn, stringify(args)], { result: { promise: true } });
70
+ const res = parse(raw);
71
+ if (res.ok) return res.value;
72
+ const err = new Error(res.error.message);
73
+ err.code = res.error.code;
74
+ throw err;
75
+ };
76
+ const spec = ${JSON.stringify(spec)};
77
+ for (const ns of Object.keys(spec)) {
78
+ const fns = {};
79
+ for (const fn of spec[ns]) fns[fn] = bind(ns, fn);
80
+ globalThis[ns] = Object.freeze(fns);
81
+ }
82
+ })();`;
83
+ function toJson(value) {
84
+ return JSON.stringify(value === undefined ? null : value);
85
+ }
86
+ export async function runInSandbox(run) {
87
+ const limits = { ...DEFAULT_LIMITS, ...run.limits };
88
+ const redact = run.redact ?? ((v) => v);
89
+ const logs = [];
90
+ let logBytes = 0;
91
+ const isolate = new ivm.Isolate({ memoryLimit: limits.memoryMb });
92
+ const budget = new Budget(limits.timeoutMs, () => {
93
+ if (!isolate.isDisposed)
94
+ isolate.dispose();
95
+ });
96
+ try {
97
+ const context = await isolate.createContext();
98
+ const jail = context.global;
99
+ await jail.set('globalThis', jail.derefInto());
100
+ const hostCall = async (ns, fn, argsJson) => {
101
+ // Own properties only: `constructor`, `__proto__` and friends are not bindings.
102
+ const fns = Object.hasOwn(run.bindings, ns) ? run.bindings[ns] : undefined;
103
+ const binding = fns && Object.hasOwn(fns, fn) ? fns[fn] : undefined;
104
+ if (!binding)
105
+ return toJson({ ok: false, error: { code: 'NOT_A_BINDING', message: `${ns}.${fn} is not available` } });
106
+ try {
107
+ const args = JSON.parse(argsJson);
108
+ return toJson({ ok: true, value: await binding(args, budget) });
109
+ }
110
+ catch (err) {
111
+ const code = err instanceof BindingError ? err.code : 'INTERNAL';
112
+ const message = err instanceof BindingError ? err.message : 'Internal error in binding';
113
+ return toJson({ ok: false, error: { code, message } });
114
+ }
115
+ };
116
+ const hostLog = (argsJson) => {
117
+ if (logBytes >= limits.maxLogBytes)
118
+ return;
119
+ let args;
120
+ try {
121
+ args = JSON.parse(argsJson);
122
+ }
123
+ catch {
124
+ args = ['[unprintable]'];
125
+ }
126
+ // Redact the values, then format: a JSON string of a secret has no key left to redact by.
127
+ const line = redact(args).map((x) => (typeof x === 'string' ? x : JSON.stringify(x))).join(' ');
128
+ const text = line.slice(0, limits.maxLogBytes - logBytes);
129
+ logBytes += text.length;
130
+ logs.push(text);
131
+ };
132
+ await jail.set('__syn', new ivm.ExternalCopy({}).copyInto());
133
+ const syn = (await jail.get('__syn'));
134
+ await syn.set('call', new ivm.Reference(hostCall));
135
+ await syn.set('log', new ivm.Reference(hostLog));
136
+ const spec = Object.fromEntries(Object.entries(run.bindings)
137
+ .filter(([ns]) => IDENT.test(ns) && !RESERVED.has(ns))
138
+ .map(([ns, fns]) => [ns, Object.keys(fns).filter((fn) => IDENT.test(fn))]));
139
+ await (await isolate.compileScript(prelude(spec))).run(context);
140
+ // The user's code is a separate script: it can't see the prelude's closure.
141
+ // Always settle with a JSON envelope so thrown errors keep their \`code\` across the boundary.
142
+ const source = `(async () => {\n${run.code}\n})().then(
143
+ (v) => JSON.stringify({ ok: true, value: v === undefined ? null : v }),
144
+ (e) => JSON.stringify({ ok: false, error: { code: typeof e?.code === 'string' ? e.code : 'CODE_ERROR', message: String(e?.message ?? e) } }),
145
+ )`;
146
+ const script = await isolate.compileScript(source).catch((err) => {
147
+ throw new SandboxFailure('SYNTAX_ERROR', err.message);
148
+ });
149
+ budget.start();
150
+ const json = (await script.run(context, { promise: true, timeout: limits.timeoutMs }));
151
+ budget.stop();
152
+ const envelope = JSON.parse(json);
153
+ if (!envelope.ok)
154
+ return { ok: false, error: redact(envelope.error), logs };
155
+ // Redact before measuring: the preview of an oversized result is raw text, beyond key redaction.
156
+ const value = redact(envelope.value);
157
+ const valueJson = JSON.stringify(value);
158
+ const bytes = Buffer.byteLength(valueJson);
159
+ if (bytes > limits.maxResultBytes) {
160
+ const preview = Buffer.from(valueJson).subarray(0, limits.maxResultBytes).toString('utf8');
161
+ return { ok: true, value: { truncated: true, bytes, preview }, truncated: true, logs };
162
+ }
163
+ return { ok: true, value, truncated: false, logs };
164
+ }
165
+ catch (err) {
166
+ budget.stop();
167
+ return {
168
+ ok: false,
169
+ error: redact(classify(err, budget.expired, isolate)),
170
+ logs,
171
+ };
172
+ }
173
+ finally {
174
+ if (!isolate.isDisposed)
175
+ isolate.dispose();
176
+ }
177
+ }
178
+ class SandboxFailure extends Error {
179
+ code;
180
+ constructor(code, message) {
181
+ super(message);
182
+ this.code = code;
183
+ }
184
+ }
185
+ function classify(err, expired, isolate) {
186
+ if (err instanceof SandboxFailure)
187
+ return { code: err.code, message: err.message };
188
+ const message = err instanceof Error ? err.message : String(err);
189
+ if (expired || /timed out/i.test(message))
190
+ return { code: 'TIMEOUT', message: 'Sandbox time limit exceeded' };
191
+ if (/memory limit/i.test(message) || isolate.isDisposed) {
192
+ return { code: 'MEMORY_LIMIT', message: 'Sandbox memory limit exceeded' };
193
+ }
194
+ // A synchronous throw at top level (the envelope catches everything asynchronous).
195
+ return { code: 'CODE_ERROR', message };
196
+ }
197
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sandbox/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { Server } from 'node:http';
2
+ import type { AppContext } from './app.js';
3
+ export interface RunningServers {
4
+ mcp: {
5
+ server: Server;
6
+ port: number;
7
+ };
8
+ admin: {
9
+ server: Server;
10
+ port: number;
11
+ };
12
+ /** Stops accepting, then waits up to `graceMs` for open requests before cutting their connections. */
13
+ close(graceMs?: number): Promise<void>;
14
+ }
15
+ /** Starts the two listeners on separate ports (design §2.1). */
16
+ export declare function startServers(ctx: AppContext): Promise<RunningServers>;
package/dist/server.js ADDED
@@ -0,0 +1,36 @@
1
+ import { serve } from '@hono/node-server';
2
+ import { createAdminApp } from './http/admin-app.js';
3
+ import { createMcpApp } from './http/mcp-app.js';
4
+ function listen(app, hostname, port) {
5
+ return new Promise((resolve, reject) => {
6
+ const server = serve({ fetch: app.fetch, hostname, port }, (info) => resolve({ server: server, port: info.port }));
7
+ server.once('error', reject);
8
+ });
9
+ }
10
+ function close(server, graceMs) {
11
+ return new Promise((resolve, reject) => {
12
+ const cut = setTimeout(() => server.closeAllConnections(), graceMs);
13
+ server.close((err) => {
14
+ clearTimeout(cut);
15
+ if (err)
16
+ reject(err);
17
+ else
18
+ resolve();
19
+ });
20
+ server.closeIdleConnections();
21
+ });
22
+ }
23
+ /** Starts the two listeners on separate ports (design §2.1). */
24
+ export async function startServers(ctx) {
25
+ const { config } = ctx;
26
+ const mcp = await listen(createMcpApp(ctx), config.MCP_HOST, config.MCP_PORT);
27
+ const admin = await listen(createAdminApp(ctx, { uiDir: config.ADMIN_UI_DIR }), config.ADMIN_HOST, config.ADMIN_PORT);
28
+ return {
29
+ mcp,
30
+ admin,
31
+ close: async (graceMs = 5000) => {
32
+ await Promise.all([close(mcp.server, graceMs), close(admin.server, graceMs)]);
33
+ },
34
+ };
35
+ }
36
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AASjD,SAAS,MAAM,CAAC,GAA8D,EAAE,QAAgB,EAAE,IAAY;IAC5G,OAAO,IAAI,OAAO,CAAmC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACvE,MAAM,MAAM,GAAG,KAAK,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,IAAiB,EAAE,EAAE,CAC/E,OAAO,CAAC,EAAE,MAAM,EAAE,MAAgB,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAC7C,CAAC;QACZ,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,KAAK,CAAC,MAAc,EAAE,OAAe;IAC5C,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3C,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,EAAE,EAAE,OAAO,CAAC,CAAC;QACpE,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACnB,YAAY,CAAC,GAAG,CAAC,CAAC;YAClB,IAAI,GAAG;gBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;;gBAChB,OAAO,EAAE,CAAC;QACjB,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,oBAAoB,EAAE,CAAC;IAChC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,gEAAgE;AAChE,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAe;IAChD,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC;IACvB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC9E,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,YAAY,EAAE,CAAC,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IACtH,OAAO;QACL,GAAG;QACH,KAAK;QACL,KAAK,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,EAAE,EAAE;YAC9B,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QAChF,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ import type { Db } from './db/index.js';
3
+ /** Global settings (design §8.2 "Settings"), one JSON document per section in the `settings` table. */
4
+ export declare const SETTINGS_SCHEMAS: {
5
+ readonly security: z.ZodPrefault<z.ZodObject<{
6
+ requireTotp: z.ZodDefault<z.ZodBoolean>;
7
+ disableLocalLogin: z.ZodDefault<z.ZodBoolean>;
8
+ sessionIdleMinutes: z.ZodDefault<z.ZodNumber>;
9
+ sessionAbsoluteHours: z.ZodDefault<z.ZodNumber>;
10
+ }, z.core.$strip>>;
11
+ readonly mcp: z.ZodPrefault<z.ZodObject<{
12
+ defaultAuthMode: z.ZodDefault<z.ZodEnum<{
13
+ external: "external";
14
+ bearer: "bearer";
15
+ oauth: "oauth";
16
+ "bearer+oauth": "bearer+oauth";
17
+ }>>;
18
+ allowDynamicRegistration: z.ZodDefault<z.ZodBoolean>;
19
+ cfAccess: z.ZodPrefault<z.ZodObject<{
20
+ teamDomain: z.ZodDefault<z.ZodString>;
21
+ aud: z.ZodDefault<z.ZodString>;
22
+ }, z.core.$strip>>;
23
+ trustedIdentityHeader: z.ZodDefault<z.ZodString>;
24
+ accessTokenTtlMinutes: z.ZodDefault<z.ZodNumber>;
25
+ refreshTokenTtlDays: z.ZodDefault<z.ZodNumber>;
26
+ }, z.core.$strip>>;
27
+ readonly audit: z.ZodPrefault<z.ZodObject<{
28
+ retentionDays: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
29
+ }, z.core.$strip>>;
30
+ };
31
+ export type SettingsSection = keyof typeof SETTINGS_SCHEMAS;
32
+ export type Settings<S extends SettingsSection> = z.infer<(typeof SETTINGS_SCHEMAS)[S]>;
33
+ export declare function isSettingsSection(name: string): name is SettingsSection;
34
+ /** Stored settings are read leniently: a field a newer schema rejects falls back to its default. */
35
+ export declare function getSettings<S extends SettingsSection>(db: Db, section: S): Settings<S>;
36
+ /**
37
+ * Startup step: removes stored settings fields the current schema rejects, once (and warned about
38
+ * once) rather than on every read. Valid fields are left exactly as stored.
39
+ */
40
+ export declare function normalizeStoredSettings(db: Db): string[];
41
+ export declare function updateSettings<S extends SettingsSection>(db: Db, section: S, patch: unknown, actor?: {
42
+ userId?: string;
43
+ }): Settings<S>;
@@ -0,0 +1,99 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { z } from 'zod';
3
+ import { writeAudit } from './audit.js';
4
+ import { settings } from './db/schema.js';
5
+ import { ValidationError } from './errors.js';
6
+ import { cleanStored, parseLeniently } from './lenient.js';
7
+ /** Global settings (design §8.2 "Settings"), one JSON document per section in the `settings` table. */
8
+ export const SETTINGS_SCHEMAS = {
9
+ security: z
10
+ .object({
11
+ /** Force every user to enroll TOTP at next login. */
12
+ requireTotp: z.boolean().default(false),
13
+ /** Only OIDC sign-in. Saving requires an OIDC-linked user; ADMIN_FORCE_LOCAL_LOGIN overrides. */
14
+ disableLocalLogin: z.boolean().default(false),
15
+ sessionIdleMinutes: z
16
+ .number()
17
+ .int()
18
+ .min(5)
19
+ .max(24 * 60)
20
+ .default(30),
21
+ sessionAbsoluteHours: z
22
+ .number()
23
+ .int()
24
+ .min(1)
25
+ .max(24 * 30)
26
+ .default(12),
27
+ })
28
+ .prefault({}),
29
+ mcp: z
30
+ .object({
31
+ defaultAuthMode: z.enum(['external', 'bearer', 'oauth', 'bearer+oauth']).default('bearer+oauth'),
32
+ allowDynamicRegistration: z.boolean().default(true),
33
+ /** Cloudflare Access JWT verification for `external` mode. */
34
+ cfAccess: z.object({ teamDomain: z.string().default(''), aud: z.string().default('') }).prefault({}),
35
+ /** Header set by an auth proxy (Authelia/Authentik) used only to attribute calls in `external` mode. */
36
+ trustedIdentityHeader: z.string().default(''),
37
+ accessTokenTtlMinutes: z
38
+ .number()
39
+ .int()
40
+ .min(5)
41
+ .max(24 * 60)
42
+ .default(60),
43
+ refreshTokenTtlDays: z.number().int().min(1).max(365).default(30),
44
+ })
45
+ .prefault({}),
46
+ audit: z
47
+ .object({
48
+ /** Unset = keep forever (design §10). */
49
+ retentionDays: z.number().int().min(7).nullable().default(null),
50
+ })
51
+ .prefault({}),
52
+ };
53
+ export function isSettingsSection(name) {
54
+ return Object.prototype.hasOwnProperty.call(SETTINGS_SCHEMAS, name);
55
+ }
56
+ /** Stored settings are read leniently: a field a newer schema rejects falls back to its default. */
57
+ export function getSettings(db, section) {
58
+ const row = db.select().from(settings).where(eq(settings.key, section)).get();
59
+ return parseLeniently(SETTINGS_SCHEMAS[section], row?.value, `settings.${section}`);
60
+ }
61
+ /**
62
+ * Startup step: removes stored settings fields the current schema rejects, once (and warned about
63
+ * once) rather than on every read. Valid fields are left exactly as stored.
64
+ */
65
+ export function normalizeStoredSettings(db) {
66
+ const changed = [];
67
+ for (const row of db.select().from(settings).all()) {
68
+ if (!isSettingsSection(row.key))
69
+ continue;
70
+ const { cleaned, dropped } = cleanStored(SETTINGS_SCHEMAS[row.key], row.value, `settings.${row.key}`);
71
+ if (!dropped.length)
72
+ continue;
73
+ db.update(settings).set({ value: cleaned }).where(eq(settings.key, row.key)).run();
74
+ changed.push(row.key);
75
+ }
76
+ return changed;
77
+ }
78
+ export function updateSettings(db, section, patch, actor = {}) {
79
+ const before = getSettings(db, section);
80
+ const parsed = SETTINGS_SCHEMAS[section].safeParse({ ...before, ...patch });
81
+ if (!parsed.success) {
82
+ throw new ValidationError('invalid_settings', 'Invalid settings', parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`));
83
+ }
84
+ db.transaction((tx) => {
85
+ tx.insert(settings)
86
+ .values({ key: section, value: parsed.data })
87
+ .onConflictDoUpdate({ target: settings.key, set: { value: parsed.data } })
88
+ .run();
89
+ writeAudit(tx, {
90
+ kind: 'config',
91
+ decision: 'settings_updated',
92
+ actorKind: 'user',
93
+ actorId: actor.userId,
94
+ detail: { section, before, after: parsed.data },
95
+ });
96
+ });
97
+ return parsed.data;
98
+ }
99
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import type { Manifest } from '@synoikia/plugin-sdk';
2
+ import type { AppContext, AppOptions } from '../app.js';
3
+ import { auditLog, operations, pendingApprovals, registryEntries } from '../db/schema.js';
4
+ import type { AccessLevel } from '../gate/access.js';
5
+ import type { SandboxResult } from '../sandbox/index.js';
6
+ /**
7
+ * End-to-end harness for plugin packages (`@synoikia/core/testing`). A plugin's own test suite
8
+ * boots the real core on its built bundle, which runs as a permission-confined child exactly as in
9
+ * production, and drives it through the same `execute`/`search` path an MCP client uses. Core never
10
+ * names a plugin: each plugin brings its own fake upstream and its own assertions.
11
+ */
12
+ export interface PluginHarnessOptions {
13
+ /** The plugin package directory (the one holding `manifest.json`); its entry must be built. */
14
+ pluginDir: string;
15
+ /** Connection config and secrets, as the admin portal would submit them. */
16
+ connection: Record<string, unknown>;
17
+ /** Endpoint slug; defaults to the plugin id. */
18
+ slug?: string;
19
+ /** Run a catalog/registry sync before returning (default true). */
20
+ sync?: boolean;
21
+ supervisor?: AppOptions['supervisor'];
22
+ }
23
+ /** An approval as the harness hands it to `onApproval`: decide it like the approval page would. */
24
+ export interface HarnessApproval {
25
+ id: string;
26
+ operationKey: string;
27
+ /** The prompt the MCP client was shown. */
28
+ message: string;
29
+ /** The stored approval (operation, params, targets, diff, confirmation literal). */
30
+ pending: typeof pendingApprovals.$inferSelect;
31
+ /** Throws, as the approval page refuses it, when `confirm` doesn't match a typed confirmation. */
32
+ approve(confirm?: string): void;
33
+ deny(): void;
34
+ }
35
+ export interface ExecuteOptions {
36
+ /**
37
+ * Called for each approval the code triggers. Without it the caller can't be prompted, so a call
38
+ * that needs a human is denied. If it throws or leaves the approval open, the approval is denied
39
+ * and the error is kept in `approvalErrors`.
40
+ */
41
+ onApproval?: (approval: HarnessApproval) => void | Promise<void>;
42
+ }
43
+ export interface PluginHarness {
44
+ ctx: AppContext;
45
+ instanceId: string;
46
+ manifest: Manifest;
47
+ /** Errors thrown by `onApproval` handlers, oldest first. */
48
+ approvalErrors: unknown[];
49
+ execute(code: string, opts?: ExecuteOptions): Promise<SandboxResult>;
50
+ search(code: string): Promise<SandboxResult>;
51
+ setGroupLevel(group: string, level: AccessLevel): void;
52
+ /** The operation's own level; `null` makes it follow its group again. */
53
+ setOperationLevel(key: string, level: AccessLevel | null): void;
54
+ addRule(rule: {
55
+ operation: string;
56
+ match: unknown[];
57
+ reason: string;
58
+ }): {
59
+ id: string;
60
+ };
61
+ operation(key: string): typeof operations.$inferSelect;
62
+ operations(): (typeof operations.$inferSelect)[];
63
+ registry(): (typeof registryEntries.$inferSelect)[];
64
+ audit(filter?: {
65
+ operationKey?: string;
66
+ }): (typeof auditLog.$inferSelect)[];
67
+ instance(): ReturnType<AppContext['instances']['get']>;
68
+ /** Runs the plugin's `testConnection` in a throwaway child with this connection. */
69
+ testConnection(connection: Record<string, unknown>): Promise<unknown>;
70
+ syncNow(): ReturnType<AppContext['instances']['syncNow']>;
71
+ stop(): Promise<void>;
72
+ }
73
+ export declare function startPluginHarness(opts: PluginHarnessOptions): Promise<PluginHarness>;