@anteros/core 0.0.39 → 0.0.41

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 (64) hide show
  1. package/database/audit.ts +193 -0
  2. package/database/collection.ts +4 -4
  3. package/database/deleteLog.ts +34 -0
  4. package/database/field.crypto.ts +1 -1
  5. package/database/file.ts +3 -3
  6. package/database/mongodbadapter.ts +321 -147
  7. package/database/replication.ts +780 -0
  8. package/database/schema.ts +140 -125
  9. package/database/tenant.ts +4 -0
  10. package/database/vars.ts +405 -0
  11. package/database/workflow.ts +6 -6
  12. package/index.ts +10 -0
  13. package/lib/audit.ts +271 -0
  14. package/lib/auditResult.ts +120 -0
  15. package/lib/define.ts +20 -1
  16. package/lib/files.ts +64 -11
  17. package/lib/lifecycle.ts +141 -0
  18. package/lib/redact.ts +117 -0
  19. package/lib/workflow.ts +1 -1
  20. package/package.json +1 -1
  21. package/server/api.ts +134 -33
  22. package/server/boot.ts +185 -53
  23. package/server/flags.ts +136 -0
  24. package/server/hono.ts +1 -1
  25. package/tests/audit-file.test.ts +296 -0
  26. package/tests/audit-operations.test.ts +184 -0
  27. package/tests/audit-redact.test.ts +233 -0
  28. package/tests/audit-results.test.ts +188 -0
  29. package/tests/audit-retention-config.test.ts +76 -0
  30. package/tests/audit-run-action.test.ts +120 -0
  31. package/tests/audit.test.ts +320 -0
  32. package/tests/crypto.test.ts +17 -17
  33. package/tests/db-cache.test.ts +7 -7
  34. package/tests/field-encryption.test.ts +25 -25
  35. package/tests/file-collection-replication.test.ts +98 -0
  36. package/tests/fixtures/index-tenant/collections/variants.model.ts +2 -2
  37. package/tests/fixtures/lc-disabled/lifecycle.ts +11 -0
  38. package/tests/fixtures/lc-hang/lifecycle.ts +13 -0
  39. package/tests/fixtures/lc-normal/lifecycle.ts +23 -0
  40. package/tests/fixtures/lc-throw/lifecycle.ts +6 -0
  41. package/tests/fixtures/repl-only/app.ts +26 -0
  42. package/tests/fixtures/repl-only/tenant/collections/orders.model.ts +14 -0
  43. package/tests/fixtures/repl-only/tenant/lifecycle.ts +14 -0
  44. package/tests/fixtures/repl-only/tenant/scripts/probe.run.ts +8 -0
  45. package/tests/fixtures/vars-tenant/vars/config.var.ts +23 -0
  46. package/tests/fixtures/vars-tenant/vars/private.var.ts +6 -0
  47. package/tests/index-spec.test.ts +5 -5
  48. package/tests/lifecycle.test.ts +96 -0
  49. package/tests/replication-control.test.ts +128 -0
  50. package/tests/replication-initial-sync.test.ts +86 -0
  51. package/tests/replication-only.test.ts +358 -0
  52. package/tests/replication.test.ts +188 -0
  53. package/tests/upload.test.ts +239 -0
  54. package/tests/vars-api.test.ts +123 -0
  55. package/tests/vars-replication.test.ts +95 -0
  56. package/tests/vars.test.ts +216 -0
  57. package/types/collection.d.ts +3 -0
  58. package/types/config.d.ts +64 -0
  59. package/types/file.d.ts +4 -1
  60. package/types/lifecycle.d.ts +71 -0
  61. package/types/replication.d.ts +199 -0
  62. package/types/tenant.d.ts +23 -0
  63. package/types/vars.d.ts +147 -0
  64. package/utils/func.ts +67 -52
@@ -8,6 +8,7 @@ import type { useRest } from "../database/rest";
8
8
  import type { jwt } from "../utils/func";
9
9
  import type { ActionsApiList, ApiAccess, ApiActions } from "./api";
10
10
  import type { Server as SocketIO } from "socket.io";
11
+ import type { CollectionReplicationConfig } from "./replication";
11
12
 
12
13
  /** Context passed to a collection custom action (`define.Action`). */
13
14
  export type CollectionActionContext = {
@@ -85,6 +86,8 @@ export type Collection = {
85
86
  /** Default TTL for cached queries — human string ('5m', '2h') or ms */
86
87
  ttl?: string | number;
87
88
  };
89
+ /** Opt-in one-way replication to destination databases — see `ReplicationConfig`. */
90
+ replication?: CollectionReplicationConfig;
88
91
  /**
89
92
  * The tenant id of the collection
90
93
  * @type {string}
package/types/config.d.ts CHANGED
@@ -7,6 +7,8 @@ import type { Service } from "./service";
7
7
  import type { Script } from "./scripts";
8
8
  import type { FileCollection } from "./file";
9
9
  import type { McpTool, McpResource } from "./mcp";
10
+ import type { VarDefinition } from "./vars";
11
+ import type { AuditFileConfig } from "../lib/audit";
10
12
  export type ServerConfig = {
11
13
  debug?: boolean;
12
14
  version?: string;
@@ -21,9 +23,70 @@ export type ServerConfig = {
21
23
  metricsPort?: number;
22
24
  /** When true, trusts proxy headers (CF-Connecting-IP / X-Forwarded-For) as the client IP. Default: false. */
23
25
  trustProxy?: boolean;
26
+ /**
27
+ * Audit trail defaults for every tenant. A tenant declaring its own
28
+ * `tenant.audit` always wins (even `false`).
29
+ */
30
+ audit?: {
31
+ /**
32
+ * How long `_audit_` entries are kept (MongoDB TTL on `ts`).
33
+ * Duration string: `'90d'`, `'24h'`.
34
+ * `false` → explicitly disabled (drops the TTL index if one exists).
35
+ * Omitted → untouched: entries are kept forever.
36
+ */
37
+ retention?: string | false;
38
+ /**
39
+ * Local append-only JSONL copy of the audit trail (one file per day,
40
+ * rotated on size, pruned by age).
41
+ * `true` → defaults, `string` → directory, object → full options.
42
+ * `false` / omitted → disabled.
43
+ */
44
+ file?: AuditFileConfig;
45
+ /**
46
+ * Object keys whose value is replaced by `"[redacted]"` in every audit
47
+ * entry (`operation.input`, `operation.result` and `meta`), at any depth,
48
+ * case-insensitively. `false` → disabled.
49
+ *
50
+ * Default: password, passwd, pwd, token, authorization, apikey, api_key,
51
+ * key, secret, cookie, otp, pin.
52
+ */
53
+ redact?: string[] | false;
54
+ /**
55
+ * What an audit entry keeps of the operation's **result**
56
+ * (default: `'summary'`).
57
+ *
58
+ * - `'summary'` (default) → the **generated identifiers** (`_id`,
59
+ * `insertedIds`) plus the operational counters (`matchedCount`,
60
+ * `modifiedCount`, `deletedCount`, `nIndexesWas`…). Never a document:
61
+ * the data lives in the collection (and in its replications).
62
+ * - `'none'` → the generated identifiers only.
63
+ * - `'full'` → the raw result, documents included.
64
+ *
65
+ * Reads (`find`, `findOne`, `aggregate`, streams, `watch`) and custom
66
+ * actions/services never store a result, whatever this setting.
67
+ */
68
+ results?: 'none' | 'summary' | 'full';
69
+ };
24
70
  body?: {
25
71
  maxSize?: number;
26
72
  };
73
+ /**
74
+ * Boot mode (default: `'full'`).
75
+ *
76
+ * - `'full'` — HTTP API **and** the replication engine.
77
+ * - `'replication-only'` — a process dedicated to the replication engine: no
78
+ * HTTP API, no websocket handlers, no scripts, no services, no MCP tools,
79
+ * no middlewares. Loaded: tenant databases, collections, file collections,
80
+ * variables and lifecycle hooks (`beforeBoot` / `onDestroy`).
81
+ * - `'no-replication'` — a full server that never starts the replication
82
+ * engine (no scheduling, no tombstones, no `_replication_` state).
83
+ * - `'api-only'` — the HTTP API and nothing around it: no replication, no
84
+ * scripts, no sockets.
85
+ *
86
+ * Flags always win over this setting: `--replication-only`, `--no-replication`,
87
+ * `--api-only`, `--no-scripts`, `--no-sockets`.
88
+ */
89
+ mode?: 'full' | 'replication-only' | 'no-replication' | 'api-only';
27
90
  cors?: {
28
91
  origin: string | string[] | ((ctx: { origin: string, c: Context }) => string | string[]);
29
92
  credentials?: boolean;
@@ -123,4 +186,5 @@ export type Config = ServerConfig & {
123
186
  fileCollections?: FileCollection[]
124
187
  mcpTools?: McpTool[]
125
188
  mcpResources?: McpResource[]
189
+ vars?: VarDefinition[]
126
190
  }
package/types/file.d.ts CHANGED
@@ -4,6 +4,7 @@ import type { HooksCollection } from "./hook";
4
4
  import type Joi from "joi";
5
5
  import type { useRest } from "../database/rest";
6
6
  import type { jwt } from "../utils/func";
7
+ import type { CollectionReplicationConfig } from "./replication";
7
8
 
8
9
  type FileAccessHandler = (ctx: {
9
10
  rest: InstanceType<typeof useRest>;
@@ -42,6 +43,8 @@ export type FileCollection = {
42
43
  };
43
44
  /** Metadata fields stored alongside the file document */
44
45
  fields?: Field[];
46
+ /** Opt this file collection into the data replication engine (its documents). */
47
+ replication?: CollectionReplicationConfig;
45
48
  /** Upload validation rules */
46
49
  upload?: {
47
50
  /** Allowed MIME types for upload.
@@ -103,7 +106,7 @@ export type FileCollection = {
103
106
  /** Private key path for SSH auth */
104
107
  privateKey?: string;
105
108
  }[];
106
- /** Enregistrer les métadonnées du fichier en base. Défaut: true */
109
+ /** Save the file metadata in the database. Default: true */
107
110
  trackMetaData?: boolean;
108
111
  api?: {
109
112
  access?: FileApiAccess;
@@ -0,0 +1,71 @@
1
+ import type { useRest } from "../database/rest";
2
+ import type { Tenant } from "./tenant";
3
+ import type { Config } from "./config";
4
+ import type { logger } from "../utils/logger";
5
+ import type { Server as SocketIO } from "socket.io";
6
+ import type { TenantReplicationApi } from "./replication";
7
+
8
+ /** Signal that triggered the shutdown. */
9
+ export type LifecycleReason = "SIGINT" | "SIGTERM";
10
+
11
+ /** Base context shared by every lifecycle hook. `rest` is tenant-scoped. */
12
+ export type LifecycleContext = {
13
+ tenant: Tenant;
14
+ rest: InstanceType<typeof useRest>;
15
+ logger: typeof logger;
16
+ cfg: Config;
17
+ /** Tenant-scoped replication API — `reset()`, `seed()`, `now()`, `state()` (no tenant id needed). */
18
+ replication: TenantReplicationApi;
19
+ };
20
+
21
+ /** Context of `beforeBoot` — runs before replication and before the HTTP server listens. */
22
+ export type LifecycleBootContext = LifecycleContext;
23
+
24
+ /** Context of `afterBoot` — runs once the server is listening. */
25
+ export type LifecycleReadyContext = LifecycleContext & {
26
+ io: SocketIO;
27
+ server: ReturnType<typeof Bun.serve>;
28
+ };
29
+
30
+ /** Context of `onDestroy` — runs on SIGINT/SIGTERM, before replication stops and DB clients close. */
31
+ export type LifecycleDestroyContext = LifecycleContext & {
32
+ reason: LifecycleReason;
33
+ };
34
+
35
+ /**
36
+ * Per-tenant lifecycle — one `lifecycle.ts` at the tenant root.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * export default define.Lifecycle({
41
+ * beforeBoot: async ({ rest, tenant }) => { await rest.insertOne('settings', { tenant: tenant.id }) },
42
+ * afterBoot: async ({ server, logger }) => { logger.info(`ready on :${server.port}`) },
43
+ * onDestroy: async ({ reason }) => { await flushSomething(reason) },
44
+ * })
45
+ * ```
46
+ */
47
+ export type Lifecycle = {
48
+ _isLifecycle_?: boolean;
49
+ /** Injected by the loader — the tenant this lifecycle belongs to. */
50
+ _tenant_?: string;
51
+ /** Turn this lifecycle off without removing the file (default: true). */
52
+ enabled?: boolean;
53
+ /**
54
+ * Blocking — runs after databases are connected and collections are loaded,
55
+ * before replication starts and before the HTTP server listens. Ideal for
56
+ * migrations, seeding, warmup. Throwing aborts the boot.
57
+ */
58
+ beforeBoot?: (ctx: LifecycleBootContext) => Promise<void> | void;
59
+ /**
60
+ * Runs once the server is listening (after `Bun.serve`). Ideal for
61
+ * notifications and cache warming. Errors are logged, never fatal.
62
+ */
63
+ afterBoot?: (ctx: LifecycleReadyContext) => Promise<void> | void;
64
+ /**
65
+ * Runs on SIGINT/SIGTERM, before replication stops and tenant DB clients
66
+ * close — so `rest` is still usable. Errors are logged; shutdown proceeds.
67
+ */
68
+ onDestroy?: (ctx: LifecycleDestroyContext) => Promise<void> | void;
69
+ /** Max time (ms) allowed for `onDestroy` before shutdown proceeds anyway (default: 10000). */
70
+ destroyTimeout?: number;
71
+ };
@@ -0,0 +1,199 @@
1
+ import type { MongoClientOptions } from "mongodb";
2
+
3
+ /** When to run replication — either a cron expression or a fixed interval. */
4
+ export type ReplicationSchedule =
5
+ | { cron: string }
6
+ | { interval: string | number };
7
+
8
+ /**
9
+ * Initial synchronization mode — how replication starts when no `_replication_`
10
+ * state exists yet.
11
+ * - `"full"` — backfill everything (default)
12
+ * - `"skip"` — ignore existing documents, start from now
13
+ * - `"latest"` — start from the latest `key` in the source
14
+ * - ISO string / `Date` — start from this date
15
+ */
16
+ export type ReplicationInitialSync = "full" | "skip" | "latest" | (string & {}) | Date;
17
+
18
+ /** A destination database that receives replicated documents. */
19
+ export type ReplicationDestination = {
20
+ /** Unique identifier — used as the `_replication_` state key (required). */
21
+ id: string;
22
+ /** MongoDB connection string (required). */
23
+ uri: string;
24
+ /** Database name override — defaults to the name in the URI. */
25
+ name?: string;
26
+ /** Extra MongoDB client options (pool size, timeouts…). */
27
+ options?: MongoClientOptions;
28
+ /** Restrict replication to these collection slugs (default: all enabled). */
29
+ collections?: string[];
30
+ /** Turn this destination off without removing it. */
31
+ enabled?: boolean;
32
+ };
33
+
34
+ /** Per-tenant replication configuration (source → destination, one-way). */
35
+ export type ReplicationConfig = {
36
+ /** Enable/disable replication for this scope (default: true when `destinations` is non-empty). */
37
+ enabled?: boolean;
38
+ /** Scheduling — `{ cron }` or `{ interval }` (default: every 5 minutes). */
39
+ schedule?: ReplicationSchedule;
40
+ /** Date key used for incremental replication (default: `'updatedAt'`). */
41
+ key?: string;
42
+ /**
43
+ * Rewind the cursor by this many milliseconds on each run, so documents
44
+ * written slightly out of order (clock skew) are not missed (default: 0).
45
+ */
46
+ lookback?: number;
47
+ /** Documents transferred per batch (default: 1000). */
48
+ batchSize?: number;
49
+ /** Run a first replication right after boot (default: true). */
50
+ runOnBoot?: boolean;
51
+ /**
52
+ * How the initial synchronization starts when no `_replication_` state exists
53
+ * yet (default: `'full'`). Use `'skip'` or `'latest'` to avoid the full
54
+ * backfill when the destination is already pre-loaded (e.g. via `mongorestore`).
55
+ */
56
+ initialSync?: ReplicationInitialSync;
57
+ /** Destination databases (required). */
58
+ destinations: ReplicationDestination[];
59
+ };
60
+
61
+ /** Per-collection opt-in replication. */
62
+ export type CollectionReplicationConfig = {
63
+ /** Opt this collection into replication (required). */
64
+ enabled: boolean;
65
+ /** Date key override — defaults to `replication.key`. */
66
+ key?: string;
67
+ /** Initial sync override — defaults to `replication.initialSync`. */
68
+ initialSync?: ReplicationInitialSync;
69
+ };
70
+
71
+ export type ReplicationStatus = "idle" | "running" | "success" | "error";
72
+
73
+ /** Cursor of the last replicated document — `value` is the date key, `id` the tie-breaker. */
74
+ export type ReplicationCursor = {
75
+ value: Date | null;
76
+ id: any;
77
+ };
78
+
79
+ export type ReplicationStats = {
80
+ inserted: number;
81
+ updated: number;
82
+ deleted: number;
83
+ failed: number;
84
+ batches: number;
85
+ durationMs: number;
86
+ };
87
+
88
+ /** State document persisted in `_replication_`, one per (tenant, destination, collection). */
89
+ export type ReplicationState = {
90
+ _id: string;
91
+ type: "state";
92
+ tenant: string;
93
+ destination: string;
94
+ collection: string;
95
+ key: string;
96
+ cursor: ReplicationCursor;
97
+ status: ReplicationStatus;
98
+ lastRunAt: Date | null;
99
+ lastError: string | null;
100
+ stats: ReplicationStats;
101
+ createdAt?: Date;
102
+ updatedAt: Date;
103
+ };
104
+
105
+ /** Tombstone document persisted in `_replication_` when a replicated document is deleted. */
106
+ export type ReplicationDeleteMarker = {
107
+ _id: string;
108
+ type: "delete";
109
+ tenant: string;
110
+ collection: string;
111
+ docId: string;
112
+ deletedAt: Date;
113
+ /** Destination ids that already applied the delete. */
114
+ synced: string[];
115
+ updatedAt: Date;
116
+ };
117
+
118
+ /** Scope of a `replication.reset()` call. */
119
+ export type ReplicationResetOptions = {
120
+ /** Only reset this destination id (default: every destination). */
121
+ destination?: string;
122
+ /** Only reset this collection slug (default: every replicated collection). */
123
+ collection?: string;
124
+ /**
125
+ * Also delete pending delete tombstones (default: true). Ignored when
126
+ * `destination` is set — tombstones are shared across destinations.
127
+ */
128
+ tombstones?: boolean;
129
+ };
130
+
131
+ /** Result of a `replication.reset()` call. */
132
+ export type ReplicationResetResult = {
133
+ tenant: string;
134
+ /** State documents deleted (one per destination + collection). */
135
+ state: number;
136
+ /** Delete tombstones deleted. */
137
+ tombstones: number;
138
+ };
139
+
140
+ /**
141
+ * Where to (re)start replication from — accepted by `replication.seed()`.
142
+ * - `"now"` — start from now (same as `initialSync: 'skip'`)
143
+ * - `"latest"` — start from the latest `key` in the source
144
+ * - ISO string / `Date` — start from this date
145
+ */
146
+ export type ReplicationSeedValue = "now" | "latest" | (string & {}) | Date;
147
+
148
+ /** Scope of a `replication.seed()` call. */
149
+ export type ReplicationSeedOptions = {
150
+ /** Only seed this destination id (default: every destination). */
151
+ destination?: string;
152
+ /** Only seed this collection slug (default: every replicated collection). */
153
+ collection?: string;
154
+ /** Optional `_id` tie-breaker for the cursor. */
155
+ id?: any;
156
+ };
157
+
158
+ /** Result of a `replication.seed()` call. */
159
+ export type ReplicationSeedResult = {
160
+ tenant: string;
161
+ /** State documents written (one per destination + collection). */
162
+ seeded: number;
163
+ };
164
+
165
+ /** Shape of the `replication` namespace exported by the package (multi-tenant). */
166
+ export type ReplicationApi = {
167
+ start: () => Promise<void>;
168
+ stop: () => Promise<void>;
169
+ now: (tenantId?: string) => Promise<ReplicationRunResult[]>;
170
+ state: (tenantId: string, destinationId?: string, collection?: string) => Promise<ReplicationState[]>;
171
+ reset: (tenantId: string, opts?: ReplicationResetOptions) => Promise<ReplicationResetResult>;
172
+ seed: (tenantId: string, value: ReplicationSeedValue, opts?: ReplicationSeedOptions) => Promise<ReplicationSeedResult>;
173
+ };
174
+
175
+ /**
176
+ * Tenant-scoped replication API — like `rest`, the tenant is implicit. This is
177
+ * the `replication` passed to lifecycle hooks. Engine control (`start`/`stop`)
178
+ * stays on the global `ReplicationApi`.
179
+ */
180
+ export type TenantReplicationApi = {
181
+ now: () => Promise<ReplicationRunResult[]>;
182
+ state: (destinationId?: string, collection?: string) => Promise<ReplicationState[]>;
183
+ reset: (opts?: ReplicationResetOptions) => Promise<ReplicationResetResult>;
184
+ seed: (value: ReplicationSeedValue, opts?: ReplicationSeedOptions) => Promise<ReplicationSeedResult>;
185
+ };
186
+
187
+ /** Outcome of a single (tenant, destination, collection) replication. */
188
+ export type ReplicationRunResult = {
189
+ tenant: string;
190
+ destination: string;
191
+ collection: string;
192
+ inserted: number;
193
+ updated: number;
194
+ deleted: number;
195
+ failed: number;
196
+ batches: number;
197
+ durationMs: number;
198
+ error?: string;
199
+ };
package/types/tenant.d.ts CHANGED
@@ -1,9 +1,32 @@
1
1
  import type { Db, MongoClient, MongoClientOptions } from "mongodb";
2
+ import type { ReplicationConfig } from "./replication";
3
+ import type { AuditFileConfig } from "../lib/audit";
2
4
  export type Tenant = {
3
5
  id: string;
4
6
  name?: string;
5
7
  description?: string;
6
8
  dir: string;
9
+ /** Per-tenant replication — each tenant has its own destinations and schedule. */
10
+ replication?: ReplicationConfig;
11
+ /**
12
+ * Audit trail (`_audit_`) — query indexes are always ensured in the background,
13
+ * retention is opt-in.
14
+ */
15
+ audit?: {
16
+ /**
17
+ * How long audit entries are kept, as a MongoDB TTL index on `ts`.
18
+ * Duration string: `'90d'`, `'24h'`, `'30m'`.
19
+ * `false` → explicitly disabled (drops the TTL index if one exists).
20
+ * Omitted → untouched: entries are kept forever.
21
+ */
22
+ retention?: string | false;
23
+ /**
24
+ * Local append-only JSONL copy of this tenant's audit trail.
25
+ * `true` → defaults, `string` → directory, object → full options.
26
+ * `false` → explicitly disabled (overrides `server.audit.file`).
27
+ */
28
+ file?: AuditFileConfig;
29
+ };
7
30
  routes?: {
8
31
  prefix?: string;
9
32
  },
@@ -0,0 +1,147 @@
1
+ import type { Field } from "./field";
2
+ import type { useRest } from "../database/rest";
3
+ import type { fn } from "../lib/error";
4
+ import type { jwt } from "../utils/func";
5
+
6
+ /** Actions exposed by the variables HTTP API (`POST /vars/:tenant_id/:action`). */
7
+ export type VarsActions =
8
+ | "set"
9
+ | "setMany"
10
+ | "get"
11
+ | "entry"
12
+ | "entries"
13
+ | "all"
14
+ | "del"
15
+ | "has"
16
+ | "incr"
17
+ | "expire"
18
+ | "clear";
19
+
20
+ /** Access rule context for a vars action. */
21
+ export type VarsAccessContext = {
22
+ rest: InstanceType<typeof useRest>;
23
+ /** Namespace targeted by the request. */
24
+ namespace: string;
25
+ /** Raw request body. */
26
+ body: any;
27
+ error: typeof fn.error;
28
+ jwt: typeof jwt;
29
+ token: { value: string | null; decoded: Record<string, unknown> | null; provided: boolean; expired: boolean };
30
+ };
31
+
32
+ /** Per-action access rules — `'*'` wildcard, same pattern as collections. */
33
+ export type VarsAccess = {
34
+ [key in VarsActions | (string & {})]?: boolean | ((ctx: VarsAccessContext) => boolean | Promise<boolean>);
35
+ };
36
+
37
+ /** HTTP API configuration of a vars namespace (declared in `define.Vars`). */
38
+ export type VarsApi = {
39
+ /** Access rules per action — **no rules = denied** (secure by default). */
40
+ access?: VarsAccess;
41
+ };
42
+
43
+ /**
44
+ * Value spec for a variable — a `Field` (minus `name`) plus an optional default
45
+ * TTL. Use it when a variable needs a type / validation / a default value / a TTL:
46
+ *
47
+ * ```ts
48
+ * vars: {
49
+ * maxUsers: 100, // shorthand: raw default
50
+ * licence: { type: 'string', defaultValue: 'RDX00', ttl: '30d' },
51
+ * }
52
+ * ```
53
+ */
54
+ export type VarSpec = Omit<Field, "name"> & {
55
+ /** Default TTL for this variable — human string (`'30m'`, `'2h'`, `'1d'`) or ms. */
56
+ ttl?: string | number;
57
+ };
58
+
59
+ /** Per-namespace replication opt-in (`define.Vars({ replication: { enabled: true } })`). */
60
+ export type VarsReplicationConfig = {
61
+ /** Replicate the `_vars_` documents of THIS namespace. */
62
+ enabled: boolean;
63
+ };
64
+
65
+ /**
66
+ * A tenant variable definition — `{tenant.dir}/vars/**\/*.var.ts`.
67
+ *
68
+ * - `scope` — **one** optional identity field (often a `relationship`): its value
69
+ * is part of the entry key, so each company/branch/environment gets its own value.
70
+ * - `meta` — annotation fields: validated, stored alongside — **never** part of the key.
71
+ * - `vars` — values: a raw default, or a `VarSpec`.
72
+ */
73
+ export type VarDefinition = {
74
+ _isVars_?: boolean;
75
+ /** Injected by the loader — the tenant this definition belongs to. */
76
+ _tenant_?: string;
77
+ /** Namespace grouping these variables (required). */
78
+ namespace: string;
79
+ /** Optional identity field — its value is part of the entry key. */
80
+ scope?: Field;
81
+ /** Annotation fields — validated, never part of the key. */
82
+ meta?: Field[];
83
+ /** Variables — a raw default value, or a `VarSpec`. */
84
+ vars: Record<string, any>;
85
+ /** HTTP API access rules — required to expose this namespace over `POST /vars/:tenant_id/:action`. */
86
+ api?: VarsApi;
87
+ /** Opt this namespace into replication — like a collection, but scoped to this namespace. */
88
+ replication?: VarsReplicationConfig;
89
+ };
90
+
91
+ /** A stored variable entry, as returned by `rest.vars.entry()` / `.entries()`. */
92
+ export type VarEntry<T = any> = {
93
+ ns: string;
94
+ key: string;
95
+ scope: any;
96
+ value: T;
97
+ meta: Record<string, any>;
98
+ expiresAt: Date | null;
99
+ createdAt?: Date;
100
+ updatedAt: Date;
101
+ };
102
+
103
+ /** Scope option shared by every `rest.vars` method. */
104
+ export type VarsOptions = { scope?: any };
105
+
106
+ export type VarsSetOptions = VarsOptions & {
107
+ /** Annotation values — validated against the namespace `meta` fields. */
108
+ meta?: Record<string, any>;
109
+ /** Time-to-live — human string or ms. Pass `null` to clear an existing TTL. */
110
+ ttl?: string | number | null;
111
+ };
112
+
113
+ export type VarsAllOptions = VarsOptions & {
114
+ /** Filter on `meta` fields (e.g. `{ note: 'x' }` → `{ 'meta.note': 'x' }`). */
115
+ where?: Record<string, any>;
116
+ };
117
+
118
+ /**
119
+ * Tenant-scoped key/value store — `rest.vars` (like Redis, MongoDB-backed).
120
+ * The tenant is implicit (like every `rest.*` accessor).
121
+ */
122
+ export type TenantVars = {
123
+ /** Set a value. `ttl` replaces the entry's TTL (omit to store without expiry). */
124
+ set(ns: string, key: string, value: any, opts?: VarsSetOptions): Promise<void>;
125
+ /** Set several keys at once (shared `meta` / `ttl`). */
126
+ setMany(ns: string, entries: Record<string, any>, opts?: VarsSetOptions): Promise<void>;
127
+ /** Read a value — `undefined` when missing or expired. */
128
+ get<T = any>(ns: string, key: string, opts?: VarsOptions): Promise<T | undefined>;
129
+ /** Read the full entry (value + meta + expiry). */
130
+ entry<T = any>(ns: string, key: string, opts?: VarsOptions): Promise<VarEntry<T> | undefined>;
131
+ /** All entries of a namespace (current scope). */
132
+ entries(ns: string, opts?: VarsAllOptions): Promise<VarEntry[]>;
133
+ /** All key/value pairs of a namespace (current scope). */
134
+ all(ns: string, opts?: VarsAllOptions): Promise<Record<string, any>>;
135
+ /** Delete a key — `true` when a key was removed. */
136
+ del(ns: string, key: string, opts?: VarsOptions): Promise<boolean>;
137
+ /** Whether a (non-expired) key exists. */
138
+ has(ns: string, key: string, opts?: VarsOptions): Promise<boolean>;
139
+ /** Atomic increment (default `1`) — does not clear an existing TTL. */
140
+ incr(ns: string, key: string, by?: number, opts?: VarsSetOptions): Promise<number>;
141
+ /** Set / renew the TTL of an existing key. */
142
+ expire(ns: string, key: string, ttl: string | number, opts?: VarsOptions): Promise<boolean>;
143
+ /** Delete every key of the namespace in the current scope. */
144
+ clear(ns: string, opts?: VarsOptions): Promise<number>;
145
+ /** Bound store with the scope pre-filled — `rest.vars.scope(companyId)`. */
146
+ scope(value: any): TenantVars;
147
+ };