@rezwanul7/settings 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +388 -2
- package/dist/index.cjs +443 -0
- package/dist/index.d.cts +348 -0
- package/dist/index.d.ts +348 -0
- package/dist/index.js +436 -0
- package/dist/prisma.cjs +55 -0
- package/dist/prisma.d.cts +148 -0
- package/dist/prisma.d.ts +148 -0
- package/dist/prisma.js +53 -0
- package/dist/store-B4pWek75.d.cts +61 -0
- package/dist/store-B4pWek75.d.ts +61 -0
- package/package.json +66 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rezwanul Islam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,389 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @rezwanul7/settings
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Per-user and app-wide settings for an app, such as a user's theme and language or a global maintenance banner. Each
|
|
4
|
+
Scope declares a schema and its defaults once; only an Owner's own choices, its Overrides, are stored, as one JSON
|
|
5
|
+
document per Owner, and every read returns the Effective settings: the defaults with the Overrides applied. Adding a
|
|
6
|
+
field or changing a default needs no migration of stored rows. Writes are validated and guarded by a version, so two
|
|
7
|
+
requests changing one Owner's settings at once both take effect. The terms used here are defined in the package
|
|
8
|
+
[glossary](./GLOSSARY.md).
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @rezwanul7/settings
|
|
14
|
+
# or
|
|
15
|
+
pnpm add @rezwanul7/settings
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The package has no runtime or peer dependencies. Schemas can come from any validator that implements
|
|
19
|
+
[Standard Schema](https://standardschema.dev), such as Zod, Valibot or ArkType; the examples use Zod.
|
|
20
|
+
|
|
21
|
+
## Entry points
|
|
22
|
+
|
|
23
|
+
| Import | Contents |
|
|
24
|
+
| ---------------------------- | -------------------------------------------------------------------------- |
|
|
25
|
+
| `@rezwanul7/settings` | `defineSettings`, `createSettings`, `MemoryStore`, every type and error |
|
|
26
|
+
| `@rezwanul7/settings/prisma` | `PrismaStore` and the delegate type it accepts; doesn't need Prisma itself |
|
|
27
|
+
|
|
28
|
+
## Defining Scopes
|
|
29
|
+
|
|
30
|
+
A Scope is a kind of Owner, such as `user` or `organization`. Give each Scope one Settings definition with
|
|
31
|
+
`defineSettings`: a schema and the defaults used wherever an Owner has no Override. The settings type is inferred from
|
|
32
|
+
the schema's output type, and `defaults` must be a complete value of it; optional fields may be left out.
|
|
33
|
+
|
|
34
|
+
A single-owner Scope, such as `global` for app-wide settings, has exactly one Owner. Mark it with `singleOwner: true`.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// src/server/settings-definitions.ts
|
|
38
|
+
import { defineSettings } from "@rezwanul7/settings";
|
|
39
|
+
import { z } from "zod";
|
|
40
|
+
|
|
41
|
+
export const user = defineSettings({
|
|
42
|
+
schema: z.object({
|
|
43
|
+
theme: z.enum(["light", "dark", "system"]),
|
|
44
|
+
language: z.string(),
|
|
45
|
+
notifications: z.object({ email: z.boolean(), push: z.boolean() }),
|
|
46
|
+
}),
|
|
47
|
+
defaults: {
|
|
48
|
+
theme: "system",
|
|
49
|
+
language: "en",
|
|
50
|
+
notifications: { email: true, push: false },
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const global = defineSettings({
|
|
55
|
+
schema: z.object({ maintenanceBanner: z.string().nullable() }),
|
|
56
|
+
defaults: { maintenanceBanner: null },
|
|
57
|
+
singleOwner: true,
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The defaults are checked against the schema on the first operation on each Scope, because Standard Schema validation
|
|
62
|
+
may be async. Defaults that fail it throw `SettingsDefinitionError` there, on every operation on that Scope. If the
|
|
63
|
+
validation itself throws, such as an async validator that can't reach its service, that operation rejects with the
|
|
64
|
+
error and the next one checks again.
|
|
65
|
+
|
|
66
|
+
### Keep the schema's output valid input
|
|
67
|
+
|
|
68
|
+
Every read merges the Overrides over the defaults and validates the result, and every write validates the Effective
|
|
69
|
+
settings it would produce. What is stored is the change as given, not the schema's output. So the defaults, which have
|
|
70
|
+
the output type, and the stored Overrides are validated again on every read, and a schema whose output doesn't pass
|
|
71
|
+
the schema again breaks:
|
|
72
|
+
|
|
73
|
+
- Don't use transforms that change a value's type, such as a string parsed into a `Date`: the defaults (a `Date`)
|
|
74
|
+
would then fail the schema, which expects a string. A transform that keeps the type, such as trimming, works, but
|
|
75
|
+
runs on every read rather than once on write.
|
|
76
|
+
- Don't use schema-level defaults, such as Zod's `.default()`. The Settings definition's `defaults` are the one place
|
|
77
|
+
defaults live, and every field already has one there.
|
|
78
|
+
- Use only JSON values: strings, finite numbers, booleans, `null`, arrays and plain objects. Every Store keeps the
|
|
79
|
+
Overrides as JSON, so a `Date` would read back as its ISO string and an `undefined` value would be dropped. For a
|
|
80
|
+
date, store the string and convert it where the setting is used. `MemoryStore` keeps them as JSON too, so tests
|
|
81
|
+
show what a database does.
|
|
82
|
+
|
|
83
|
+
## Creating the settings
|
|
84
|
+
|
|
85
|
+
Pass every Settings definition, by Scope name, and a Store to `createSettings`. Then `scope(name, id)` gives one
|
|
86
|
+
Owner's `ScopedSettings`. Owner ids are strings. A single-owner Scope takes no id; any other Scope requires one, and
|
|
87
|
+
both rules are checked by the types.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { createSettings, MemoryStore } from "@rezwanul7/settings";
|
|
91
|
+
import { global, user } from "./settings-definitions";
|
|
92
|
+
|
|
93
|
+
const settings = createSettings({
|
|
94
|
+
definitions: { user, global },
|
|
95
|
+
store: new MemoryStore(),
|
|
96
|
+
onInvalid: (owner, issues) => {
|
|
97
|
+
console.warn("Ignored stale settings", owner, issues);
|
|
98
|
+
},
|
|
99
|
+
maxRetries: 3,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const userSettings = settings.scope("user", "42");
|
|
103
|
+
const globalSettings = settings.scope("global");
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
| Option | Default | What it does |
|
|
107
|
+
| ------------- | ---------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
108
|
+
| `definitions` | (required) | Each Scope's Settings definition, by Scope name. |
|
|
109
|
+
| `store` | (required) | Where the Overrides are kept: `MemoryStore`, `PrismaStore` or your own `SettingsStore`. |
|
|
110
|
+
| `onInvalid` | none | Called with the Owner and the issues when stored Overrides are ignored; see [Stale Overrides](#stale-overrides). |
|
|
111
|
+
| `maxRetries` | `3` | How often a write that lost a version race tries again; see [Concurrent writes](#concurrent-writes). |
|
|
112
|
+
|
|
113
|
+
`maxRetries` must be a whole number, 0 or more, or `createSettings` throws a `RangeError`. With `0`, a write never
|
|
114
|
+
retries.
|
|
115
|
+
|
|
116
|
+
`MemoryStore` keeps Overrides in process memory, for tests and prototypes; they are lost when the process exits and
|
|
117
|
+
aren't shared between processes. For a database, use the [Prisma Store](#storing-settings-with-prisma) or implement
|
|
118
|
+
`SettingsStore` (its contract is in the [API reference](./docs/api/README.md#settingsstore)).
|
|
119
|
+
|
|
120
|
+
## Reading and writing an Owner's settings
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const me = settings.scope("user", "42");
|
|
124
|
+
|
|
125
|
+
await me.get(); // { theme: "system", language: "en", notifications: { email: true, push: false } }
|
|
126
|
+
await me.get("notifications.email"); // true
|
|
127
|
+
|
|
128
|
+
await me.set("theme", "dark");
|
|
129
|
+
await me.update({ language: "de", notifications: { push: true } });
|
|
130
|
+
await me.getOverrides(); // { theme: "dark", language: "de", notifications: { push: true } }
|
|
131
|
+
|
|
132
|
+
await me.reset("theme"); // theme follows the default again
|
|
133
|
+
await me.reset(); // deletes everything stored for the Owner
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
| Member | What it does |
|
|
137
|
+
| ------------------ | ------------------------------------------------------------------------------------------- |
|
|
138
|
+
| `owner` | The Owner, `{ scope, id }`. The Owner of a single-owner Scope has the id `"default"`. |
|
|
139
|
+
| `get()` | The complete Effective settings, as the schema outputs them. |
|
|
140
|
+
| `get(path)` | One typed value of the Effective settings, at a dot path such as `"notifications.email"`. |
|
|
141
|
+
| `set(path, value)` | Stores `value` as the Override at `path`, keeping the other Overrides. |
|
|
142
|
+
| `update(partial)` | Deep-merges `partial` into the Overrides, to change several settings at once. |
|
|
143
|
+
| `getOverrides()` | The Overrides as stored, without the defaults, or `{}` when nothing is stored. |
|
|
144
|
+
| `reset(path)` | Removes the Override at `path` (and every Override under it), so the default applies again. |
|
|
145
|
+
| `reset()` | Deletes everything stored for the Owner, so it reads the defaults. |
|
|
146
|
+
|
|
147
|
+
- Paths and values are type-checked: `set("theme", "blue")` and `get("colour")` don't compile. A path never goes into
|
|
148
|
+
an array; an array is one value, set and reset as a whole.
|
|
149
|
+
- `__proto__`, `constructor` and `prototype` can't be setting names, at any depth, because they could reach
|
|
150
|
+
`Object.prototype`. A write with one in its path or value throws `SettingsValidationError` with a `"Forbidden key"`
|
|
151
|
+
issue, and `get(path)` reads one as `undefined`. Stored ones are ignored as unknown keys.
|
|
152
|
+
- Merging works the same on reads and in `update`: plain objects merge key by key; arrays, scalars and `null` replace.
|
|
153
|
+
- In `update`, an `undefined` value leaves that Override as it was, and `null` is stored as a value, which passes only
|
|
154
|
+
if the schema allows it.
|
|
155
|
+
- A value equal to the current default is still stored as an Override, so a later change of the default doesn't change
|
|
156
|
+
what the Owner chose. `reset(path)` is how an Owner follows the default again; with no Override at `path`, it
|
|
157
|
+
writes nothing.
|
|
158
|
+
- A write whose Effective settings fail the schema throws `SettingsValidationError` and stores nothing. So does a
|
|
159
|
+
write with a key the schema doesn't know, which a schema that strips unknown keys would otherwise drop silently; the
|
|
160
|
+
issue for it has the message `"Unknown setting"`, as on read.
|
|
161
|
+
- `getOverrides()` suits a settings page that marks which values are the Owner's own; it returns the document as
|
|
162
|
+
stored, including any Overrides a read would ignore.
|
|
163
|
+
|
|
164
|
+
### Authorize the Owner first
|
|
165
|
+
|
|
166
|
+
An Owner id is data, not a credential: the package stores and returns whatever Owner the app names, and checks
|
|
167
|
+
nothing about who is asking. Before calling `scope(name, id)`, the app must decide that the current request may read
|
|
168
|
+
or change that Owner's settings, for example by taking the user id from the session rather than from the request
|
|
169
|
+
body, and by checking that the user is an admin before writing a `global` setting.
|
|
170
|
+
|
|
171
|
+
### Deleting an Owner
|
|
172
|
+
|
|
173
|
+
Settings aren't removed when the user or organization they belong to is deleted. Call `reset()` for that Owner as
|
|
174
|
+
part of deleting it, so no Overrides are left behind.
|
|
175
|
+
|
|
176
|
+
## Storing settings with Prisma
|
|
177
|
+
|
|
178
|
+
`@rezwanul7/settings/prisma` exports `PrismaStore`, a Store over your own Prisma model. The package doesn't depend on
|
|
179
|
+
`@prisma/client`; it takes the model delegate your generated client provides. Copy this model into your Prisma schema
|
|
180
|
+
and create a migration:
|
|
181
|
+
|
|
182
|
+
```prisma
|
|
183
|
+
model Setting {
|
|
184
|
+
scope String
|
|
185
|
+
ownerId String
|
|
186
|
+
overrides Json
|
|
187
|
+
version Int @default(1)
|
|
188
|
+
createdAt DateTime @default(now())
|
|
189
|
+
updatedAt DateTime @updatedAt
|
|
190
|
+
|
|
191
|
+
@@id([scope, ownerId])
|
|
192
|
+
@@map("settings")
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Each row holds one Owner's Overrides; an Owner with nothing stored has no row and reads the defaults. Keep the field
|
|
197
|
+
names, the compound id on `scope` and `ownerId`, and the `version` default of `1`; the model and table names can be
|
|
198
|
+
changed, as long as you pass the matching delegate.
|
|
199
|
+
|
|
200
|
+
The row is removed by `reset()`, and a row created after it starts again at version 1. So a write that read the Owner
|
|
201
|
+
just before a `reset()`, and stores after another write has recreated the row and brought it back to the version it
|
|
202
|
+
read, overwrites that row: the Overrides written since the reset are lost. The model keeps no version once the row is
|
|
203
|
+
gone, so the Store can't prevent that; call `reset()` when no other write to the Owner is expected, such as when
|
|
204
|
+
deleting the Owner.
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { createSettings } from "@rezwanul7/settings";
|
|
208
|
+
import { PrismaStore } from "@rezwanul7/settings/prisma";
|
|
209
|
+
import { prisma } from "./db";
|
|
210
|
+
import { global, user } from "./settings-definitions";
|
|
211
|
+
|
|
212
|
+
export const settings = createSettings({
|
|
213
|
+
definitions: { user, global },
|
|
214
|
+
store: new PrismaStore({ delegate: prisma.setting }),
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Next.js
|
|
219
|
+
|
|
220
|
+
Create the settings once, in a server module, and wrap `get()` in React's `cache()` so a request that reads an
|
|
221
|
+
Owner's settings in several Server Components reads the database once. Take the Owner id from the session, never from
|
|
222
|
+
the client.
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
// src/server/settings.ts
|
|
226
|
+
import "server-only";
|
|
227
|
+
import { cache } from "react";
|
|
228
|
+
import { createSettings } from "@rezwanul7/settings";
|
|
229
|
+
import { PrismaStore } from "@rezwanul7/settings/prisma";
|
|
230
|
+
import { auth } from "./auth";
|
|
231
|
+
import { prisma } from "./db";
|
|
232
|
+
import { global, user } from "./settings-definitions";
|
|
233
|
+
|
|
234
|
+
export const settings = createSettings({
|
|
235
|
+
definitions: { user, global },
|
|
236
|
+
store: new PrismaStore({ delegate: prisma.setting }),
|
|
237
|
+
onInvalid: (owner, issues) => {
|
|
238
|
+
console.warn("Ignored stale settings", { owner, issues });
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
/** The signed-in user's Effective settings, read once per request. */
|
|
243
|
+
export const getMySettings = cache(async () => {
|
|
244
|
+
const session = await auth();
|
|
245
|
+
if (!session) throw new Error("Not signed in");
|
|
246
|
+
return settings.scope("user", session.user.id).get();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
/** The app-wide settings, read once per request. */
|
|
250
|
+
export const getGlobalSettings = cache(() => settings.scope("global").get());
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
// src/app/settings/actions.ts
|
|
255
|
+
"use server";
|
|
256
|
+
import { auth } from "@/server/auth";
|
|
257
|
+
import { settings } from "@/server/settings";
|
|
258
|
+
|
|
259
|
+
export async function setTheme(theme: "light" | "dark" | "system") {
|
|
260
|
+
const session = await auth();
|
|
261
|
+
if (!session) throw new Error("Not signed in");
|
|
262
|
+
await settings.scope("user", session.user.id).set("theme", theme);
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`cache()` lasts for one server request. A Server Action runs in its own request, so the page it revalidates reads the
|
|
267
|
+
new value; code that writes and then reads in the same request should call `scope(...).get()` directly rather than the
|
|
268
|
+
cached function.
|
|
269
|
+
|
|
270
|
+
## Stale Overrides
|
|
271
|
+
|
|
272
|
+
A schema change can make stored Overrides invalid, for example when an enum value is renamed or a field is removed.
|
|
273
|
+
Such an Override is ignored rather than thrown, so the default applies at its path and one stale value doesn't break
|
|
274
|
+
the Owner's page ([ADR-0001](./docs/adr/0001-overrides-only-ignored-when-invalid.md)):
|
|
275
|
+
|
|
276
|
+
- On read, every Override at a path a validation issue names (or under it) is ignored, and so is every stored key the
|
|
277
|
+
schema no longer knows. Ignoring Overrides can reveal more issues, such as a refinement the validator skipped
|
|
278
|
+
while the shape had issues, so the settings are validated again until they pass or no issue names an Override left
|
|
279
|
+
to ignore. The rest of the Owner's Overrides still apply.
|
|
280
|
+
- `onInvalid(owner, issues)` is called with the issues naming what was ignored; an unknown key is reported as an issue
|
|
281
|
+
with the message `"Unknown setting"`. Without `onInvalid`, they are ignored silently, so pass it to log them.
|
|
282
|
+
- A read never changes the stored document. The next successful write for that Owner drops the ignored Overrides.
|
|
283
|
+
- Writes read the Overrides first, so `onInvalid` can also be called during `set`, `update` or `reset(path)`, and more
|
|
284
|
+
than once for one write when it retries after a conflict.
|
|
285
|
+
- If the Effective settings still fail the schema after the invalid Overrides are ignored, which means the defaults
|
|
286
|
+
themselves no longer fit, the read throws `SettingsDefinitionError`. A write still works from the Overrides it
|
|
287
|
+
can keep and validates only the result of its change, so a write that repairs the settings, such as setting the
|
|
288
|
+
value a refinement asks for, succeeds; one that doesn't throws `SettingsValidationError`.
|
|
289
|
+
|
|
290
|
+
Changing a schema in a way that invalidates stored values is a product decision: those Owners go back to the default.
|
|
291
|
+
|
|
292
|
+
## Concurrent writes
|
|
293
|
+
|
|
294
|
+
Each write reads the Owner's Overrides and their version, applies its change and stores the result only if the version
|
|
295
|
+
is unchanged. If another write got there first, it reads again and applies its change to the newer Overrides, so two
|
|
296
|
+
requests changing different settings of one Owner both take effect. After `maxRetries` retries that all lost the race,
|
|
297
|
+
it throws `SettingsConflictError`, whose `owner` names the Owner, and stores nothing; it is safe to try the whole write
|
|
298
|
+
again later. `reset()` deletes without a version check. `MemoryStore` keeps an Owner's versions increasing across
|
|
299
|
+
`reset()`, so a write that read the Owner before the reset is a conflict; `PrismaStore` can't, see
|
|
300
|
+
[Storing settings with Prisma](#storing-settings-with-prisma).
|
|
301
|
+
|
|
302
|
+
## Secret settings
|
|
303
|
+
|
|
304
|
+
The package doesn't encrypt anything. Keep a secret setting, such as an SMTP password, sealed with
|
|
305
|
+
[`@rezwanul7/secret-vault`](../secret-vault/README.md): seal it before `set()`, store the Sealed value as a string,
|
|
306
|
+
and open it after `get()`. Give the Context the Owner, so a Sealed value copied to another Owner doesn't open.
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
import {
|
|
310
|
+
createSettings,
|
|
311
|
+
defineSettings,
|
|
312
|
+
type Owner,
|
|
313
|
+
} from "@rezwanul7/settings";
|
|
314
|
+
import { PrismaStore } from "@rezwanul7/settings/prisma";
|
|
315
|
+
import { z } from "zod";
|
|
316
|
+
import { prisma } from "./db";
|
|
317
|
+
import { vault } from "./vault";
|
|
318
|
+
|
|
319
|
+
const organization = defineSettings({
|
|
320
|
+
schema: z.object({
|
|
321
|
+
smtp: z.object({
|
|
322
|
+
host: z.string(),
|
|
323
|
+
// A Sealed value made by the Vault, never the password itself.
|
|
324
|
+
password: z.string().startsWith("vault:").nullable(),
|
|
325
|
+
}),
|
|
326
|
+
}),
|
|
327
|
+
defaults: { smtp: { host: "localhost", password: null } },
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const settings = createSettings({
|
|
331
|
+
definitions: { organization },
|
|
332
|
+
store: new PrismaStore({ delegate: prisma.setting }),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
function smtpPasswordContext(owner: Owner): string {
|
|
336
|
+
return `settings:smtp.password:${owner.scope}:${owner.id}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export async function saveSmtpPassword(orgId: string, password: string) {
|
|
340
|
+
const org = settings.scope("organization", orgId);
|
|
341
|
+
const sealed = await vault.encrypt(password, smtpPasswordContext(org.owner));
|
|
342
|
+
await org.set("smtp.password", sealed);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export async function readSmtpPassword(orgId: string): Promise<string | null> {
|
|
346
|
+
const org = settings.scope("organization", orgId);
|
|
347
|
+
const sealed = await org.get("smtp.password");
|
|
348
|
+
return sealed === null
|
|
349
|
+
? null
|
|
350
|
+
: vault.decrypt(sealed, smtpPasswordContext(org.owner));
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
The Effective settings then hold only the Sealed value, so they can be logged or passed around without exposing the
|
|
355
|
+
secret; open it on the server, only where it is used.
|
|
356
|
+
|
|
357
|
+
## Errors
|
|
358
|
+
|
|
359
|
+
- `SettingsValidationError`: a write would make the Effective settings fail the schema. Nothing is stored. Its
|
|
360
|
+
`issues` are the Standard Schema issues, with paths into the settings, for showing next to form fields.
|
|
361
|
+
- `SettingsConflictError`: a write lost the version race on every try, `maxRetries` retries included. Nothing is
|
|
362
|
+
stored, and the write can be tried again. Its `owner` names the Owner.
|
|
363
|
+
- `SettingsDefinitionError`: a Scope name isn't in `definitions`, a Scope's defaults fail its schema, or an Owner's
|
|
364
|
+
Effective settings still fail it after ignoring stale Overrides. It is a bug in the app's Settings definitions, not
|
|
365
|
+
in the input, so don't show it to a user. It carries the `scope` and, for a failed validation, the `issues`.
|
|
366
|
+
|
|
367
|
+
`createSettings` also throws a `RangeError` for an invalid `maxRetries`, and `scope()` throws a `TypeError` for an id
|
|
368
|
+
passed to a single-owner Scope or missing for any other, which the types already rule out.
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
import { SettingsValidationError } from "@rezwanul7/settings";
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
await settings.scope("user", session.user.id).update(formValues);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (error instanceof SettingsValidationError) {
|
|
377
|
+
return { fieldErrors: error.issues };
|
|
378
|
+
}
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## API reference
|
|
384
|
+
|
|
385
|
+
Every export, parameter, and default is listed in the [API reference](./docs/api/README.md), generated from the source.
|
|
386
|
+
|
|
387
|
+
## License
|
|
388
|
+
|
|
389
|
+
MIT License. See [LICENSE](./LICENSE) for details.
|