@objectstack/service-settings 0.1.1
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 +202 -0
- package/README.md +62 -0
- package/dist/index.cjs +1746 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +601 -0
- package/dist/index.d.ts +601 -0
- package/dist/index.js +1697 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import * as _objectstack_spec_contracts from '@objectstack/spec/contracts';
|
|
2
|
+
import { ICryptoProvider, IHttpRequest, IHttpServer } from '@objectstack/spec/contracts';
|
|
3
|
+
import { SettingsActionResult, SpecifierScope, SettingsChangeHandler, SettingsUnsubscribe, SettingsManifest, ResolvedSettingValue, SettingsNamespacePayload, TranslationData, TranslationBundle } from '@objectstack/spec/system';
|
|
4
|
+
export { ResolvedSettingValue, SettingsActionResult, SettingsManifest, SettingsNamespacePayload, SpecifierScope } from '@objectstack/spec/system';
|
|
5
|
+
import { Plugin, PluginContext } from '@objectstack/core';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pluggable adapter for at-rest encryption of `Specifier.encrypted: true`
|
|
9
|
+
* values. The default {@link NoopCryptoAdapter} provides a transparent
|
|
10
|
+
* base64 wrapping suitable for development and tests; production
|
|
11
|
+
* deployments MUST inject a real KMS-backed adapter.
|
|
12
|
+
*
|
|
13
|
+
* encrypt/decrypt are async to leave room for KMS round-trips.
|
|
14
|
+
*/
|
|
15
|
+
interface CryptoAdapter {
|
|
16
|
+
/** Returns the ciphertext blob to store in `sys_setting.value_enc`. */
|
|
17
|
+
encrypt(plaintext: string, ctx: {
|
|
18
|
+
namespace: string;
|
|
19
|
+
key: string;
|
|
20
|
+
}): Promise<string>;
|
|
21
|
+
/** Returns the plaintext used by the resolver. */
|
|
22
|
+
decrypt(ciphertext: string, ctx: {
|
|
23
|
+
namespace: string;
|
|
24
|
+
key: string;
|
|
25
|
+
}): Promise<string>;
|
|
26
|
+
/**
|
|
27
|
+
* Stable, short, non-reversible digest used for audit-log entries so
|
|
28
|
+
* operators can correlate value changes without leaking secrets.
|
|
29
|
+
*/
|
|
30
|
+
digest(plaintext: string): string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Development / test default. Base64-wraps the plaintext so the column
|
|
34
|
+
* isn't a literal mirror but provides no real confidentiality.
|
|
35
|
+
*
|
|
36
|
+
* Operators are expected to override this via
|
|
37
|
+
* `SettingsServicePluginOptions.crypto`.
|
|
38
|
+
*/
|
|
39
|
+
declare class NoopCryptoAdapter implements CryptoAdapter {
|
|
40
|
+
encrypt(plaintext: string): Promise<string>;
|
|
41
|
+
decrypt(ciphertext: string): Promise<string>;
|
|
42
|
+
digest(plaintext: string): string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Caller identity used by the resolver and audit log. */
|
|
46
|
+
interface SettingsContext {
|
|
47
|
+
/** Calling user id, when known. Required for `scope: 'user'` reads. */
|
|
48
|
+
userId?: string;
|
|
49
|
+
/** Tenant / project id. Reserved for multi-tenant deployments. */
|
|
50
|
+
tenantId?: string;
|
|
51
|
+
/** Permissions held by the caller (used by REST authz). */
|
|
52
|
+
permissions?: string[];
|
|
53
|
+
/** Source IP / request id for audit correlation. */
|
|
54
|
+
requestId?: string;
|
|
55
|
+
}
|
|
56
|
+
/** Storage row shape used by both the engine and the in-memory store. */
|
|
57
|
+
interface SettingsRow {
|
|
58
|
+
namespace: string;
|
|
59
|
+
key: string;
|
|
60
|
+
scope: SpecifierScope;
|
|
61
|
+
user_id: string | null;
|
|
62
|
+
value: unknown | null;
|
|
63
|
+
value_enc: string | null;
|
|
64
|
+
encrypted: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* When true, lower-scope rows for the same (namespace, key) are
|
|
67
|
+
* read-only — the resolver still returns this row's value and the
|
|
68
|
+
* mutation API throws `SettingsLockedError`. Only meaningful on
|
|
69
|
+
* upper-scope rows (`global`, `tenant`). (Phase 2)
|
|
70
|
+
*/
|
|
71
|
+
locked?: boolean;
|
|
72
|
+
/** Human-readable reason the lock was applied (UI tooltip). */
|
|
73
|
+
locked_reason?: string | null;
|
|
74
|
+
updated_at?: string;
|
|
75
|
+
updated_by?: string | null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Minimal data-engine surface used by the SettingsService. Mirrors the
|
|
79
|
+
* methods we actually call so we can stub it cleanly in tests without
|
|
80
|
+
* pulling the whole `IDataEngine`.
|
|
81
|
+
*/
|
|
82
|
+
interface SettingsEngine {
|
|
83
|
+
find(objectName: string, opts: {
|
|
84
|
+
where?: Record<string, unknown>;
|
|
85
|
+
limit?: number;
|
|
86
|
+
bypassTenantAudit?: boolean;
|
|
87
|
+
}): Promise<any[]>;
|
|
88
|
+
insert(objectName: string, data: Record<string, unknown>, opts?: {
|
|
89
|
+
bypassTenantAudit?: boolean;
|
|
90
|
+
}): Promise<any>;
|
|
91
|
+
update(objectName: string, opts: {
|
|
92
|
+
where: Record<string, unknown>;
|
|
93
|
+
data: Record<string, unknown>;
|
|
94
|
+
bypassTenantAudit?: boolean;
|
|
95
|
+
}): Promise<any>;
|
|
96
|
+
delete?(objectName: string, opts: {
|
|
97
|
+
where: Record<string, unknown>;
|
|
98
|
+
}): Promise<any>;
|
|
99
|
+
}
|
|
100
|
+
/** Optional audit hook — service-settings won't crash if absent. */
|
|
101
|
+
interface SettingsAuditSink {
|
|
102
|
+
record(entry: {
|
|
103
|
+
namespace: string;
|
|
104
|
+
key: string;
|
|
105
|
+
scope: SpecifierScope;
|
|
106
|
+
userId?: string;
|
|
107
|
+
actor?: string;
|
|
108
|
+
action: 'set' | 'reset';
|
|
109
|
+
valueDigest: string;
|
|
110
|
+
encrypted: boolean;
|
|
111
|
+
requestId?: string;
|
|
112
|
+
}): Promise<void> | void;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Persistence hook for the `sys_secret` object — used by the secret
|
|
116
|
+
* split introduced in Phase 3. When provided, `SettingsService` writes
|
|
117
|
+
* encrypted specifier values via `ICryptoProvider` into `sys_secret`
|
|
118
|
+
* and stores only the handle id in `sys_setting.value_enc`. When
|
|
119
|
+
* absent, the legacy inline `crypto.encrypt → value_enc` path is used.
|
|
120
|
+
*/
|
|
121
|
+
interface SettingsSecretStore {
|
|
122
|
+
/** Insert a new secret row; returns the row id (handle id). */
|
|
123
|
+
insert(row: {
|
|
124
|
+
id: string;
|
|
125
|
+
namespace: string;
|
|
126
|
+
key: string;
|
|
127
|
+
kms_key_id: string;
|
|
128
|
+
alg: string;
|
|
129
|
+
version: number;
|
|
130
|
+
ciphertext: string;
|
|
131
|
+
}): Promise<{
|
|
132
|
+
id: string;
|
|
133
|
+
}>;
|
|
134
|
+
/** Look up the latest ciphertext for a handle id; null when missing. */
|
|
135
|
+
get(id: string): Promise<{
|
|
136
|
+
id: string;
|
|
137
|
+
namespace: string;
|
|
138
|
+
key: string;
|
|
139
|
+
kms_key_id: string;
|
|
140
|
+
alg: string;
|
|
141
|
+
version: number;
|
|
142
|
+
ciphertext: string;
|
|
143
|
+
} | null>;
|
|
144
|
+
/** Replace an existing secret row (used by rotateKey). */
|
|
145
|
+
update(id: string, patch: {
|
|
146
|
+
kms_key_id?: string;
|
|
147
|
+
alg?: string;
|
|
148
|
+
version?: number;
|
|
149
|
+
ciphertext?: string;
|
|
150
|
+
}): Promise<void>;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Append-only writer for the `sys_setting_audit` object — Phase 3
|
|
154
|
+
* audit trail. Distinct from `SettingsAuditSink` (which still writes
|
|
155
|
+
* to the generic `sys_audit_log`) so audit consumers can subscribe
|
|
156
|
+
* to settings activity without scanning the firehose.
|
|
157
|
+
*/
|
|
158
|
+
interface SettingsAuditWriter {
|
|
159
|
+
write(entry: {
|
|
160
|
+
namespace: string;
|
|
161
|
+
key: string;
|
|
162
|
+
scope: SpecifierScope;
|
|
163
|
+
action: 'set' | 'reset' | 'lock' | 'unlock' | 'rotate';
|
|
164
|
+
source?: 'ui' | 'api' | 'migration' | 'import' | 'system';
|
|
165
|
+
actorId?: string;
|
|
166
|
+
oldHash?: string | null;
|
|
167
|
+
newHash?: string | null;
|
|
168
|
+
encrypted: boolean;
|
|
169
|
+
requestId?: string;
|
|
170
|
+
reason?: string;
|
|
171
|
+
}): Promise<void> | void;
|
|
172
|
+
}
|
|
173
|
+
/** Action handler signature for `Specifier.type === 'action_button'`. */
|
|
174
|
+
type SettingsActionHandler = (input: {
|
|
175
|
+
namespace: string;
|
|
176
|
+
actionId: string;
|
|
177
|
+
values: Record<string, unknown>;
|
|
178
|
+
payload?: unknown;
|
|
179
|
+
ctx: SettingsContext;
|
|
180
|
+
}) => Promise<SettingsActionResult> | SettingsActionResult;
|
|
181
|
+
interface SettingsServiceOptions {
|
|
182
|
+
/** Persistence engine. When undefined, an in-memory store is used. */
|
|
183
|
+
engine?: SettingsEngine;
|
|
184
|
+
/** Crypto adapter for `encrypted` values. Defaults to NoopCryptoAdapter. */
|
|
185
|
+
crypto?: CryptoAdapter;
|
|
186
|
+
/**
|
|
187
|
+
* Phase 3 ICryptoProvider used together with `secretStore`. When both
|
|
188
|
+
* are wired, encrypted writes flow to `sys_secret` and `value_enc`
|
|
189
|
+
* holds the handle id. When omitted, the legacy inline `crypto`
|
|
190
|
+
* adapter path remains in effect (back-compat).
|
|
191
|
+
*/
|
|
192
|
+
cryptoProvider?: _objectstack_spec_contracts.ICryptoProvider;
|
|
193
|
+
/** Phase 3 secret store backing the `sys_secret` object. */
|
|
194
|
+
secretStore?: SettingsSecretStore;
|
|
195
|
+
/** Audit sink. When undefined, writes still succeed but are not logged. */
|
|
196
|
+
audit?: SettingsAuditSink;
|
|
197
|
+
/** Phase 3 dedicated writer for `sys_setting_audit`. */
|
|
198
|
+
auditWriter?: SettingsAuditWriter;
|
|
199
|
+
/**
|
|
200
|
+
* `process.env`-like map. Defaults to `process.env`. Injected so
|
|
201
|
+
* unit tests can simulate locked values without polluting the host
|
|
202
|
+
* environment.
|
|
203
|
+
*/
|
|
204
|
+
env?: Record<string, string | undefined>;
|
|
205
|
+
/** Object name backing the K/V store. Defaults to 'sys_setting'. */
|
|
206
|
+
objectName?: string;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Convert `(namespace, key)` to the env var convention defined in
|
|
210
|
+
* ADR-0007: uppercase, dots → underscores, hyphens → underscores.
|
|
211
|
+
*/
|
|
212
|
+
declare function envKeyOf(namespace: string, key: string): string;
|
|
213
|
+
/** Thrown when a caller tries to write a value pinned by env. */
|
|
214
|
+
declare class SettingsLockedError extends Error {
|
|
215
|
+
readonly namespace: string;
|
|
216
|
+
readonly key: string;
|
|
217
|
+
readonly reason: string;
|
|
218
|
+
readonly code: "SETTINGS_LOCKED";
|
|
219
|
+
constructor(namespace: string, key: string, reason?: string);
|
|
220
|
+
}
|
|
221
|
+
/** Thrown when the requested namespace has no registered manifest. */
|
|
222
|
+
declare class UnknownNamespaceError extends Error {
|
|
223
|
+
readonly namespace: string;
|
|
224
|
+
readonly code: "SETTINGS_UNKNOWN_NAMESPACE";
|
|
225
|
+
constructor(namespace: string);
|
|
226
|
+
}
|
|
227
|
+
/** Thrown when a key isn't declared by the namespace's manifest. */
|
|
228
|
+
declare class UnknownKeyError extends Error {
|
|
229
|
+
readonly namespace: string;
|
|
230
|
+
readonly key: string;
|
|
231
|
+
readonly code: "SETTINGS_UNKNOWN_KEY";
|
|
232
|
+
constructor(namespace: string, key: string);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Concrete SettingsService. See `src/settings-service.types.ts` for
|
|
237
|
+
* the supporting types and `README.md` for the high-level contract.
|
|
238
|
+
*/
|
|
239
|
+
declare class SettingsService {
|
|
240
|
+
private engine?;
|
|
241
|
+
private readonly crypto;
|
|
242
|
+
private cryptoProvider?;
|
|
243
|
+
private secretStore?;
|
|
244
|
+
private audit?;
|
|
245
|
+
private auditWriter?;
|
|
246
|
+
private readonly env;
|
|
247
|
+
private readonly objectName;
|
|
248
|
+
private readonly registry;
|
|
249
|
+
/** In-memory fallback when no engine is wired. */
|
|
250
|
+
private readonly memory;
|
|
251
|
+
/** Change subscribers, optionally scoped to a namespace. */
|
|
252
|
+
private readonly subscribers;
|
|
253
|
+
constructor(opts?: SettingsServiceOptions);
|
|
254
|
+
/**
|
|
255
|
+
* Late-bind a data engine and (optionally) an audit sink. Plugins
|
|
256
|
+
* call this from `kernel:ready` once `objectql` is wired so the
|
|
257
|
+
* SettingsService swaps from its in-memory fallback to the real
|
|
258
|
+
* `sys_setting` table without re-registering the service.
|
|
259
|
+
*/
|
|
260
|
+
bindEngine(engine: SettingsEngine, audit?: SettingsAuditSink, extras?: {
|
|
261
|
+
secretStore?: SettingsSecretStore;
|
|
262
|
+
auditWriter?: SettingsAuditWriter;
|
|
263
|
+
cryptoProvider?: _objectstack_spec_contracts.ICryptoProvider;
|
|
264
|
+
}): void;
|
|
265
|
+
/**
|
|
266
|
+
* Cascade priority ranks for lock comparisons (lower = higher
|
|
267
|
+
* precedence). env<global<tenant<user<default. A locked row at a
|
|
268
|
+
* lower rank blocks writes at all higher ranks.
|
|
269
|
+
*/
|
|
270
|
+
private scopeRank;
|
|
271
|
+
/**
|
|
272
|
+
* Subscribe to `settings:changed` events. When `namespace` is set the
|
|
273
|
+
* handler only fires for that namespace, otherwise it fires for every
|
|
274
|
+
* mutation across the service.
|
|
275
|
+
*
|
|
276
|
+
* Returns an idempotent unsubscribe handle — call it from the
|
|
277
|
+
* consumer's shutdown hook to avoid leaks.
|
|
278
|
+
*/
|
|
279
|
+
subscribe(namespace: string | undefined, handler: SettingsChangeHandler): SettingsUnsubscribe;
|
|
280
|
+
/**
|
|
281
|
+
* Dispatch a change event to all matching subscribers. Errors thrown
|
|
282
|
+
* by a handler are swallowed to keep the bus crash-safe — handlers
|
|
283
|
+
* are expected to enqueue async work themselves.
|
|
284
|
+
*/
|
|
285
|
+
private emitChange;
|
|
286
|
+
/** Register (or replace) a manifest. Idempotent. */
|
|
287
|
+
registerManifest(manifest: SettingsManifest): void;
|
|
288
|
+
/** Look up a manifest, or throw `UnknownNamespaceError`. */
|
|
289
|
+
getManifest(namespace: string): SettingsManifest;
|
|
290
|
+
/** List all registered manifests, optionally filtered by permission. */
|
|
291
|
+
listManifests(ctx?: SettingsContext): SettingsManifest[];
|
|
292
|
+
/** Register a handler for an `action_button` declared in a manifest. */
|
|
293
|
+
registerAction(namespace: string, actionId: string, handler: SettingsActionHandler): void;
|
|
294
|
+
/** Resolve a single key. */
|
|
295
|
+
get<T = unknown>(namespace: string, key: string, ctx?: SettingsContext): Promise<ResolvedSettingValue<T>>;
|
|
296
|
+
/** Resolve every value in a namespace + return the manifest. */
|
|
297
|
+
getNamespace(namespace: string, ctx?: SettingsContext): Promise<SettingsNamespacePayload>;
|
|
298
|
+
/**
|
|
299
|
+
* Build a reactive `ISettingsClient` for a namespace.
|
|
300
|
+
*
|
|
301
|
+
* The client maintains an internal snapshot of the resolved values,
|
|
302
|
+
* refreshing on every `settings:changed` event for the namespace.
|
|
303
|
+
* Consumers call `current` / `get(key)` for synchronous reads and
|
|
304
|
+
* register handlers via `onChange()`.
|
|
305
|
+
*
|
|
306
|
+
* `schema` is optional. When supplied, the snapshot is parsed (and
|
|
307
|
+
* defaulted) through the Zod schema on each refresh — this gives
|
|
308
|
+
* plugins strong types and runtime validation in one call. When
|
|
309
|
+
* absent, raw resolved values flow through unchanged (used by the
|
|
310
|
+
* dynamic console UI which validates per-field).
|
|
311
|
+
*/
|
|
312
|
+
createClient<T extends Record<string, unknown> = Record<string, unknown>>(namespace: string, opts?: {
|
|
313
|
+
ctx?: SettingsContext;
|
|
314
|
+
parse?: (raw: Record<string, unknown>) => T;
|
|
315
|
+
}): Promise<{
|
|
316
|
+
readonly namespace: string;
|
|
317
|
+
readonly current: T;
|
|
318
|
+
get<K extends keyof T>(key: K): T[K];
|
|
319
|
+
onChange(handler: SettingsChangeHandler): SettingsUnsubscribe;
|
|
320
|
+
refresh(): Promise<void>;
|
|
321
|
+
dispose(): void;
|
|
322
|
+
}>;
|
|
323
|
+
private snapshotOf;
|
|
324
|
+
/** Persist a single key. Throws SettingsLockedError when env-locked. */
|
|
325
|
+
set(namespace: string, key: string, value: unknown, ctx?: SettingsContext): Promise<ResolvedSettingValue>;
|
|
326
|
+
/** Persist multiple keys atomically (best-effort). */
|
|
327
|
+
setMany(namespace: string, patch: Record<string, unknown>, ctx?: SettingsContext): Promise<Record<string, ResolvedSettingValue>>;
|
|
328
|
+
/** Invoke a declared action (test connection, rotate, …). */
|
|
329
|
+
runAction(namespace: string, actionId: string, payload: unknown, ctx?: SettingsContext): Promise<SettingsActionResult>;
|
|
330
|
+
private loadRows;
|
|
331
|
+
private upsertRow;
|
|
332
|
+
private materialiseRow;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Configuration options for the SettingsServicePlugin. */
|
|
336
|
+
interface SettingsServicePluginOptions {
|
|
337
|
+
/**
|
|
338
|
+
* Pre-register these manifests at boot. When omitted, the bundled
|
|
339
|
+
* builtin manifests (mail / branding / feature_flags) are loaded so
|
|
340
|
+
* a host gets a working Settings hub out of the box. Pass an empty
|
|
341
|
+
* array to opt out entirely.
|
|
342
|
+
*/
|
|
343
|
+
manifests?: SettingsManifest[];
|
|
344
|
+
/** Override the default crypto adapter. */
|
|
345
|
+
crypto?: CryptoAdapter;
|
|
346
|
+
/**
|
|
347
|
+
* Phase 3 KMS hook. When provided, encrypted specifier values are
|
|
348
|
+
* routed through this provider into `sys_secret`; `sys_setting.value_enc`
|
|
349
|
+
* holds the handle id only. Defaults to `InMemoryCryptoProvider`
|
|
350
|
+
* (NOT suitable for production secrets — replace with an AWS / GCP
|
|
351
|
+
* KMS-backed implementation).
|
|
352
|
+
*/
|
|
353
|
+
cryptoProvider?: ICryptoProvider;
|
|
354
|
+
/** Override the default base path (`/api/settings`). */
|
|
355
|
+
basePath?: string;
|
|
356
|
+
/** Disable REST route registration. */
|
|
357
|
+
registerRoutes?: boolean;
|
|
358
|
+
/** Override the env source. Defaults to `process.env`. */
|
|
359
|
+
env?: Record<string, string | undefined>;
|
|
360
|
+
/**
|
|
361
|
+
* Action handlers to register at boot, keyed by namespace and action
|
|
362
|
+
* id. The bundled `mail.test` handler is registered automatically
|
|
363
|
+
* unless this object is provided.
|
|
364
|
+
*/
|
|
365
|
+
actionHandlers?: Record<string, Record<string, SettingsActionHandler>>;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* SettingsServicePlugin — wires the SettingsService into the kernel.
|
|
369
|
+
*
|
|
370
|
+
* 1. `init`: instantiate the service, register it under `'settings'`,
|
|
371
|
+
* and ship `sys_setting` to the manifest service so the engine
|
|
372
|
+
* auto-provisions the table.
|
|
373
|
+
* 2. `start` → `kernel:ready`: bind the data engine (when present),
|
|
374
|
+
* wire the audit sink (when present), mount REST routes.
|
|
375
|
+
*/
|
|
376
|
+
declare class SettingsServicePlugin implements Plugin {
|
|
377
|
+
name: string;
|
|
378
|
+
version: string;
|
|
379
|
+
type: "standard";
|
|
380
|
+
private readonly opts;
|
|
381
|
+
private service;
|
|
382
|
+
constructor(opts?: SettingsServicePluginOptions);
|
|
383
|
+
init(ctx: PluginContext): Promise<void>;
|
|
384
|
+
start(ctx: PluginContext): Promise<void>;
|
|
385
|
+
/** Glue an `engine.insert('sys_audit_log', …)` audit sink. */
|
|
386
|
+
private buildAuditSink;
|
|
387
|
+
/**
|
|
388
|
+
* Phase 3: build a `sys_secret`-backed implementation of
|
|
389
|
+
* `SettingsSecretStore`. The store bypasses the tenant audit
|
|
390
|
+
* warning because secrets are scoped through their owning
|
|
391
|
+
* `sys_setting` row (which already carries the tenant context).
|
|
392
|
+
*/
|
|
393
|
+
private buildSecretStore;
|
|
394
|
+
/**
|
|
395
|
+
* Phase 3: append-only writer for `sys_setting_audit`. Failures here
|
|
396
|
+
* MUST NOT abort the settings write, so all calls are wrapped in a
|
|
397
|
+
* try/catch and reported through the plugin logger.
|
|
398
|
+
*/
|
|
399
|
+
private buildAuditWriter;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* REST surface for the SettingsService — see ADR-0007 §REST.
|
|
404
|
+
*
|
|
405
|
+
* GET /api/settings → visible manifests
|
|
406
|
+
* GET /api/settings/:namespace → { manifest, values }
|
|
407
|
+
* PUT /api/settings/:namespace → batch upsert
|
|
408
|
+
* POST /api/settings/:namespace/:actionId → invoke declared action
|
|
409
|
+
*
|
|
410
|
+
* The route layer is a thin wrapper that maps thrown service errors
|
|
411
|
+
* into proper HTTP status codes; all business logic lives in
|
|
412
|
+
* `SettingsService`.
|
|
413
|
+
*/
|
|
414
|
+
|
|
415
|
+
interface SettingsRoutesOptions {
|
|
416
|
+
/** Base path. Default `/api/settings`. */
|
|
417
|
+
basePath?: string;
|
|
418
|
+
/**
|
|
419
|
+
* Extract caller identity from the request. The default reads
|
|
420
|
+
* `x-user-id` / `x-tenant-id` headers and parses
|
|
421
|
+
* `x-permissions` as a comma-separated list — fine for dev and
|
|
422
|
+
* straightforward to override in production wiring.
|
|
423
|
+
*/
|
|
424
|
+
contextFromRequest?: (req: IHttpRequest) => SettingsContext;
|
|
425
|
+
}
|
|
426
|
+
declare function registerSettingsRoutes(http: IHttpServer, service: SettingsService, opts?: SettingsRoutesOptions): void;
|
|
427
|
+
|
|
428
|
+
declare const SETTINGS_PLUGIN_ID = "com.objectstack.service.settings";
|
|
429
|
+
declare const SETTINGS_PLUGIN_VERSION = "0.1.0";
|
|
430
|
+
/** Objects owned by service-settings. Currently just the K/V store. */
|
|
431
|
+
declare const settingsObjects: any[];
|
|
432
|
+
/** Manifest header shared by compile-time config and runtime registration. */
|
|
433
|
+
declare const settingsPluginManifestHeader: {
|
|
434
|
+
id: string;
|
|
435
|
+
namespace: string;
|
|
436
|
+
version: string;
|
|
437
|
+
type: "plugin";
|
|
438
|
+
scope: "project";
|
|
439
|
+
name: string;
|
|
440
|
+
description: string;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
/** Mail Delivery — SMTP / API provider configuration. */
|
|
444
|
+
declare const mailSettingsManifest: SettingsManifest;
|
|
445
|
+
/** Built-in action handler stub for `mail/test`. */
|
|
446
|
+
declare const mailTestActionHandler: SettingsActionHandler;
|
|
447
|
+
|
|
448
|
+
/** Branding — workspace identity (name, logo, theme). */
|
|
449
|
+
declare const brandingSettingsManifest: SettingsManifest;
|
|
450
|
+
|
|
451
|
+
/** Feature Flags — opt into experimental capabilities. */
|
|
452
|
+
declare const featureFlagsSettingsManifest: SettingsManifest;
|
|
453
|
+
|
|
454
|
+
/** File Storage — local FS / S3-compatible backend configuration. */
|
|
455
|
+
declare const storageSettingsManifest: SettingsManifest;
|
|
456
|
+
/**
|
|
457
|
+
* Built-in fallback action handler for `storage/test`. The real
|
|
458
|
+
* implementation lives in `@objectstack/service-storage` and is
|
|
459
|
+
* registered by `StorageServicePlugin` on `kernel:ready` (it overrides
|
|
460
|
+
* this stub via `registerAction`). This fallback only validates the
|
|
461
|
+
* form so the button is still useful when the storage plugin is
|
|
462
|
+
* absent (e.g. in a unit-test kernel that mounts settings only).
|
|
463
|
+
*/
|
|
464
|
+
declare const storageTestActionHandler: SettingsActionHandler;
|
|
465
|
+
|
|
466
|
+
/** Reference manifests bundled with service-settings. */
|
|
467
|
+
|
|
468
|
+
/** Convenience aggregate — pass to `SettingsServicePlugin({ manifests })`. */
|
|
469
|
+
declare const builtinSettingsManifests: {
|
|
470
|
+
namespace: string;
|
|
471
|
+
version: number;
|
|
472
|
+
label: string;
|
|
473
|
+
scope: "global" | "tenant" | "user";
|
|
474
|
+
readPermission: string;
|
|
475
|
+
writePermission: string;
|
|
476
|
+
specifiers: {
|
|
477
|
+
type: "number" | "group" | "info_banner" | "child_pane" | "title_value" | "action_button" | "email" | "phone" | "password" | "text" | "url" | "json" | "textarea" | "toggle" | "select" | "multiselect" | "radio" | "color" | "slider";
|
|
478
|
+
label: string;
|
|
479
|
+
required: boolean;
|
|
480
|
+
id?: string | undefined;
|
|
481
|
+
key?: string | undefined;
|
|
482
|
+
description?: string | undefined;
|
|
483
|
+
icon?: string | undefined;
|
|
484
|
+
default?: unknown;
|
|
485
|
+
visible?: {
|
|
486
|
+
dialect: "cel" | "js" | "cron" | "template";
|
|
487
|
+
source?: string | undefined;
|
|
488
|
+
ast?: unknown;
|
|
489
|
+
meta?: {
|
|
490
|
+
rationale?: string | undefined;
|
|
491
|
+
generatedBy?: string | undefined;
|
|
492
|
+
} | undefined;
|
|
493
|
+
} | {
|
|
494
|
+
dialect: "cel" | "js" | "cron" | "template";
|
|
495
|
+
source?: string | undefined;
|
|
496
|
+
ast?: unknown;
|
|
497
|
+
meta?: {
|
|
498
|
+
rationale?: string | undefined;
|
|
499
|
+
generatedBy?: string | undefined;
|
|
500
|
+
} | undefined;
|
|
501
|
+
} | undefined;
|
|
502
|
+
encrypted?: boolean | undefined;
|
|
503
|
+
scope?: "global" | "tenant" | "user" | undefined;
|
|
504
|
+
availableScopes?: ("global" | "tenant" | "user")[] | undefined;
|
|
505
|
+
lockable?: boolean | undefined;
|
|
506
|
+
readPermission?: string | undefined;
|
|
507
|
+
writePermission?: string | undefined;
|
|
508
|
+
deprecated?: boolean | undefined;
|
|
509
|
+
replacedBy?: string | undefined;
|
|
510
|
+
options?: {
|
|
511
|
+
value: string | number | boolean;
|
|
512
|
+
label: string;
|
|
513
|
+
description?: string | undefined;
|
|
514
|
+
icon?: string | undefined;
|
|
515
|
+
}[] | undefined;
|
|
516
|
+
min?: number | undefined;
|
|
517
|
+
max?: number | undefined;
|
|
518
|
+
step?: number | undefined;
|
|
519
|
+
minLength?: number | undefined;
|
|
520
|
+
maxLength?: number | undefined;
|
|
521
|
+
pattern?: string | undefined;
|
|
522
|
+
rows?: number | undefined;
|
|
523
|
+
handler?: {
|
|
524
|
+
kind: "http";
|
|
525
|
+
method: "GET" | "PUT" | "POST" | "DELETE" | "PATCH";
|
|
526
|
+
url: string;
|
|
527
|
+
body?: Record<string, unknown> | undefined;
|
|
528
|
+
confirmText?: string | undefined;
|
|
529
|
+
} | {
|
|
530
|
+
kind: "action";
|
|
531
|
+
name: string;
|
|
532
|
+
params?: Record<string, unknown> | undefined;
|
|
533
|
+
confirmText?: string | undefined;
|
|
534
|
+
} | {
|
|
535
|
+
kind: "navigate";
|
|
536
|
+
url: string;
|
|
537
|
+
target: "_self" | "_blank";
|
|
538
|
+
} | undefined;
|
|
539
|
+
childNamespace?: string | undefined;
|
|
540
|
+
bannerText?: string | undefined;
|
|
541
|
+
bannerSeverity?: "error" | "success" | "info" | "warning" | undefined;
|
|
542
|
+
}[];
|
|
543
|
+
icon?: string | undefined;
|
|
544
|
+
description?: string | undefined;
|
|
545
|
+
helpText?: string | undefined;
|
|
546
|
+
category?: string | undefined;
|
|
547
|
+
order?: number | undefined;
|
|
548
|
+
visible?: {
|
|
549
|
+
dialect: "cel" | "js" | "cron" | "template";
|
|
550
|
+
source?: string | undefined;
|
|
551
|
+
ast?: unknown;
|
|
552
|
+
meta?: {
|
|
553
|
+
rationale?: string | undefined;
|
|
554
|
+
generatedBy?: string | undefined;
|
|
555
|
+
} | undefined;
|
|
556
|
+
} | {
|
|
557
|
+
dialect: "cel" | "js" | "cron" | "template";
|
|
558
|
+
source?: string | undefined;
|
|
559
|
+
ast?: unknown;
|
|
560
|
+
meta?: {
|
|
561
|
+
rationale?: string | undefined;
|
|
562
|
+
generatedBy?: string | undefined;
|
|
563
|
+
} | undefined;
|
|
564
|
+
} | undefined;
|
|
565
|
+
featureFlag?: string | undefined;
|
|
566
|
+
beta?: boolean | undefined;
|
|
567
|
+
}[];
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* English (en) — built-in settings manifest translations.
|
|
571
|
+
*
|
|
572
|
+
* Mirrors literals in `manifests/{mail,branding,feature-flags,storage}.manifest.ts`.
|
|
573
|
+
* Keeping them explicit here lets the resolver chain (locale → fallback → literal)
|
|
574
|
+
* always have at least an English entry to fall back to.
|
|
575
|
+
*/
|
|
576
|
+
declare const en: TranslationData;
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* 简体中文 (zh-CN) — built-in settings manifest translations.
|
|
580
|
+
*/
|
|
581
|
+
declare const zhCN: TranslationData;
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* 日本語 (ja-JP) — built-in settings manifest translations.
|
|
585
|
+
*/
|
|
586
|
+
declare const jaJP: TranslationData;
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Built-in Settings translations.
|
|
590
|
+
*
|
|
591
|
+
* Mirrors the CRM example's `src/translations/{en,zh-CN,ja-JP}.ts` convention —
|
|
592
|
+
* one file per locale, aggregated into a `TranslationBundle` here.
|
|
593
|
+
*
|
|
594
|
+
* Hosts merge `settingsBuiltinTranslations` into the i18next resource tree
|
|
595
|
+
* under whatever namespace makes sense (the console wires it as `system`),
|
|
596
|
+
* making keys resolvable as `<ns>.settings.<namespace>.{title,description,...}`.
|
|
597
|
+
*/
|
|
598
|
+
|
|
599
|
+
declare const settingsBuiltinTranslations: TranslationBundle;
|
|
600
|
+
|
|
601
|
+
export { type CryptoAdapter, NoopCryptoAdapter, SETTINGS_PLUGIN_ID, SETTINGS_PLUGIN_VERSION, type SettingsActionHandler, type SettingsAuditSink, type SettingsContext, type SettingsEngine, SettingsLockedError, type SettingsRoutesOptions, type SettingsRow, SettingsService, type SettingsServiceOptions, SettingsServicePlugin, type SettingsServicePluginOptions, UnknownKeyError, UnknownNamespaceError, brandingSettingsManifest, builtinSettingsManifests, envKeyOf, featureFlagsSettingsManifest, mailSettingsManifest, mailTestActionHandler, registerSettingsRoutes, settingsBuiltinTranslations, settingsObjects, settingsPluginManifestHeader, en as settingsTranslationsEn, jaJP as settingsTranslationsJaJP, zhCN as settingsTranslationsZhCN, storageSettingsManifest, storageTestActionHandler };
|