@rezwanul7/settings 1.1.0 → 2.0.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/dist/index.d.cts CHANGED
@@ -76,11 +76,43 @@ interface SettingsDefinition<T = unknown, SingleOwner extends boolean = boolean>
76
76
  /** Whether the Scope has exactly one Owner, stored with the id `"default"`. */
77
77
  readonly singleOwner: SingleOwner;
78
78
  }
79
+ /** The settings type of a Settings definition. Internal: not exported from the package entry. */
80
+ type SettingsOf<D> = D extends SettingsDefinition<infer T> ? T : never;
81
+ /** Counts down the depth {@link NonJsonPaths} still walks, so a recursive settings type can't recurse forever. */
82
+ type Depth = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
83
+ /** Whether an object type has a method, as a `Date`, `Map`, `Set` or class instance does. */
84
+ type HasMethod<T> = true extends {
85
+ [K in keyof T]-?: 0 extends 1 & T[K] ? false : T[K] extends (...args: never[]) => unknown ? true : false;
86
+ }[keyof T] ? true : false;
87
+ /**
88
+ * The dot paths in `T` whose values can't be kept as JSON, such as a `Date`, `Map`, `Set`, `bigint` or class
89
+ * instance; `never` when every value can. An array's elements are named `path[]`. `unknown` and `any` pass, since
90
+ * the runtime check covers them, and so does anything deeper than the walk goes.
91
+ */
92
+ type NonJsonPaths<T, P extends string, D extends number = 10> = [D] extends [
93
+ never
94
+ ] ? never : 0 extends 1 & T ? never : unknown extends T ? never : T extends string | number | boolean | null | undefined ? never : T extends bigint | symbol | ((...args: never[]) => unknown) ? P : T extends readonly (infer E)[] ? NonJsonPaths<E, `${P}[]`, Depth[D]> : T extends object ? HasMethod<T> extends true ? P : {
95
+ [K in keyof T & string]-?: NonJsonPaths<T[K], P extends "" ? K : `${P}.${K}`, Depth[D]>;
96
+ }[keyof T & string] : P;
97
+ /**
98
+ * Nothing extra when the schema's output can be kept as JSON. Otherwise a property no schema has, whose name states
99
+ * the rule and whose type names the offending paths, so the compile error at `schema` reads as the reason.
100
+ */
101
+ type JsonOutputCheck<Schema extends StandardSchemaV1> = [
102
+ NonJsonPaths<StandardSchemaV1.InferOutput<Schema>, "">
103
+ ] extends [never] ? unknown : {
104
+ readonly "Not a JSON value, so it can't be stored at": NonJsonPaths<StandardSchemaV1.InferOutput<Schema>, ""> extends "" ? "(the whole settings)" : NonJsonPaths<StandardSchemaV1.InferOutput<Schema>, "">;
105
+ };
79
106
  /**
80
107
  * Declares a Scope's Settings definition. The settings type is inferred from the schema's output type, and
81
108
  * `defaults` must be a complete value of it; optional schema fields may be left out. Any Standard Schema works, such
82
109
  * as one from Zod, Valibot or ArkType.
83
110
  *
111
+ * Overrides are kept as JSON, so a schema whose output has a value that isn't, such as a `Date` from
112
+ * `z.coerce.date()`, a `Map`, a `Set`, a `bigint` or a class instance, is a compile error at `schema` that names the
113
+ * path. Store a date as its string and convert it where the setting is used. Output typed `unknown` or `any` passes;
114
+ * writes still check their values at runtime.
115
+ *
84
116
  * The defaults are checked against the schema on the first operation on the Scope, because Standard Schema
85
117
  * validation may be async; `createSettings` throws `SettingsDefinitionError` there if they fail.
86
118
  *
@@ -88,27 +120,27 @@ interface SettingsDefinition<T = unknown, SingleOwner extends boolean = boolean>
88
120
  * @returns The Settings definition, to pass to `createSettings` under its Scope name.
89
121
  *
90
122
  * @example
91
- * const user = defineSettings({
123
+ * const userDefinition = defineSettings({
92
124
  * schema: z.object({ theme: z.enum(["light", "dark"]), language: z.string() }),
93
125
  * defaults: { theme: "light", language: "en" },
94
126
  * });
95
- * const global = defineSettings({
127
+ * const globalDefinition = defineSettings({
96
128
  * schema: z.object({ maintenanceBanner: z.string().nullable() }),
97
129
  * defaults: { maintenanceBanner: null },
98
130
  * singleOwner: true,
99
131
  * });
100
132
  */
101
133
  declare function defineSettings<Schema extends StandardSchemaV1, SingleOwner extends boolean = false>(definition: {
102
- /** The Standard Schema every Owner's Effective settings must pass. */
103
- readonly schema: Schema;
134
+ /** The Standard Schema every Owner's Effective settings must pass. Its output must be JSON. */
135
+ readonly schema: Schema & JsonOutputCheck<Schema>;
104
136
  /** The complete settings used where an Owner has no Override. */
105
137
  readonly defaults: StandardSchemaV1.InferOutput<Schema>;
106
138
  /** `true` for a Scope with exactly one Owner, which takes no id. Defaults to `false`. */
107
139
  readonly singleOwner?: SingleOwner;
108
140
  }): SettingsDefinition<StandardSchemaV1.InferOutput<Schema>, SingleOwner>;
109
141
 
110
- /** Objects whose keys a dot path walks through; arrays, functions and dates are values, never walked into. */
111
- type Walkable<T> = T extends readonly unknown[] | Date | ((...args: never[]) => unknown) ? never : T extends object ? T : never;
142
+ /** Objects whose keys a dot path walks through; arrays and functions are values, never walked into. */
143
+ type Walkable<T> = T extends readonly unknown[] | ((...args: never[]) => unknown) ? never : T extends object ? T : never;
112
144
  /**
113
145
  * The union of dot paths through the nested plain objects of `T`, such as `"theme" | "notifications" |
114
146
  * "notifications.email"`. A path never descends into an array: an array is one value, set and reset as a whole.
@@ -128,11 +160,11 @@ type Path<T> = [Walkable<T>] extends [never] ? never : {
128
160
  type PathValue<T, P extends string> = P extends keyof Walkable<T> ? Walkable<T>[P] : P extends `${infer K}.${infer Rest}` ? K extends keyof Walkable<T> ? PathValue<NonNullable<Walkable<T>[K]>, Rest> | (null extends Walkable<T>[K] ? undefined : never) | (undefined extends Walkable<T>[K] ? undefined : never) : never : never;
129
161
  /**
130
162
  * A partial of `T` at any depth, for `update`: every key of a nested plain object may be left out or `undefined`.
131
- * Arrays, dates and functions are whole values, never partial.
163
+ * Arrays and functions are whole values, never partial.
132
164
  *
133
165
  * @typeParam T - The settings type.
134
166
  */
135
- type DeepPartial<T> = T extends readonly unknown[] | Date | ((...args: never[]) => unknown) ? T : T extends object ? {
167
+ type DeepPartial<T> = T extends readonly unknown[] | ((...args: never[]) => unknown) ? T : T extends object ? {
136
168
  [K in keyof T]?: DeepPartial<T[K]> | undefined;
137
169
  } : T;
138
170
 
@@ -152,6 +184,10 @@ interface SettingsOptions<Definitions extends Record<string, SettingsDefinition>
152
184
  * next successful write drops them. Writes read the Overrides first, so it can also be called during a write, again
153
185
  * on each retry. Without it, ignored Overrides are dropped silently.
154
186
  *
187
+ * It isn't awaited, and it can't change the result of the read or write that calls it: if it throws or returns a
188
+ * rejected promise, the operation goes on as if it had succeeded, and the failure goes to `console.warn` the first
189
+ * time only for each {@link createSettings} result.
190
+ *
155
191
  * @param owner - The Owner whose Overrides were ignored.
156
192
  * @param issues - The Standard Schema issues the Effective settings failed with, naming the ignored paths.
157
193
  */
@@ -259,8 +295,6 @@ interface ScopedSettings<T> {
259
295
  */
260
296
  reset(): Promise<void>;
261
297
  }
262
- /** The settings type of a Settings definition. */
263
- type SettingsOf<D> = D extends SettingsDefinition<infer T> ? T : never;
264
298
  /** The id argument of `scope()`: none for a single-owner Scope, a string for any other. */
265
299
  type OwnerIdArgs<D> = D extends SettingsDefinition<unknown, true> ? [] : [id: string];
266
300
  /**
@@ -279,7 +313,10 @@ type OwnerIdArgs<D> = D extends SettingsDefinition<unknown, true> ? [] : [id: st
279
313
  * @throws `RangeError` If `maxRetries` isn't a whole number, 0 or more.
280
314
  *
281
315
  * @example
282
- * const settings = createSettings({ definitions: { user, global }, store: new MemoryStore() });
316
+ * const settings = createSettings({
317
+ * definitions: { user: userDefinition, global: globalDefinition },
318
+ * store: new MemoryStore(),
319
+ * });
283
320
  * const theme = await settings.scope("user", "1").get("theme");
284
321
  * await settings.scope("global").set("maintenanceBanner", "Back at 10:00");
285
322
  */
@@ -288,8 +325,11 @@ declare function createSettings<Definitions extends Record<string, SettingsDefin
288
325
  * Returns the settings of one Owner of the Scope `name`.
289
326
  *
290
327
  * @param name - The Scope name, a key of `definitions`.
291
- * @param id - The Owner's id; left out for a single-owner Scope.
328
+ * @param id - The Owner's id, any non-empty string; left out for a single-owner Scope. The package doesn't interpret
329
+ * it, so ids that differ in any way, such as `"1"` and `" 1"`, are different Owners.
292
330
  * @throws {@link SettingsDefinitionError} If `name` isn't a key of `definitions`.
331
+ * @throws `TypeError` If `id` is the empty string, is missing for a normal Scope, or is given for a single-owner
332
+ * Scope.
293
333
  */
294
334
  scope<Name extends keyof Definitions & string>(name: Name, ...id: OwnerIdArgs<Definitions[Name]>): ScopedSettings<SettingsOf<Definitions[Name]>>;
295
335
  };
@@ -302,7 +342,7 @@ declare function createSettings<Definitions extends Record<string, SettingsDefin
302
342
  * write that read a version before a delete is a conflict; the Store remembers one number per Owner it ever stored.
303
343
  *
304
344
  * @example
305
- * const settings = createSettings({ definitions: { user }, store: new MemoryStore() });
345
+ * const settings = createSettings({ definitions: { user: userDefinition }, store: new MemoryStore() });
306
346
  */
307
347
  declare class MemoryStore implements SettingsStore {
308
348
  #private;
@@ -344,17 +384,37 @@ interface SealingVault {
344
384
  */
345
385
  isSealed?(value: unknown): boolean;
346
386
  }
347
- /** Options of {@link SealedStore}. */
348
- interface SealedStoreOptions {
387
+ /**
388
+ * The paths to keep sealed, by Scope name, each checked against its Scope's settings type, such as
389
+ * `{ organization: ["smtp.password"] }`. A Scope may be left out.
390
+ *
391
+ * @typeParam Definitions - The Settings definitions by Scope name.
392
+ */
393
+ type SealedPaths<Definitions extends Record<string, SettingsDefinition>> = {
394
+ readonly [Scope in keyof Definitions & string]?: readonly Path<SettingsOf<Definitions[Scope]>>[];
395
+ };
396
+ /**
397
+ * Options of {@link SealedStore}.
398
+ *
399
+ * @typeParam Definitions - The Settings definitions by Scope name, the same ones passed to `createSettings`.
400
+ */
401
+ interface SealedStoreOptions<Definitions extends Record<string, SettingsDefinition>> {
349
402
  /** The Store that keeps the Sealed values. */
350
403
  readonly store: SettingsStore;
351
404
  /** Seals and opens values; a Secret Vault `Vault` fits. */
352
405
  readonly vault: SealingVault;
353
406
  /**
354
- * Dot paths into the settings to keep sealed, such as `"smtp.password"`. A path may name an object, which is sealed
355
- * whole as JSON. No path may be inside another.
407
+ * Each Scope's Settings definition, by Scope name: the same object passed to `createSettings`. Only used to check
408
+ * `paths`.
409
+ */
410
+ readonly definitions: Definitions;
411
+ /**
412
+ * Dot paths into the settings to keep sealed, by Scope name, such as `{ organization: ["smtp.password"] }`. A path
413
+ * may name an object, which is sealed whole as JSON. No path may be inside another of the same Scope; two Scopes may
414
+ * list the same path. A Scope left out has nothing sealed: a Sealed value stored there reads back as its raw
415
+ * `vault:…` string.
356
416
  */
357
- readonly paths: readonly string[];
417
+ readonly paths: NoInfer<SealedPaths<Definitions>>;
358
418
  }
359
419
  /**
360
420
  * A Store that keeps the Overrides at the listed paths as Sealed values in the Store it wraps, and hands the core
@@ -365,26 +425,53 @@ interface SealedStoreOptions {
365
425
  * replace the secret with its default. A plain value at a listed path reads as it is and is sealed on the Owner's next
366
426
  * write. Values at unlisted paths are never opened.
367
427
  *
428
+ * Paths are listed per Scope and checked against each Scope's settings type, so a misspelled path doesn't compile. Only
429
+ * the paths listed for an Owner's Scope are sealed and opened; the same path in another Scope stays plain.
430
+ *
368
431
  * Nest it outermost, around a {@link CachedStore}, so the cache holds only Sealed values.
369
432
  *
433
+ * @typeParam Definitions - The Settings definitions by Scope name.
434
+ *
370
435
  * @example
371
436
  * const store = new SealedStore({
372
437
  * store: new PrismaStore({ delegate: prisma.setting }),
373
438
  * vault,
374
- * paths: ["smtp.password"],
439
+ * definitions: { user, organization },
440
+ * paths: { organization: ["smtp.password"] },
375
441
  * });
376
442
  */
377
- declare class SealedStore implements SettingsStore {
443
+ declare class SealedStore<Definitions extends Record<string, SettingsDefinition> = Record<string, SettingsDefinition>> implements SettingsStore {
378
444
  #private;
379
445
  /**
380
- * @param options - The Store to wrap, the Vault and the sealed paths.
381
- * @throws `TypeError` when a path is empty or inside another path.
446
+ * @param options - The Store to wrap, the Vault, the Settings definitions and the sealed paths by Scope.
447
+ * @throws `TypeError` when no Scope lists a path, a Scope in `paths` isn't in `definitions`, a path is empty, or a
448
+ * path is inside another of the same Scope.
382
449
  */
383
- constructor(options: SealedStoreOptions);
450
+ constructor(options: SealedStoreOptions<Definitions>);
384
451
  /** {@inheritDoc SettingsStore.read} */
385
452
  read(owner: Owner): Promise<StoredOverrides | null>;
386
453
  /** {@inheritDoc SettingsStore.write} */
387
454
  write(owner: Owner, overrides: Record<string, unknown>, expectedVersion: number | null): Promise<number | null>;
455
+ /**
456
+ * Removes one Sealed value that won't open, so the Owner can read its settings and set a new secret again; its
457
+ * other Sealed values stay sealed and aren't opened, and a plain value at another listed path is sealed, as on any
458
+ * write. Its other Overrides stay as they are. A value that opens, a
459
+ * plain value and an empty path are left alone, so running it again after the Owner set a new secret changes
460
+ * nothing. Any rejection from `decryptJson` counts as not opening.
461
+ *
462
+ * **Check the Vault secret first.** With a wrong or missing secret every Sealed value fails to open, so `discard`
463
+ * would remove secrets that fixing the configuration would have recovered.
464
+ *
465
+ * It reads the wrapped Store and writes with the version it read. On a conflict it reads again and retries, up to 3
466
+ * times; a {@link CachedStore} underneath evicts on the conflict, so the retry reads fresh Overrides.
467
+ *
468
+ * @param owner - The Owner whose Sealed value to remove.
469
+ * @param path - A path listed for the Owner's Scope, such as `"smtp.password"`.
470
+ * @returns `true` when a Sealed value that won't open was removed, `false` when nothing was changed.
471
+ * @throws `TypeError` when `path` isn't listed for the Owner's Scope.
472
+ * @throws {@link SettingsConflictError} when every write lost the version race to another write.
473
+ */
474
+ discard(owner: Owner, path: string): Promise<boolean>;
388
475
  /** {@inheritDoc SettingsStore.delete} */
389
476
  delete(owner: Owner): Promise<void>;
390
477
  }
@@ -426,17 +513,25 @@ interface CachedStoreOptions {
426
513
  readonly ttl: number;
427
514
  /** Starts every cache key. Defaults to `"settings"`. */
428
515
  readonly prefix?: string;
429
- /** Called with an error a cache method threw; the Store then answers instead. */
516
+ /**
517
+ * Called with an error a cache method threw; the Store then answers instead.
518
+ *
519
+ * It isn't awaited, and it can't change the result of the read, write or delete that calls it: if it throws or
520
+ * returns a rejected promise, the operation goes on as if it had succeeded, so a write still returns the wrapped
521
+ * Store's result. The failure goes to `console.warn` the first time only for each {@link CachedStore}.
522
+ */
430
523
  readonly onError?: (error: unknown) => void;
431
524
  }
432
525
  /**
433
526
  * A Store that keeps what the Store it wraps returns in a cache for `ttl` seconds, an Owner with nothing stored
434
- * included. Every write, conflict and delete evicts the Owner, so a write on this instance is read back at once and a
435
- * conflicting write retries against fresh Overrides. A cache that throws is reported to `onError` and the Store
436
- * answers instead.
527
+ * included. Every write, conflict and delete evicts the Owner, so a conflicting write retries against fresh Overrides,
528
+ * and a write on this instance is read back at once by this instance, even when one of its reads of that Owner was in
529
+ * flight during the write: a read that an eviction overtook returns what it read but doesn't cache it. A cache that
530
+ * throws is reported to `onError` and the Store answers instead.
437
531
  *
438
532
  * With a per-process cache and several instances, another instance can read Overrides up to `ttl` seconds old. Share
439
- * a cache, such as Redis, to avoid it.
533
+ * a cache, such as Redis, to avoid most of it. Even then, a read on another instance that was in flight during the
534
+ * write can still cache the old Overrides, which are then read for up to `ttl` seconds.
440
535
  *
441
536
  * @example
442
537
  * const store = new CachedStore({ store: prismaStore, cache: new MemoryCache(), ttl: 60 });
@@ -527,4 +622,4 @@ declare class SettingsDefinitionError extends Error {
527
622
  constructor(message: string, scope: string, issues?: ReadonlyArray<StandardSchemaV1.Issue>);
528
623
  }
529
624
 
530
- export { CachedStore, type CachedStoreOptions, MemoryCache, type MemoryCacheOptions, MemoryStore, Owner, type Path, type PathValue, type ScopedSettings, SealedStore, type SealedStoreOptions, type SealingVault, type SettingsCache, SettingsConflictError, type SettingsDefinition, SettingsDefinitionError, type SettingsOptions, SettingsStore, SettingsValidationError, StoredOverrides, createSettings, defineSettings };
625
+ export { CachedStore, type CachedStoreOptions, MemoryCache, type MemoryCacheOptions, MemoryStore, Owner, type Path, type PathValue, type ScopedSettings, type SealedPaths, SealedStore, type SealedStoreOptions, type SealingVault, type SettingsCache, SettingsConflictError, type SettingsDefinition, SettingsDefinitionError, type SettingsOptions, SettingsStore, SettingsValidationError, StoredOverrides, createSettings, defineSettings };
package/dist/index.d.ts CHANGED
@@ -76,11 +76,43 @@ interface SettingsDefinition<T = unknown, SingleOwner extends boolean = boolean>
76
76
  /** Whether the Scope has exactly one Owner, stored with the id `"default"`. */
77
77
  readonly singleOwner: SingleOwner;
78
78
  }
79
+ /** The settings type of a Settings definition. Internal: not exported from the package entry. */
80
+ type SettingsOf<D> = D extends SettingsDefinition<infer T> ? T : never;
81
+ /** Counts down the depth {@link NonJsonPaths} still walks, so a recursive settings type can't recurse forever. */
82
+ type Depth = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
83
+ /** Whether an object type has a method, as a `Date`, `Map`, `Set` or class instance does. */
84
+ type HasMethod<T> = true extends {
85
+ [K in keyof T]-?: 0 extends 1 & T[K] ? false : T[K] extends (...args: never[]) => unknown ? true : false;
86
+ }[keyof T] ? true : false;
87
+ /**
88
+ * The dot paths in `T` whose values can't be kept as JSON, such as a `Date`, `Map`, `Set`, `bigint` or class
89
+ * instance; `never` when every value can. An array's elements are named `path[]`. `unknown` and `any` pass, since
90
+ * the runtime check covers them, and so does anything deeper than the walk goes.
91
+ */
92
+ type NonJsonPaths<T, P extends string, D extends number = 10> = [D] extends [
93
+ never
94
+ ] ? never : 0 extends 1 & T ? never : unknown extends T ? never : T extends string | number | boolean | null | undefined ? never : T extends bigint | symbol | ((...args: never[]) => unknown) ? P : T extends readonly (infer E)[] ? NonJsonPaths<E, `${P}[]`, Depth[D]> : T extends object ? HasMethod<T> extends true ? P : {
95
+ [K in keyof T & string]-?: NonJsonPaths<T[K], P extends "" ? K : `${P}.${K}`, Depth[D]>;
96
+ }[keyof T & string] : P;
97
+ /**
98
+ * Nothing extra when the schema's output can be kept as JSON. Otherwise a property no schema has, whose name states
99
+ * the rule and whose type names the offending paths, so the compile error at `schema` reads as the reason.
100
+ */
101
+ type JsonOutputCheck<Schema extends StandardSchemaV1> = [
102
+ NonJsonPaths<StandardSchemaV1.InferOutput<Schema>, "">
103
+ ] extends [never] ? unknown : {
104
+ readonly "Not a JSON value, so it can't be stored at": NonJsonPaths<StandardSchemaV1.InferOutput<Schema>, ""> extends "" ? "(the whole settings)" : NonJsonPaths<StandardSchemaV1.InferOutput<Schema>, "">;
105
+ };
79
106
  /**
80
107
  * Declares a Scope's Settings definition. The settings type is inferred from the schema's output type, and
81
108
  * `defaults` must be a complete value of it; optional schema fields may be left out. Any Standard Schema works, such
82
109
  * as one from Zod, Valibot or ArkType.
83
110
  *
111
+ * Overrides are kept as JSON, so a schema whose output has a value that isn't, such as a `Date` from
112
+ * `z.coerce.date()`, a `Map`, a `Set`, a `bigint` or a class instance, is a compile error at `schema` that names the
113
+ * path. Store a date as its string and convert it where the setting is used. Output typed `unknown` or `any` passes;
114
+ * writes still check their values at runtime.
115
+ *
84
116
  * The defaults are checked against the schema on the first operation on the Scope, because Standard Schema
85
117
  * validation may be async; `createSettings` throws `SettingsDefinitionError` there if they fail.
86
118
  *
@@ -88,27 +120,27 @@ interface SettingsDefinition<T = unknown, SingleOwner extends boolean = boolean>
88
120
  * @returns The Settings definition, to pass to `createSettings` under its Scope name.
89
121
  *
90
122
  * @example
91
- * const user = defineSettings({
123
+ * const userDefinition = defineSettings({
92
124
  * schema: z.object({ theme: z.enum(["light", "dark"]), language: z.string() }),
93
125
  * defaults: { theme: "light", language: "en" },
94
126
  * });
95
- * const global = defineSettings({
127
+ * const globalDefinition = defineSettings({
96
128
  * schema: z.object({ maintenanceBanner: z.string().nullable() }),
97
129
  * defaults: { maintenanceBanner: null },
98
130
  * singleOwner: true,
99
131
  * });
100
132
  */
101
133
  declare function defineSettings<Schema extends StandardSchemaV1, SingleOwner extends boolean = false>(definition: {
102
- /** The Standard Schema every Owner's Effective settings must pass. */
103
- readonly schema: Schema;
134
+ /** The Standard Schema every Owner's Effective settings must pass. Its output must be JSON. */
135
+ readonly schema: Schema & JsonOutputCheck<Schema>;
104
136
  /** The complete settings used where an Owner has no Override. */
105
137
  readonly defaults: StandardSchemaV1.InferOutput<Schema>;
106
138
  /** `true` for a Scope with exactly one Owner, which takes no id. Defaults to `false`. */
107
139
  readonly singleOwner?: SingleOwner;
108
140
  }): SettingsDefinition<StandardSchemaV1.InferOutput<Schema>, SingleOwner>;
109
141
 
110
- /** Objects whose keys a dot path walks through; arrays, functions and dates are values, never walked into. */
111
- type Walkable<T> = T extends readonly unknown[] | Date | ((...args: never[]) => unknown) ? never : T extends object ? T : never;
142
+ /** Objects whose keys a dot path walks through; arrays and functions are values, never walked into. */
143
+ type Walkable<T> = T extends readonly unknown[] | ((...args: never[]) => unknown) ? never : T extends object ? T : never;
112
144
  /**
113
145
  * The union of dot paths through the nested plain objects of `T`, such as `"theme" | "notifications" |
114
146
  * "notifications.email"`. A path never descends into an array: an array is one value, set and reset as a whole.
@@ -128,11 +160,11 @@ type Path<T> = [Walkable<T>] extends [never] ? never : {
128
160
  type PathValue<T, P extends string> = P extends keyof Walkable<T> ? Walkable<T>[P] : P extends `${infer K}.${infer Rest}` ? K extends keyof Walkable<T> ? PathValue<NonNullable<Walkable<T>[K]>, Rest> | (null extends Walkable<T>[K] ? undefined : never) | (undefined extends Walkable<T>[K] ? undefined : never) : never : never;
129
161
  /**
130
162
  * A partial of `T` at any depth, for `update`: every key of a nested plain object may be left out or `undefined`.
131
- * Arrays, dates and functions are whole values, never partial.
163
+ * Arrays and functions are whole values, never partial.
132
164
  *
133
165
  * @typeParam T - The settings type.
134
166
  */
135
- type DeepPartial<T> = T extends readonly unknown[] | Date | ((...args: never[]) => unknown) ? T : T extends object ? {
167
+ type DeepPartial<T> = T extends readonly unknown[] | ((...args: never[]) => unknown) ? T : T extends object ? {
136
168
  [K in keyof T]?: DeepPartial<T[K]> | undefined;
137
169
  } : T;
138
170
 
@@ -152,6 +184,10 @@ interface SettingsOptions<Definitions extends Record<string, SettingsDefinition>
152
184
  * next successful write drops them. Writes read the Overrides first, so it can also be called during a write, again
153
185
  * on each retry. Without it, ignored Overrides are dropped silently.
154
186
  *
187
+ * It isn't awaited, and it can't change the result of the read or write that calls it: if it throws or returns a
188
+ * rejected promise, the operation goes on as if it had succeeded, and the failure goes to `console.warn` the first
189
+ * time only for each {@link createSettings} result.
190
+ *
155
191
  * @param owner - The Owner whose Overrides were ignored.
156
192
  * @param issues - The Standard Schema issues the Effective settings failed with, naming the ignored paths.
157
193
  */
@@ -259,8 +295,6 @@ interface ScopedSettings<T> {
259
295
  */
260
296
  reset(): Promise<void>;
261
297
  }
262
- /** The settings type of a Settings definition. */
263
- type SettingsOf<D> = D extends SettingsDefinition<infer T> ? T : never;
264
298
  /** The id argument of `scope()`: none for a single-owner Scope, a string for any other. */
265
299
  type OwnerIdArgs<D> = D extends SettingsDefinition<unknown, true> ? [] : [id: string];
266
300
  /**
@@ -279,7 +313,10 @@ type OwnerIdArgs<D> = D extends SettingsDefinition<unknown, true> ? [] : [id: st
279
313
  * @throws `RangeError` If `maxRetries` isn't a whole number, 0 or more.
280
314
  *
281
315
  * @example
282
- * const settings = createSettings({ definitions: { user, global }, store: new MemoryStore() });
316
+ * const settings = createSettings({
317
+ * definitions: { user: userDefinition, global: globalDefinition },
318
+ * store: new MemoryStore(),
319
+ * });
283
320
  * const theme = await settings.scope("user", "1").get("theme");
284
321
  * await settings.scope("global").set("maintenanceBanner", "Back at 10:00");
285
322
  */
@@ -288,8 +325,11 @@ declare function createSettings<Definitions extends Record<string, SettingsDefin
288
325
  * Returns the settings of one Owner of the Scope `name`.
289
326
  *
290
327
  * @param name - The Scope name, a key of `definitions`.
291
- * @param id - The Owner's id; left out for a single-owner Scope.
328
+ * @param id - The Owner's id, any non-empty string; left out for a single-owner Scope. The package doesn't interpret
329
+ * it, so ids that differ in any way, such as `"1"` and `" 1"`, are different Owners.
292
330
  * @throws {@link SettingsDefinitionError} If `name` isn't a key of `definitions`.
331
+ * @throws `TypeError` If `id` is the empty string, is missing for a normal Scope, or is given for a single-owner
332
+ * Scope.
293
333
  */
294
334
  scope<Name extends keyof Definitions & string>(name: Name, ...id: OwnerIdArgs<Definitions[Name]>): ScopedSettings<SettingsOf<Definitions[Name]>>;
295
335
  };
@@ -302,7 +342,7 @@ declare function createSettings<Definitions extends Record<string, SettingsDefin
302
342
  * write that read a version before a delete is a conflict; the Store remembers one number per Owner it ever stored.
303
343
  *
304
344
  * @example
305
- * const settings = createSettings({ definitions: { user }, store: new MemoryStore() });
345
+ * const settings = createSettings({ definitions: { user: userDefinition }, store: new MemoryStore() });
306
346
  */
307
347
  declare class MemoryStore implements SettingsStore {
308
348
  #private;
@@ -344,17 +384,37 @@ interface SealingVault {
344
384
  */
345
385
  isSealed?(value: unknown): boolean;
346
386
  }
347
- /** Options of {@link SealedStore}. */
348
- interface SealedStoreOptions {
387
+ /**
388
+ * The paths to keep sealed, by Scope name, each checked against its Scope's settings type, such as
389
+ * `{ organization: ["smtp.password"] }`. A Scope may be left out.
390
+ *
391
+ * @typeParam Definitions - The Settings definitions by Scope name.
392
+ */
393
+ type SealedPaths<Definitions extends Record<string, SettingsDefinition>> = {
394
+ readonly [Scope in keyof Definitions & string]?: readonly Path<SettingsOf<Definitions[Scope]>>[];
395
+ };
396
+ /**
397
+ * Options of {@link SealedStore}.
398
+ *
399
+ * @typeParam Definitions - The Settings definitions by Scope name, the same ones passed to `createSettings`.
400
+ */
401
+ interface SealedStoreOptions<Definitions extends Record<string, SettingsDefinition>> {
349
402
  /** The Store that keeps the Sealed values. */
350
403
  readonly store: SettingsStore;
351
404
  /** Seals and opens values; a Secret Vault `Vault` fits. */
352
405
  readonly vault: SealingVault;
353
406
  /**
354
- * Dot paths into the settings to keep sealed, such as `"smtp.password"`. A path may name an object, which is sealed
355
- * whole as JSON. No path may be inside another.
407
+ * Each Scope's Settings definition, by Scope name: the same object passed to `createSettings`. Only used to check
408
+ * `paths`.
409
+ */
410
+ readonly definitions: Definitions;
411
+ /**
412
+ * Dot paths into the settings to keep sealed, by Scope name, such as `{ organization: ["smtp.password"] }`. A path
413
+ * may name an object, which is sealed whole as JSON. No path may be inside another of the same Scope; two Scopes may
414
+ * list the same path. A Scope left out has nothing sealed: a Sealed value stored there reads back as its raw
415
+ * `vault:…` string.
356
416
  */
357
- readonly paths: readonly string[];
417
+ readonly paths: NoInfer<SealedPaths<Definitions>>;
358
418
  }
359
419
  /**
360
420
  * A Store that keeps the Overrides at the listed paths as Sealed values in the Store it wraps, and hands the core
@@ -365,26 +425,53 @@ interface SealedStoreOptions {
365
425
  * replace the secret with its default. A plain value at a listed path reads as it is and is sealed on the Owner's next
366
426
  * write. Values at unlisted paths are never opened.
367
427
  *
428
+ * Paths are listed per Scope and checked against each Scope's settings type, so a misspelled path doesn't compile. Only
429
+ * the paths listed for an Owner's Scope are sealed and opened; the same path in another Scope stays plain.
430
+ *
368
431
  * Nest it outermost, around a {@link CachedStore}, so the cache holds only Sealed values.
369
432
  *
433
+ * @typeParam Definitions - The Settings definitions by Scope name.
434
+ *
370
435
  * @example
371
436
  * const store = new SealedStore({
372
437
  * store: new PrismaStore({ delegate: prisma.setting }),
373
438
  * vault,
374
- * paths: ["smtp.password"],
439
+ * definitions: { user, organization },
440
+ * paths: { organization: ["smtp.password"] },
375
441
  * });
376
442
  */
377
- declare class SealedStore implements SettingsStore {
443
+ declare class SealedStore<Definitions extends Record<string, SettingsDefinition> = Record<string, SettingsDefinition>> implements SettingsStore {
378
444
  #private;
379
445
  /**
380
- * @param options - The Store to wrap, the Vault and the sealed paths.
381
- * @throws `TypeError` when a path is empty or inside another path.
446
+ * @param options - The Store to wrap, the Vault, the Settings definitions and the sealed paths by Scope.
447
+ * @throws `TypeError` when no Scope lists a path, a Scope in `paths` isn't in `definitions`, a path is empty, or a
448
+ * path is inside another of the same Scope.
382
449
  */
383
- constructor(options: SealedStoreOptions);
450
+ constructor(options: SealedStoreOptions<Definitions>);
384
451
  /** {@inheritDoc SettingsStore.read} */
385
452
  read(owner: Owner): Promise<StoredOverrides | null>;
386
453
  /** {@inheritDoc SettingsStore.write} */
387
454
  write(owner: Owner, overrides: Record<string, unknown>, expectedVersion: number | null): Promise<number | null>;
455
+ /**
456
+ * Removes one Sealed value that won't open, so the Owner can read its settings and set a new secret again; its
457
+ * other Sealed values stay sealed and aren't opened, and a plain value at another listed path is sealed, as on any
458
+ * write. Its other Overrides stay as they are. A value that opens, a
459
+ * plain value and an empty path are left alone, so running it again after the Owner set a new secret changes
460
+ * nothing. Any rejection from `decryptJson` counts as not opening.
461
+ *
462
+ * **Check the Vault secret first.** With a wrong or missing secret every Sealed value fails to open, so `discard`
463
+ * would remove secrets that fixing the configuration would have recovered.
464
+ *
465
+ * It reads the wrapped Store and writes with the version it read. On a conflict it reads again and retries, up to 3
466
+ * times; a {@link CachedStore} underneath evicts on the conflict, so the retry reads fresh Overrides.
467
+ *
468
+ * @param owner - The Owner whose Sealed value to remove.
469
+ * @param path - A path listed for the Owner's Scope, such as `"smtp.password"`.
470
+ * @returns `true` when a Sealed value that won't open was removed, `false` when nothing was changed.
471
+ * @throws `TypeError` when `path` isn't listed for the Owner's Scope.
472
+ * @throws {@link SettingsConflictError} when every write lost the version race to another write.
473
+ */
474
+ discard(owner: Owner, path: string): Promise<boolean>;
388
475
  /** {@inheritDoc SettingsStore.delete} */
389
476
  delete(owner: Owner): Promise<void>;
390
477
  }
@@ -426,17 +513,25 @@ interface CachedStoreOptions {
426
513
  readonly ttl: number;
427
514
  /** Starts every cache key. Defaults to `"settings"`. */
428
515
  readonly prefix?: string;
429
- /** Called with an error a cache method threw; the Store then answers instead. */
516
+ /**
517
+ * Called with an error a cache method threw; the Store then answers instead.
518
+ *
519
+ * It isn't awaited, and it can't change the result of the read, write or delete that calls it: if it throws or
520
+ * returns a rejected promise, the operation goes on as if it had succeeded, so a write still returns the wrapped
521
+ * Store's result. The failure goes to `console.warn` the first time only for each {@link CachedStore}.
522
+ */
430
523
  readonly onError?: (error: unknown) => void;
431
524
  }
432
525
  /**
433
526
  * A Store that keeps what the Store it wraps returns in a cache for `ttl` seconds, an Owner with nothing stored
434
- * included. Every write, conflict and delete evicts the Owner, so a write on this instance is read back at once and a
435
- * conflicting write retries against fresh Overrides. A cache that throws is reported to `onError` and the Store
436
- * answers instead.
527
+ * included. Every write, conflict and delete evicts the Owner, so a conflicting write retries against fresh Overrides,
528
+ * and a write on this instance is read back at once by this instance, even when one of its reads of that Owner was in
529
+ * flight during the write: a read that an eviction overtook returns what it read but doesn't cache it. A cache that
530
+ * throws is reported to `onError` and the Store answers instead.
437
531
  *
438
532
  * With a per-process cache and several instances, another instance can read Overrides up to `ttl` seconds old. Share
439
- * a cache, such as Redis, to avoid it.
533
+ * a cache, such as Redis, to avoid most of it. Even then, a read on another instance that was in flight during the
534
+ * write can still cache the old Overrides, which are then read for up to `ttl` seconds.
440
535
  *
441
536
  * @example
442
537
  * const store = new CachedStore({ store: prismaStore, cache: new MemoryCache(), ttl: 60 });
@@ -527,4 +622,4 @@ declare class SettingsDefinitionError extends Error {
527
622
  constructor(message: string, scope: string, issues?: ReadonlyArray<StandardSchemaV1.Issue>);
528
623
  }
529
624
 
530
- export { CachedStore, type CachedStoreOptions, MemoryCache, type MemoryCacheOptions, MemoryStore, Owner, type Path, type PathValue, type ScopedSettings, SealedStore, type SealedStoreOptions, type SealingVault, type SettingsCache, SettingsConflictError, type SettingsDefinition, SettingsDefinitionError, type SettingsOptions, SettingsStore, SettingsValidationError, StoredOverrides, createSettings, defineSettings };
625
+ export { CachedStore, type CachedStoreOptions, MemoryCache, type MemoryCacheOptions, MemoryStore, Owner, type Path, type PathValue, type ScopedSettings, type SealedPaths, SealedStore, type SealedStoreOptions, type SealingVault, type SettingsCache, SettingsConflictError, type SettingsDefinition, SettingsDefinitionError, type SettingsOptions, SettingsStore, SettingsValidationError, StoredOverrides, createSettings, defineSettings };