app-settings-js 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/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] — 2026-09-12
10
+
11
+ First release.
12
+
13
+ ### Added
14
+
15
+ - `AppSettingsClient`, covering every route the API exposes: resolution,
16
+ setting definitions, values in all three layers, groups and membership,
17
+ roles, platforms, environments, and API keys.
18
+ - `SettingsSnapshot`, an immutable view over a resolution with typed accessors
19
+ (`boolean`, `number`, `string`, `date`, `list`, `json`) and override helpers
20
+ (`isEnforced`, `visibleOverride`, `editable`).
21
+ - `createSettingsStore`, a framework-agnostic reactive store exposing the
22
+ `subscribe` / `getSnapshot` / `getServerSnapshot` trio that React's
23
+ `useSyncExternalStore` consumes directly, with optimistic writes, rollback
24
+ on rejection, and server-rendered hydration through `initialData`.
25
+ - Datetime helpers for the API's strict RFC 3339 rules: `toInstant`,
26
+ `localToInstant`, `parseInstant`, `toDateTimeLocal`, `isInstant`.
27
+ - A single `AppSettingsError` carrying the server's `code`, `status` and
28
+ `requestId`, with `isNotFound` / `isForbidden` / `isConflict` guards.
29
+ - Automatic retries with jittered exponential backoff on network errors, 5xx
30
+ and 429, honouring `Retry-After` and never retrying a `POST`.
31
+
32
+ [Unreleased]: https://github.com/connordoman/app-settings/compare/v0.1.0...HEAD
33
+ [0.1.0]: https://github.com/connordoman/app-settings/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Connor Doman
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 ADDED
@@ -0,0 +1,335 @@
1
+ # app-settings-js
2
+
3
+ A TypeScript SDK for the [App Settings](../README.md) API.
4
+
5
+ - **No dependencies.** Nothing but the platform's own `fetch`.
6
+ - **Runs anywhere.** Browsers, Node 18+, Bun, Deno, and edge runtimes. ESM and CJS.
7
+ - **React-ready, React-free.** No React import anywhere in this package. The
8
+ store exposes the `subscribe` / `getSnapshot` pair `useSyncExternalStore`
9
+ wants, so React is three lines away — and so are Vue, Svelte and Solid.
10
+
11
+ ```sh
12
+ bun add app-settings-js # or npm / pnpm / yarn
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ ```ts
18
+ import { AppSettingsClient } from "app-settings-js";
19
+
20
+ const client = new AppSettingsClient({
21
+ baseUrl: "https://settings.example.com",
22
+ apiKey: process.env.SETTINGS_API_KEY!,
23
+ environment: "production",
24
+ });
25
+
26
+ const settings = await client.resolveUser("alice");
27
+
28
+ settings.boolean("dark_mode"); // false
29
+ settings.number("page_size", 25); // 25 if unset
30
+ settings.string("theme");
31
+ settings.date("digest_at"); // a Date
32
+ settings.json<Limits>("rate_limits");
33
+ ```
34
+
35
+ `resolveUser` returns a [`SettingsSnapshot`](#the-snapshot): every setting the
36
+ user can see, already collapsed to one effective value each. That is the call a
37
+ product backend makes, and usually the only one it needs.
38
+
39
+ > **Where this runs.** The API key is a server credential. Settings are meant to
40
+ > be read through your own backend, which authenticates the user first. Putting
41
+ > a key in a browser bundle hands every visitor whatever that key can reach.
42
+ > Front ends should call your server, and hydrate a store from what it returns —
43
+ > see [Server rendering](#server-rendering).
44
+
45
+ ## The snapshot
46
+
47
+ A snapshot is immutable, and its identity changes only when the data does, which
48
+ is what lets a UI framework skip a re-render.
49
+
50
+ ```ts
51
+ settings.has("dark_mode"); // is it visible to this role at all?
52
+ settings.isSet("dark_mode"); // did any layer, or the default, supply a value?
53
+ settings.source("dark_mode"); // "PERSONAL" | "SERVER" | "DEFAULT" | ...
54
+ settings.get("dark_mode"); // the whole ResolvedSetting
55
+ settings.toObject(); // { dark_mode: false, page_size: 25, ... }
56
+
57
+ for (const setting of settings) { /* iterable, in the server's order */ }
58
+ ```
59
+
60
+ Typed accessors return the fallback when a setting is unset or invisible to the
61
+ role, and **throw** when the setting exists but holds another type — which can
62
+ only mean the wrong name was asked for:
63
+
64
+ ```ts
65
+ settings.string("dark_mode");
66
+ // AppSettingsError: setting "dark_mode" is declared BOOLEAN and holds a boolean, not a string
67
+ ```
68
+
69
+ `list()` covers `SELECT`, returning a single choice as a one-element array so no
70
+ caller has to branch on `multiple`:
71
+
72
+ ```ts
73
+ settings.list<string>("tags"); // ["a", "b"]
74
+ settings.options("tags"); // [["A", "a"], ["B", "b"]] for a picker
75
+ ```
76
+
77
+ ### Overrides
78
+
79
+ A group override is either **advisory** (a group-wide default the user's own
80
+ value beats) or **enforced** (a policy that beats it). Orthogonally, an override
81
+ may be marked invisible, meaning the user is not meant to observe it.
82
+
83
+ ```ts
84
+ settings.isEnforced("retention_days"); // disable the control
85
+ settings.visibleOverride("retention_days"); // the override you may tell them about
86
+ settings.override("retention_days"); // the raw one, visible or not
87
+ settings.editable(); // personal scope, not enforced
88
+ ```
89
+
90
+ `visibleOverride` is the accessor a UI should reach for: it withholds an
91
+ override the server marked `visible: false`, so a snapshot cannot leak one by
92
+ accident.
93
+
94
+ ## The store
95
+
96
+ `createSettingsStore` keeps one resolution current and tells subscribers when it
97
+ changes. It handles the parts that are tedious to get right: no fetch until
98
+ something is listening, one request shared between concurrent refreshes, stale
99
+ responses dropped, and the last good snapshot kept when a refresh fails.
100
+
101
+ ```ts
102
+ import { createSettingsStore } from "app-settings-js";
103
+
104
+ const store = createSettingsStore(client, {
105
+ userId: "alice",
106
+ refreshIntervalMs: 60_000,
107
+ revalidateOnFocus: true,
108
+ });
109
+
110
+ store.subscribe(() => console.log(store.getSnapshot().snapshot?.toObject()));
111
+ await store.set("dark_mode", true); // optimistic, rolled back if rejected
112
+ await store.clear("dark_mode"); // fall back to the layer beneath
113
+ ```
114
+
115
+ `getSnapshot()` returns a `SettingsState`:
116
+
117
+ | Field | Meaning |
118
+ | --- | --- |
119
+ | `status` | `idle` before the first load, then `loading`, `ready` or `error` |
120
+ | `snapshot` | the current `SettingsSnapshot`, or `null` before the first one |
121
+ | `error` | why the last load failed; cleared by a successful one |
122
+ | `isValidating` | a request is in flight, including a background refresh |
123
+ | `updatedAt` | when the current snapshot arrived |
124
+
125
+ `refresh()` never rejects — the failure lands in `error` — so a caller needs no
126
+ `try`/`catch`. `set()` and `clear()` do reject, because a rejected write is
127
+ something the caller has to react to.
128
+
129
+ ### React
130
+
131
+ There is no React module here, and there does not need to be one. Paste this
132
+ into your app:
133
+
134
+ ```tsx
135
+ import { useSyncExternalStore } from "react";
136
+ import { createSettingsStore, type SettingsStore } from "app-settings-js";
137
+
138
+ const store = createSettingsStore(client, { userId: currentUserId });
139
+
140
+ function useSettings(store: SettingsStore) {
141
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
142
+ }
143
+
144
+ function DarkModeToggle() {
145
+ const { snapshot, status } = useSettings(store);
146
+ if (!snapshot) return status === "error" ? <Failed /> : <Spinner />;
147
+
148
+ return (
149
+ <label>
150
+ <input
151
+ type="checkbox"
152
+ checked={snapshot.boolean("dark_mode")}
153
+ disabled={snapshot.isEnforced("dark_mode")}
154
+ onChange={(event) => store.set("dark_mode", event.target.checked)}
155
+ />
156
+ Dark mode
157
+ {snapshot.visibleOverride("dark_mode") && <Badge>Set by your team</Badge>}
158
+ </label>
159
+ );
160
+ }
161
+ ```
162
+
163
+ `getSnapshot` returns the same object until something actually changes, which is
164
+ exactly the contract `useSyncExternalStore` enforces, and `getServerSnapshot`
165
+ makes the store safe to render on a server.
166
+
167
+ The same two functions adapt anywhere: Svelte's `readable`, Vue's
168
+ `shallowRef` plus `onScopeDispose`, or a Solid signal.
169
+
170
+ ### Server rendering
171
+
172
+ Resolve on the server, serialise the response into the page, and hand it to the
173
+ store so the first render already has data and no request is made:
174
+
175
+ ```ts
176
+ // server
177
+ const settings = await client.resolveUser(userId);
178
+ return { props: { settings: settings.toJSON() } };
179
+
180
+ // client
181
+ const store = createSettingsStore(client, { userId, initialData: props.settings });
182
+ ```
183
+
184
+ ## Writing values
185
+
186
+ Each layer has its own namespace, mirroring the API's routes:
187
+
188
+ ```ts
189
+ await client.values.server.set(settingId, true);
190
+ await client.values.personal.set(settingId, "alice", "solarized");
191
+ await client.values.group.set(settingId, groupId, false, {
192
+ enforced: true, // beats the user's own value
193
+ visible: false, // the user is not meant to observe it
194
+ });
195
+
196
+ await client.values.personal.clear(settingId, "alice");
197
+ ```
198
+
199
+ `undefined` is refused before a request is made — clearing a value is `clear()`,
200
+ which is a `DELETE`. `null` goes through, because it is a legitimate JSON value.
201
+
202
+ ## Definitions, groups, taxonomy and keys
203
+
204
+ ```ts
205
+ await client.settings.list({ scope: "PERSONAL" });
206
+ await client.settings.create({
207
+ name: "page_size",
208
+ type: "NUMBER",
209
+ scope: "PERSONAL",
210
+ platform: "web",
211
+ typeConfig: { min: 10, max: 100, integer: true },
212
+ defaultValue: 25,
213
+ });
214
+ await client.settings.delete(id, { cascade: true }); // required if values exist
215
+
216
+ await client.groups.create({ name: "beta", members: ["alice"], priority: 10 });
217
+ await client.groups.addMembers(groupId, ["bob"]);
218
+
219
+ await client.taxonomy.roles();
220
+ await client.taxonomy.upsertRole("auditor", { rank: 25 });
221
+
222
+ const minted = await client.keys.create({ name: "web backend", scopes: ["resolve"] });
223
+ minted.token; // the only time it is ever available
224
+ ```
225
+
226
+ ## Datetimes
227
+
228
+ `DATETIME` accepts only a strict RFC 3339 value with an offset, because an
229
+ offset is what makes a value an instant rather than a wall-clock reading. A
230
+ `Date` satisfies that automatically; `<input type="datetime-local">` does not.
231
+
232
+ ```ts
233
+ import { localToInstant, parseInstant, toDateTimeLocal, toInstant } from "app-settings-js";
234
+
235
+ toInstant(new Date()); // "2026-01-02T20:04:05.250Z"
236
+ toInstant("2026-01-02T15:04:05-05:00"); // "2026-01-02T20:04:05.000Z"
237
+ toInstant("2026-01-02T15:04"); // throws, naming the problem
238
+
239
+ localToInstant("2026-01-02T15:04", "America/New_York"); // "2026-01-02T20:04:00.000Z"
240
+ toDateTimeLocal(value, "America/New_York"); // back into the input
241
+ parseInstant(settings.value("digest_at")); // a Date
242
+ ```
243
+
244
+ Passing the timezone to `localToInstant` is the point: the same reading is a
245
+ different moment in every zone, so the SDK makes you say which one rather than
246
+ guessing. `store.set()` converts a `Date` for you.
247
+
248
+ ## Errors
249
+
250
+ Everything throws one class. Branch on `code`, which is stable, not on the
251
+ message, which is written for a human.
252
+
253
+ ```ts
254
+ import { AppSettingsError, isNotFound, isForbidden } from "app-settings-js";
255
+
256
+ try {
257
+ await client.settings.get(id);
258
+ } catch (error) {
259
+ if (isNotFound(error)) return null;
260
+ if (isForbidden(error)) throw new Error("this key is fenced out of that environment");
261
+ if (AppSettingsError.is(error)) {
262
+ error.code; // "not_found" | "forbidden" | "network_error" | "timeout" | ...
263
+ error.status; // 404
264
+ error.requestId; // the server's X-Request-ID, worth quoting in a bug report
265
+ error.retryable;
266
+ }
267
+ throw error;
268
+ }
269
+ ```
270
+
271
+ A key fenced out of an environment gets `not_found` rather than `forbidden` for
272
+ a setting inside it — the server declines to reveal that it exists.
273
+
274
+ Failed requests are retried automatically on network errors, 5xx and 429, with
275
+ exponential backoff and jitter, honouring `Retry-After`. `POST` is never
276
+ retried, being the only non-idempotent method in the API. Tune it per client:
277
+
278
+ ```ts
279
+ new AppSettingsClient({ baseUrl, apiKey, retries: 5, retryDelayMs: 100, timeoutMs: 5_000 });
280
+ ```
281
+
282
+ Every call also takes `{ signal, timeoutMs, headers }` for a one-off override.
283
+
284
+ ## Testing against it
285
+
286
+ The client takes any `fetch`, so tests need no network and no mocking library:
287
+
288
+ ```ts
289
+ const client = new AppSettingsClient({
290
+ baseUrl: "https://settings.test",
291
+ apiKey: "as_test",
292
+ environment: "test",
293
+ fetch: async () => new Response(JSON.stringify(fixture), { status: 200 }),
294
+ });
295
+ ```
296
+
297
+ ## Requirements
298
+
299
+ Any runtime with a global `fetch`: **Node 18+**, Bun, Deno, or a browser. Pass
300
+ your own `fetch` for anything older. TypeScript is optional; when used, the
301
+ declarations are checked against **TypeScript 5.9 and 7.x**.
302
+
303
+ ## Development
304
+
305
+ ```sh
306
+ just # list every recipe
307
+ just check # typecheck + test
308
+ just build # ESM, CJS and declarations into dist/
309
+ just test # bun test
310
+ ```
311
+
312
+ From the repository root, `just sdk <recipe>` reaches these, and `just
313
+ check-all` runs the server's checks and the SDK's together.
314
+
315
+ ### Releasing
316
+
317
+ ```sh
318
+ just version 0.2.0 # bump, then write the CHANGELOG entry
319
+ just preflight # tests, build, and the packaging checks below
320
+ just publish # bun publish; prepublishOnly re-runs the gate
321
+ ```
322
+
323
+ `just verify` runs [`@arethetypeswrong/cli`](https://arethetypeswrong.github.io)
324
+ and [`publint`](https://publint.dev) against the real tarball, which is what
325
+ catches the two failure modes this package is shaped around:
326
+
327
+ - **The `require` path needs its own declarations.** `package.json` is
328
+ `type: module`, so a lone `index.d.ts` describes the CommonJS build as ESM.
329
+ The build emits a `.d.cts` beside every `.d.ts` and the `exports` map points
330
+ each condition at the matching one.
331
+ - **`sideEffects` is deliberately absent.** Bun 1.4.0's bundler tree-shakes
332
+ every module out of a pure re-export barrel when that field is present,
333
+ emitting `export { … }` with nothing bound — valid-looking output, a zero
334
+ exit code, and a package that throws `SyntaxError` on import. `bun run build`
335
+ asserts the bundle contains an implementation so this cannot ship again.
@@ -0,0 +1,221 @@
1
+ import { type FetchLike, type RequestOptions, type TransportOptions } from "./http.cjs";
2
+ import { SettingsSnapshot } from "./snapshot.cjs";
3
+ import type { ApiKey, CreatedApiKey, Environment, Group, GroupMember, Health, IntermediateValue, PersonalValue, Platform, Role, Scope, ServerValue, Setting, SettingScope, SettingType, SettingValue, TypeConfig, WhoAmI } from "./types.cjs";
4
+ /** How to reach the server, and what to assume when a call does not say. */
5
+ export interface ClientOptions extends TransportOptions {
6
+ /**
7
+ * The environment used by calls that require one. Resolution always needs an
8
+ * environment, so setting it here means most calls take no options at all.
9
+ */
10
+ environment?: string;
11
+ /** The platform filter applied to resolution and to setting lookups. */
12
+ platform?: string | string[];
13
+ }
14
+ /** Options shared by both resolution calls. */
15
+ export interface ResolveOptions extends RequestOptions {
16
+ /** Overrides the client's environment. Required if the client has none. */
17
+ environment?: string;
18
+ /** Restricts the resolution to these platforms. Capped by the key's own fence. */
19
+ platform?: string | string[];
20
+ /** Resolves as this role instead of the key's. It may not outrank the key. */
21
+ role?: string;
22
+ }
23
+ /** Resolution for one user. */
24
+ export interface ResolveUserOptions extends ResolveOptions {
25
+ /**
26
+ * Groups to apply without storing membership, which is how an ad-hoc group
27
+ * is used. Saved memberships apply regardless.
28
+ */
29
+ groupId?: string | string[];
30
+ }
31
+ /** Filters for listing setting definitions. */
32
+ export interface ListSettingsOptions extends RequestOptions {
33
+ environment?: string;
34
+ platform?: string;
35
+ scope?: SettingScope;
36
+ }
37
+ /** A new setting definition. Type, scope, platform and environment are fixed at creation. */
38
+ export interface CreateSettingInput {
39
+ name: string;
40
+ type: SettingType;
41
+ scope: SettingScope;
42
+ platform: string;
43
+ /** Defaults to the client's environment. */
44
+ environment?: string;
45
+ description?: string;
46
+ /** The rules for this type. See {@link TypeConfig}. */
47
+ typeConfig?: TypeConfig;
48
+ /** Defaults to the calling key's own role. */
49
+ role?: string;
50
+ /** Used when no layer supplies a value. */
51
+ defaultValue?: SettingValue;
52
+ }
53
+ /** The parts of a definition that are safe to change after creation. */
54
+ export interface UpdateSettingInput {
55
+ description?: string;
56
+ typeConfig?: TypeConfig;
57
+ role?: string;
58
+ defaultValue?: SettingValue;
59
+ }
60
+ /** Options for writing a group override. */
61
+ export interface GroupValueOptions extends RequestOptions {
62
+ /** Whether the user is meant to observe the override. Defaults to true. */
63
+ visible?: boolean;
64
+ /** Whether the override beats the user's own value. Defaults to false. */
65
+ enforced?: boolean;
66
+ }
67
+ /** A new group. */
68
+ export interface CreateGroupInput {
69
+ name: string;
70
+ description?: string;
71
+ /** Omit to span every environment, which a fenced key may not do. */
72
+ environment?: string | null;
73
+ /** Higher priority wins when two groups override the same setting. */
74
+ priority?: number;
75
+ /** Marks an ad-hoc group so operators can prune it later. */
76
+ ephemeral?: boolean;
77
+ /** Seeds membership in the same request. */
78
+ members?: string[];
79
+ }
80
+ /** A new API key. It can never reach further than the key that mints it. */
81
+ export interface CreateKeyInput {
82
+ name: string;
83
+ scopes: Scope[];
84
+ /** Empty inherits the creating key's fence rather than granting everything. */
85
+ environments?: string[];
86
+ platforms?: string[];
87
+ /** Defaults to the lowest-ranked role. May not outrank the creating key. */
88
+ role?: string;
89
+ /** Supply at most one of these. */
90
+ expiresAt?: Date | string;
91
+ /** A Go duration such as `"720h"`. */
92
+ expiresIn?: string;
93
+ }
94
+ /**
95
+ * A client for the App Settings API.
96
+ *
97
+ * One instance is cheap and holds no connection state, so it is safe to build
98
+ * once at module scope and share it.
99
+ *
100
+ * @example
101
+ * const client = new AppSettingsClient({
102
+ * baseUrl: "https://settings.example.com",
103
+ * apiKey: process.env.SETTINGS_API_KEY!,
104
+ * environment: "production",
105
+ * });
106
+ *
107
+ * const settings = await client.resolveUser("alice");
108
+ * if (settings.boolean("dark_mode")) { ... }
109
+ */
110
+ export declare class AppSettingsClient {
111
+ #private;
112
+ constructor(options: ClientOptions);
113
+ /** The environment this client defaults to, if it has one. */
114
+ get environment(): string | undefined;
115
+ /** A copy of this client bound to a different environment. */
116
+ withEnvironment(environment: string): AppSettingsClient;
117
+ /**
118
+ * Every setting a user can see, collapsed to one effective value each.
119
+ *
120
+ * This is the call a product backend makes. Precedence, lowest to highest, is
121
+ * `default < server < advisory group < personal < enforced group`.
122
+ */
123
+ resolveUser(userId: string, options?: ResolveUserOptions): Promise<SettingsSnapshot>;
124
+ /** The server's own settings, with no user layer applied. */
125
+ resolveServer(options?: ResolveOptions): Promise<SettingsSnapshot>;
126
+ /** Describes the calling key, so a deployment can confirm what it can do. */
127
+ whoami(options?: RequestOptions): Promise<WhoAmI>;
128
+ /** Whether the process is up. Needs no API key on the server, but sends one. */
129
+ health(options?: RequestOptions): Promise<Health>;
130
+ /** Whether the server's dependencies are reachable. */
131
+ ready(options?: RequestOptions): Promise<Health>;
132
+ readonly settings: {
133
+ /** Every definition this key may see, narrowed by the given filters. */
134
+ list: (options?: ListSettingsOptions) => Promise<Setting[]>;
135
+ /** One definition by id. */
136
+ get: (id: string, options?: RequestOptions) => Promise<Setting>;
137
+ /** Defines a new setting. */
138
+ create: (input: CreateSettingInput, options?: RequestOptions) => Promise<Setting>;
139
+ /** Changes a definition. Omitted fields are left as they are. */
140
+ update: (id: string, input: UpdateSettingInput, options?: RequestOptions) => Promise<Setting>;
141
+ /**
142
+ * Removes a definition.
143
+ *
144
+ * A delete that would destroy stored values is refused with a `conflict`
145
+ * naming how many, unless `cascade` says to go ahead.
146
+ */
147
+ delete: (id: string, options?: RequestOptions & {
148
+ cascade?: boolean;
149
+ }) => Promise<void>;
150
+ };
151
+ readonly values: {
152
+ /** The server-wide layer, beneath every group and user value. */
153
+ server: {
154
+ get: (settingId: string, options?: RequestOptions) => Promise<ServerValue>;
155
+ set: (settingId: string, value: SettingValue, options?: RequestOptions) => Promise<ServerValue>;
156
+ /** Removes the value, falling back to the definition's default. */
157
+ clear: (settingId: string, options?: RequestOptions) => Promise<void>;
158
+ };
159
+ /** One user's own choice. */
160
+ personal: {
161
+ get: (settingId: string, userId: string, options?: RequestOptions) => Promise<PersonalValue>;
162
+ set: (settingId: string, userId: string, value: SettingValue, options?: RequestOptions) => Promise<PersonalValue>;
163
+ clear: (settingId: string, userId: string, options?: RequestOptions) => Promise<void>;
164
+ };
165
+ /** A group override, in either direction. */
166
+ group: {
167
+ get: (settingId: string, groupId: string, options?: RequestOptions) => Promise<IntermediateValue>;
168
+ /**
169
+ * Writes an override. `enforced` decides its direction: an enforced
170
+ * override beats the user's own value, an advisory one yields to it.
171
+ */
172
+ set: (settingId: string, groupId: string, value: SettingValue, options?: GroupValueOptions) => Promise<IntermediateValue>;
173
+ clear: (settingId: string, groupId: string, options?: RequestOptions) => Promise<void>;
174
+ };
175
+ };
176
+ readonly groups: {
177
+ list: (options?: RequestOptions & {
178
+ environment?: string;
179
+ includeEphemeral?: boolean;
180
+ }) => Promise<Group[]>;
181
+ get: (id: string, options?: RequestOptions) => Promise<Group>;
182
+ create: (input: CreateGroupInput, options?: RequestOptions) => Promise<Group>;
183
+ update: (id: string, input: {
184
+ description?: string;
185
+ priority?: number;
186
+ }, options?: RequestOptions) => Promise<Group>;
187
+ delete: (id: string, options?: RequestOptions) => Promise<void>;
188
+ /** Everyone whose membership is saved. Ad-hoc application does not appear here. */
189
+ members: (id: string, options?: RequestOptions) => Promise<GroupMember[]>;
190
+ addMembers: (id: string, userIds: string[], options?: RequestOptions) => Promise<number>;
191
+ removeMembers: (id: string, userIds: string[], options?: RequestOptions) => Promise<number>;
192
+ };
193
+ readonly taxonomy: {
194
+ roles: (options?: RequestOptions) => Promise<Role[]>;
195
+ /** Creates or updates a role. Rank orders roles and may not exceed the key's. */
196
+ upsertRole: (name: string, input: {
197
+ rank: number;
198
+ description?: string;
199
+ }, options?: RequestOptions) => Promise<Role>;
200
+ deleteRole: (name: string, options?: RequestOptions) => Promise<void>;
201
+ platforms: (options?: RequestOptions) => Promise<Platform[]>;
202
+ upsertPlatform: (name: string, description?: string, options?: RequestOptions) => Promise<Platform>;
203
+ deletePlatform: (name: string, options?: RequestOptions) => Promise<void>;
204
+ environments: (options?: RequestOptions) => Promise<Environment[]>;
205
+ upsertEnvironment: (name: string, description?: string, options?: RequestOptions) => Promise<Environment>;
206
+ deleteEnvironment: (name: string, options?: RequestOptions) => Promise<void>;
207
+ };
208
+ readonly keys: {
209
+ list: (options?: RequestOptions & {
210
+ includeRevoked?: boolean;
211
+ }) => Promise<ApiKey[]>;
212
+ /**
213
+ * Mints a key. The returned `token` is the only time it is ever available:
214
+ * only its hash is stored.
215
+ */
216
+ create: (input: CreateKeyInput, options?: RequestOptions) => Promise<CreatedApiKey>;
217
+ /** Revokes a key. This is permanent and takes effect immediately. */
218
+ revoke: (id: string, options?: RequestOptions) => Promise<void>;
219
+ };
220
+ }
221
+ export type { FetchLike, RequestOptions };