@crustjs/store 0.1.1 → 0.2.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/README.md +173 -136
- package/dist/index.d.ts +36 -1
- package/dist/index.js +3 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -25,12 +25,12 @@ pnpm add @crustjs/store
|
|
|
25
25
|
import { createStore, configDir } from "@crustjs/store";
|
|
26
26
|
|
|
27
27
|
const store = createStore({
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
dirPath: configDir("my-cli"),
|
|
29
|
+
fields: {
|
|
30
|
+
theme: { type: "string", default: "light" },
|
|
31
|
+
fontSize: { type: "number", default: 14 },
|
|
32
|
+
verbose: { type: "boolean", default: false },
|
|
33
|
+
},
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
// Read state (returns defaults when no persisted file exists)
|
|
@@ -64,47 +64,41 @@ No explicit generics needed — types are inferred from your `fields` definition
|
|
|
64
64
|
| `cacheDir` | Regenerable cached data | HTTP caches, compiled assets |
|
|
65
65
|
|
|
66
66
|
```ts
|
|
67
|
-
import {
|
|
68
|
-
createStore,
|
|
69
|
-
configDir,
|
|
70
|
-
dataDir,
|
|
71
|
-
stateDir,
|
|
72
|
-
cacheDir,
|
|
73
|
-
} from "@crustjs/store";
|
|
67
|
+
import { createStore, configDir, dataDir, stateDir, cacheDir } from "@crustjs/store";
|
|
74
68
|
|
|
75
69
|
// User preferences → ~/.config/my-cli/config.json
|
|
76
70
|
const config = createStore({
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
dirPath: configDir("my-cli"),
|
|
72
|
+
fields: {
|
|
73
|
+
theme: { type: "string", default: "light" },
|
|
74
|
+
verbose: { type: "boolean", default: false },
|
|
75
|
+
},
|
|
82
76
|
});
|
|
83
77
|
|
|
84
78
|
// App data → ~/.local/share/my-cli/config.json
|
|
85
79
|
const data = createStore({
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
dirPath: dataDir("my-cli"),
|
|
81
|
+
fields: {
|
|
82
|
+
bookmarks: { type: "string", array: true, default: [] },
|
|
83
|
+
},
|
|
90
84
|
});
|
|
91
85
|
|
|
92
86
|
// Runtime state → ~/.local/state/my-cli/config.json
|
|
93
87
|
const state = createStore({
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
88
|
+
dirPath: stateDir("my-cli"),
|
|
89
|
+
fields: {
|
|
90
|
+
lastOpened: { type: "string", default: "" },
|
|
91
|
+
scrollY: { type: "number", default: 0 },
|
|
92
|
+
},
|
|
99
93
|
});
|
|
100
94
|
|
|
101
95
|
// Cache → ~/.cache/my-cli/config.json
|
|
102
96
|
const cache = createStore({
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
97
|
+
dirPath: cacheDir("my-cli"),
|
|
98
|
+
fields: {
|
|
99
|
+
etag: { type: "string", default: "" },
|
|
100
|
+
payload: { type: "string", default: "" },
|
|
101
|
+
},
|
|
108
102
|
});
|
|
109
103
|
```
|
|
110
104
|
|
|
@@ -137,12 +131,55 @@ const store = createStore(options);
|
|
|
137
131
|
|
|
138
132
|
#### Options
|
|
139
133
|
|
|
140
|
-
| Option | Type
|
|
141
|
-
| -------------- |
|
|
142
|
-
| `dirPath` | `string`
|
|
143
|
-
| `name` | `string`
|
|
144
|
-
| `fields` | `FieldsDef`
|
|
145
|
-
| `pruneUnknown` | `boolean`
|
|
134
|
+
| Option | Type | Required | Description |
|
|
135
|
+
| -------------- | ----------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
|
|
136
|
+
| `dirPath` | `string` | Yes | Absolute directory path where the JSON file is stored. |
|
|
137
|
+
| `name` | `string` | No | Store name used as filename (default `"config"` → `config.json`). |
|
|
138
|
+
| `fields` | `FieldsDef` | Yes | Field definitions defining the store's data shape, types, defaults, and optional validation. |
|
|
139
|
+
| `pruneUnknown` | `boolean` | No | Drop unknown persisted keys on read (default `true`). |
|
|
140
|
+
| `access` | `"default" \| "private" \| { file?: number; directory?: number }` | No | Persistence visibility. Use `"private"` for owner-only secret stores, or pass explicit Unix permission bits. |
|
|
141
|
+
|
|
142
|
+
#### Securing secrets
|
|
143
|
+
|
|
144
|
+
Config/state stores that hold tokens or API keys should not be world-readable. Pass `access: "private"` so the file is owner-only on Unix:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const auth = createStore({
|
|
148
|
+
dirPath: configDir("my-cli"),
|
|
149
|
+
name: "auth",
|
|
150
|
+
fields: { token: { type: "string" } },
|
|
151
|
+
access: "private",
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await auth.write({ token: "secret" }); // auth.json is 0600 even under a permissive umask
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`access: "private"` maps to `0600` for the file (`rw-------`) and `0700` for the parent directory (`rwx------`, only when created). The file mode is enforced on the temp file before the atomic rename, so the persisted file is never momentarily group/other-readable.
|
|
158
|
+
|
|
159
|
+
The built-in string presets are intentionally small:
|
|
160
|
+
|
|
161
|
+
| Access | File | Directory | Use case |
|
|
162
|
+
| --------------------- | ----------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
163
|
+
| omitted / `"default"` | Platform default, usually `0644` after a common umask | Platform default, usually `0755` after a common umask | Non-secret stores where the user's environment should decide permissions. |
|
|
164
|
+
| `"private"` | `0600` | `0700` | Secret-bearing stores such as tokens, API keys, and local auth state. |
|
|
165
|
+
|
|
166
|
+
Advanced callers can provide explicit permission bits without adding more named presets:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const store = createStore({
|
|
170
|
+
dirPath: configDir("my-cli"),
|
|
171
|
+
name: "auth",
|
|
172
|
+
fields: { token: { type: "string" } },
|
|
173
|
+
// Owner-only secret store (same as access: "private")
|
|
174
|
+
access: { file: 0o600, directory: 0o700 },
|
|
175
|
+
// Group-readable store; group ownership is managed outside @crustjs/store
|
|
176
|
+
// access: { file: 0o640, directory: 0o750 },
|
|
177
|
+
// World-readable non-secret store
|
|
178
|
+
// access: { file: 0o644, directory: 0o755 },
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Platform behavior:** `access` permission bits are enforced on **macOS and Linux** (and other Unix systems), where POSIX permission bits are checked by the OS. On **Windows** they are **not enforced** — Windows uses ACLs, not Unix bits, so `access: "private"` does not make a file owner-only there. The store does not throw or warn in this case. On Windows, confidentiality relies on the ACL inherited from the parent directory; the per-user profile location resolved by `configDir` / `stateDir` (under `%APPDATA%` / `%LOCALAPPDATA%`) is already restricted to the user. If you need strong secret confidentiality on Windows, use the OS credential store (Windows Credential Manager / DPAPI) rather than file permissions.
|
|
146
183
|
|
|
147
184
|
### `store.read()`
|
|
148
185
|
|
|
@@ -170,8 +207,8 @@ Reads the current effective state, applies the updater function, and atomically
|
|
|
170
207
|
|
|
171
208
|
```ts
|
|
172
209
|
await store.update((current) => ({
|
|
173
|
-
|
|
174
|
-
|
|
210
|
+
...current,
|
|
211
|
+
theme: "dark",
|
|
175
212
|
}));
|
|
176
213
|
```
|
|
177
214
|
|
|
@@ -260,21 +297,21 @@ const dir = configDir("my-cli");
|
|
|
260
297
|
|
|
261
298
|
// Default store → ~/.config/my-cli/config.json
|
|
262
299
|
const settingsStore = createStore({
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
300
|
+
dirPath: dir,
|
|
301
|
+
fields: {
|
|
302
|
+
theme: { type: "string", default: "light" },
|
|
303
|
+
fontSize: { type: "number", default: 14 },
|
|
304
|
+
verbose: { type: "boolean", default: false },
|
|
305
|
+
},
|
|
269
306
|
});
|
|
270
307
|
|
|
271
308
|
// Auth store → ~/.config/my-cli/auth.json
|
|
272
309
|
const authStore = createStore({
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
310
|
+
dirPath: dir,
|
|
311
|
+
name: "auth",
|
|
312
|
+
fields: {
|
|
313
|
+
token: { type: "string", default: "" },
|
|
314
|
+
},
|
|
278
315
|
});
|
|
279
316
|
```
|
|
280
317
|
|
|
@@ -288,12 +325,12 @@ When a persisted file exists, `read()` fills missing keys from field `defaults`:
|
|
|
288
325
|
|
|
289
326
|
```ts
|
|
290
327
|
const store = createStore({
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
328
|
+
dirPath: configDir("my-cli"),
|
|
329
|
+
fields: {
|
|
330
|
+
theme: { type: "string", default: "light" },
|
|
331
|
+
fontSize: { type: "number", default: 14 },
|
|
332
|
+
verbose: { type: "boolean", default: false },
|
|
333
|
+
},
|
|
297
334
|
});
|
|
298
335
|
|
|
299
336
|
// Persisted file contains: { "theme": "dark", "verbose": true }
|
|
@@ -329,11 +366,11 @@ import { z } from "zod";
|
|
|
329
366
|
import { createStore, configDir, field } from "@crustjs/store";
|
|
330
367
|
|
|
331
368
|
const store = createStore({
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
369
|
+
dirPath: configDir("my-cli"),
|
|
370
|
+
fields: {
|
|
371
|
+
theme: field(z.enum(["light", "dark"]).default("light")),
|
|
372
|
+
verbose: { type: "boolean", default: false },
|
|
373
|
+
},
|
|
337
374
|
});
|
|
338
375
|
|
|
339
376
|
// write() validates before persisting
|
|
@@ -352,10 +389,10 @@ import * as Schema from "effect/Schema";
|
|
|
352
389
|
import { createStore, configDir, field } from "@crustjs/store";
|
|
353
390
|
|
|
354
391
|
const store = createStore({
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
392
|
+
dirPath: configDir("my-cli"),
|
|
393
|
+
fields: {
|
|
394
|
+
theme: field(Schema.standardSchemaV1(Schema.Literal("light", "dark"))),
|
|
395
|
+
},
|
|
359
396
|
});
|
|
360
397
|
```
|
|
361
398
|
|
|
@@ -397,18 +434,18 @@ issue path.
|
|
|
397
434
|
|
|
398
435
|
```ts
|
|
399
436
|
const store = createStore({
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
437
|
+
dirPath: configDir("my-cli"),
|
|
438
|
+
fields: {
|
|
439
|
+
theme: {
|
|
440
|
+
type: "string",
|
|
441
|
+
default: "light",
|
|
442
|
+
validate(value) {
|
|
443
|
+
if (value !== "light" && value !== "dark") {
|
|
444
|
+
throw new Error('Must be "light" or "dark"');
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
},
|
|
412
449
|
});
|
|
413
450
|
```
|
|
414
451
|
|
|
@@ -450,15 +487,15 @@ with `CrustStoreError("VALIDATION")` and an issue message tagged
|
|
|
450
487
|
import { CrustStoreError } from "@crustjs/store";
|
|
451
488
|
|
|
452
489
|
try {
|
|
453
|
-
|
|
490
|
+
await store.read();
|
|
454
491
|
} catch (err) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
492
|
+
if (err instanceof CrustStoreError && err.is("VALIDATION")) {
|
|
493
|
+
// err.details is { operation: "read" | "write" | "update" | "patch", issues: StoreValidatorIssue[] }
|
|
494
|
+
console.error(`Validation failed during ${err.details.operation}:`);
|
|
495
|
+
for (const issue of err.details.issues) {
|
|
496
|
+
console.error(` ${issue.path || "(root)"}: ${issue.message}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
462
499
|
}
|
|
463
500
|
```
|
|
464
501
|
|
|
@@ -466,13 +503,13 @@ try {
|
|
|
466
503
|
|
|
467
504
|
All errors thrown by `@crustjs/store` are instances of `CrustStoreError` with a typed `code` property:
|
|
468
505
|
|
|
469
|
-
| Code | When
|
|
470
|
-
| ------------ |
|
|
471
|
-
| `PATH` | Invalid `dirPath`, invalid `name`, unsupported platform
|
|
472
|
-
| `PARSE` | Malformed JSON in persisted config file
|
|
473
|
-
| `IO` | Filesystem read, write, or delete failure
|
|
474
|
-
| `VALIDATION` | Config fails validator on read, write, update, or patch
|
|
475
|
-
| `DEFINITION` | `field()` received an invalid (non-Standard-Schema) input | `{ vendor? }`
|
|
506
|
+
| Code | When | Details |
|
|
507
|
+
| ------------ | --------------------------------------------------------- | ----------------------- |
|
|
508
|
+
| `PATH` | Invalid `dirPath`, invalid `name`, unsupported platform | `{ path: string }` |
|
|
509
|
+
| `PARSE` | Malformed JSON in persisted config file | `{ path: string }` |
|
|
510
|
+
| `IO` | Filesystem read, write, or delete failure | `{ path, operation }` |
|
|
511
|
+
| `VALIDATION` | Config fails validator on read, write, update, or patch | `{ operation, issues }` |
|
|
512
|
+
| `DEFINITION` | `field()` received an invalid (non-Standard-Schema) input | `{ vendor? }` |
|
|
476
513
|
|
|
477
514
|
### Catching errors by code
|
|
478
515
|
|
|
@@ -480,19 +517,19 @@ All errors thrown by `@crustjs/store` are instances of `CrustStoreError` with a
|
|
|
480
517
|
import { CrustStoreError } from "@crustjs/store";
|
|
481
518
|
|
|
482
519
|
try {
|
|
483
|
-
|
|
520
|
+
const state = await store.read();
|
|
484
521
|
} catch (err) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
522
|
+
if (err instanceof CrustStoreError) {
|
|
523
|
+
switch (err.code) {
|
|
524
|
+
case "PARSE":
|
|
525
|
+
console.error(`Corrupt file at ${err.details.path}`);
|
|
526
|
+
console.error("Delete the file and retry, or fix the JSON manually.");
|
|
527
|
+
break;
|
|
528
|
+
case "IO":
|
|
529
|
+
console.error(`File ${err.details.operation} failed: ${err.message}`);
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
496
533
|
}
|
|
497
534
|
```
|
|
498
535
|
|
|
@@ -502,8 +539,8 @@ The `.is()` method narrows the error type so `details` is fully typed:
|
|
|
502
539
|
|
|
503
540
|
```ts
|
|
504
541
|
if (err instanceof CrustStoreError && err.is("IO")) {
|
|
505
|
-
|
|
506
|
-
|
|
542
|
+
// err.details is { path: string; operation: "read" | "write" | "delete" }
|
|
543
|
+
console.error(err.details.operation, err.details.path);
|
|
507
544
|
}
|
|
508
545
|
```
|
|
509
546
|
|
|
@@ -513,7 +550,7 @@ if (err instanceof CrustStoreError && err.is("IO")) {
|
|
|
513
550
|
|
|
514
551
|
```ts
|
|
515
552
|
if (err instanceof CrustStoreError && err.is("VALIDATION")) {
|
|
516
|
-
|
|
553
|
+
console.error("Validation error:", err.cause);
|
|
517
554
|
}
|
|
518
555
|
```
|
|
519
556
|
|
|
@@ -527,38 +564,38 @@ All types are exported for use in your application:
|
|
|
527
564
|
|
|
528
565
|
```ts
|
|
529
566
|
import type {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
567
|
+
CreateStoreOptions,
|
|
568
|
+
Store,
|
|
569
|
+
StoreUpdater,
|
|
570
|
+
FieldDef,
|
|
571
|
+
FieldsDef,
|
|
572
|
+
FieldOptions,
|
|
573
|
+
InferStoreConfig,
|
|
574
|
+
StoreValidatorIssue,
|
|
575
|
+
StoreErrorCode,
|
|
576
|
+
ValidationErrorDetails,
|
|
577
|
+
DefinitionErrorDetails,
|
|
578
|
+
ValueType,
|
|
579
|
+
PlatformEnv,
|
|
543
580
|
} from "@crustjs/store";
|
|
544
581
|
```
|
|
545
582
|
|
|
546
|
-
| Type | Description
|
|
547
|
-
| ------------------------ |
|
|
548
|
-
| `CreateStoreOptions` | Options object for `createStore()`.
|
|
549
|
-
| `Store` | Store instance with `read`, `write`, `update`, `patch`, `reset`.
|
|
550
|
-
| `StoreUpdater` | Updater function type `(current: T) => T`.
|
|
551
|
-
| `FieldDef` | Single field definition with `type`, optional `default`, and optional `validate`.
|
|
552
|
-
| `FieldsDef` | Record of field names to `FieldDef` definitions.
|
|
583
|
+
| Type | Description |
|
|
584
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------ |
|
|
585
|
+
| `CreateStoreOptions` | Options object for `createStore()`. |
|
|
586
|
+
| `Store` | Store instance with `read`, `write`, `update`, `patch`, `reset`. |
|
|
587
|
+
| `StoreUpdater` | Updater function type `(current: T) => T`. |
|
|
588
|
+
| `FieldDef` | Single field definition with `type`, optional `default`, and optional `validate`. |
|
|
589
|
+
| `FieldsDef` | Record of field names to `FieldDef` definitions. |
|
|
553
590
|
| `FieldOptions` | Optional Crust metadata accepted by the `field()` factory (`type`, `default`, `array`, `description`). |
|
|
554
|
-
| `InferStoreConfig` | Inferred store state type from a `FieldsDef` definition.
|
|
555
|
-
| `StoreValidatorIssue` | `{ message: string, path: string }`.
|
|
556
|
-
| `ValidationErrorDetails` | Error details for `VALIDATION` code: `{ operation, issues }`.
|
|
557
|
-
| `DefinitionErrorDetails` | Error details for `DEFINITION` code: `{ vendor? }`.
|
|
558
|
-
| `StoreErrorCode` | Union of error codes: `"PATH" \| "PARSE" \| "IO" \| "VALIDATION" \| "DEFINITION"`.
|
|
559
|
-
| `ValueType` | Supported field type literals: `"string" \| "number" \| "boolean"`.
|
|
560
|
-
| `PlatformEnv` | Injectable platform environment for testing path helpers.
|
|
561
|
-
| `CrustStoreError` | Typed error class with `code`, `details`, and `cause`.
|
|
591
|
+
| `InferStoreConfig` | Inferred store state type from a `FieldsDef` definition. |
|
|
592
|
+
| `StoreValidatorIssue` | `{ message: string, path: string }`. |
|
|
593
|
+
| `ValidationErrorDetails` | Error details for `VALIDATION` code: `{ operation, issues }`. |
|
|
594
|
+
| `DefinitionErrorDetails` | Error details for `DEFINITION` code: `{ vendor? }`. |
|
|
595
|
+
| `StoreErrorCode` | Union of error codes: `"PATH" \| "PARSE" \| "IO" \| "VALIDATION" \| "DEFINITION"`. |
|
|
596
|
+
| `ValueType` | Supported field type literals: `"string" \| "number" \| "boolean"`. |
|
|
597
|
+
| `PlatformEnv` | Injectable platform environment for testing path helpers. |
|
|
598
|
+
| `CrustStoreError` | Typed error class with `code`, `details`, and `cause`. |
|
|
562
599
|
|
|
563
600
|
## Scope & Non-Goals
|
|
564
601
|
|
package/dist/index.d.ts
CHANGED
|
@@ -161,6 +161,22 @@ interface StoreValidatorIssue {
|
|
|
161
161
|
/** Field name where the issue occurred. */
|
|
162
162
|
path: string;
|
|
163
163
|
}
|
|
164
|
+
/** Explicit Unix permission bits for store persistence. */
|
|
165
|
+
interface StorePermissionBits {
|
|
166
|
+
/** Permission bits for the persisted store file (e.g. `0o600`). */
|
|
167
|
+
file?: number;
|
|
168
|
+
/** Permission bits for the store's parent directory (e.g. `0o700`). */
|
|
169
|
+
directory?: number;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Persistence visibility for store writes.
|
|
173
|
+
*
|
|
174
|
+
* - `"default"` or omitted uses platform defaults.
|
|
175
|
+
* - `"private"` keeps the store owner-only on Unix (`0600` file, `0700` directory).
|
|
176
|
+
* - An object lets advanced callers provide explicit Unix permission bits for
|
|
177
|
+
* group-readable or public non-secret stores without expanding the preset list.
|
|
178
|
+
*/
|
|
179
|
+
type StoreAccess = "default" | "private" | StorePermissionBits;
|
|
164
180
|
/**
|
|
165
181
|
* Options for {@link createStore}.
|
|
166
182
|
*
|
|
@@ -210,6 +226,25 @@ interface CreateStoreOptions<F extends FieldsDef> {
|
|
|
210
226
|
* @default true
|
|
211
227
|
*/
|
|
212
228
|
pruneUnknown?: boolean;
|
|
229
|
+
/**
|
|
230
|
+
* Persistence visibility for store writes.
|
|
231
|
+
*
|
|
232
|
+
* Use `"private"` for config/state stores holding secrets (tokens, API keys)
|
|
233
|
+
* that should stay owner-only. On Unix this writes the file as `0600` and
|
|
234
|
+
* creates the parent directory as `0700`; on Windows it is not enforced
|
|
235
|
+
* because Windows uses ACLs instead of Unix permission bits.
|
|
236
|
+
*
|
|
237
|
+
* The only built-in presets are `"default"` and `"private"`. Advanced callers
|
|
238
|
+
* can pass explicit bits with `{ file, directory }` for group-readable or public
|
|
239
|
+
* non-secret stores. The file bits are enforced exactly on every write
|
|
240
|
+
* regardless of `umask`. Directory bits apply only when a write creates the
|
|
241
|
+
* directory; pre-existing directories are left untouched.
|
|
242
|
+
*
|
|
243
|
+
* @default "default"
|
|
244
|
+
* @example "private"
|
|
245
|
+
* @example { file: 0o600, directory: 0o700 }
|
|
246
|
+
*/
|
|
247
|
+
access?: StoreAccess;
|
|
213
248
|
}
|
|
214
249
|
/**
|
|
215
250
|
* Receives the current effective config and returns an updated config.
|
|
@@ -756,4 +791,4 @@ declare function cacheDir(appName: string, env?: PlatformEnv): string;
|
|
|
756
791
|
* ```
|
|
757
792
|
*/
|
|
758
793
|
declare function createStore<const F extends FieldsDef>(options: CreateStoreOptions<F>): Store<InferStoreConfig<F>>;
|
|
759
|
-
export { stateDir, field, dataDir, createStore, configDir, cacheDir, ValueType, ValidationErrorDetails, StoreValidatorIssue, StoreUpdater, StoreErrorCode, Store, PlatformEnv, InferStoreConfig, FieldsDef, FieldOptions, FieldDef, DefinitionErrorDetails, CrustStoreError, CreateStoreOptions };
|
|
794
|
+
export { stateDir, field, dataDir, createStore, configDir, cacheDir, ValueType, ValidationErrorDetails, StoreValidatorIssue, StoreUpdater, StorePermissionBits, StoreErrorCode, StoreAccess, Store, PlatformEnv, InferStoreConfig, FieldsDef, FieldOptions, FieldDef, DefinitionErrorDetails, CrustStoreError, CreateStoreOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
class X extends Error{code;details;cause;constructor(q,H,G){super(H);this.name="CrustStoreError",this.code=q,this.details=G}is(q){return this.code===q}withCause(q){return this.cause=q,this}}import{isStandardSchema as
|
|
3
|
-
${
|
|
4
|
-
`)}`;throw new X("VALIDATION",
|
|
2
|
+
class X extends Error{code;details;cause;constructor(q,H,G){super(H);this.name="CrustStoreError",this.code=q,this.details=G}is(q){return this.code===q}withCause(q){return this.cause=q,this}}import{isStandardSchema as d,normalizeStandardIssues as o}from"@crustjs/utils/schema";function l(q){return async(H)=>{let G=await q["~standard"].validate(H);if(G.issues){let Q=o(G.issues).map((Y)=>Y.path?`${Y.path}: ${Y.message}`:Y.message);throw Error(Q.join("; "))}return{value:G.value}}}function i(q,H){if(!d(q))throw new X("DEFINITION",`field(): argument must be a Standard Schema v1 object (got ${typeof q})`,{});let K={validate:l(q),validateMissing:!0};if(H?.type!==void 0)K.type=H.type;if(H?.array===!0)K.array=!0;if(H?.description!==void 0)K.description=H.description;if(H&&"default"in H)K.default=H.default;return K}import{homedir as s}from"os";import{join as J}from"path";var t="config";function b(){return{platform:process.platform,env:process.env,homedir:s()}}function j(q){if(!q||q.trim().length===0)throw new X("PATH","appName must be a non-empty string",{path:q??""});if(q.includes("/")||q.includes("\\"))throw new X("PATH","appName must not contain path separators",{path:q})}function r(q){if(!q||q.trim().length===0)throw new X("PATH","name must be a non-empty string",{path:q??""});if(q.includes("/")||q.includes("\\"))throw new X("PATH","name must not contain path separators",{path:q});if(q.endsWith(".json"))throw new X("PATH","name must not include the .json extension",{path:q})}function a(q){if(!q||q.trim().length===0)throw new X("PATH","dirPath must be a non-empty string",{path:q??""});if(!(q.startsWith("/")||/^[A-Za-z]:[/\\]/.test(q)))throw new X("PATH","dirPath must be an absolute path",{path:q});if(q.endsWith(".json"))throw new X("PATH","dirPath must be a directory path, not a file path (should not end with .json)",{path:q})}function C(q,H,G,K){let Q=q.env[H],Y=Q&&Q.trim().length>0?Q:J(q.homedir,...G);return J(Y,K)}function D(q){throw new X("PATH",`Unsupported platform: ${q}`,{path:q})}function e(q,H){j(q);let G=H??b();switch(G.platform){case"linux":case"darwin":return C(G,"XDG_CONFIG_HOME",[".config"],q);case"win32":{let K=G.env.APPDATA,Q=K&&K.trim().length>0?K:J(G.homedir,"AppData","Roaming");return J(Q,q)}default:D(G.platform)}}function qq(q,H){j(q);let G=H??b();switch(G.platform){case"linux":case"darwin":return C(G,"XDG_DATA_HOME",[".local","share"],q);case"win32":{let K=G.env.LOCALAPPDATA,Q=K&&K.trim().length>0?K:J(G.homedir,"AppData","Local");return J(Q,q,"Data")}default:D(G.platform)}}function Gq(q,H){j(q);let G=H??b();switch(G.platform){case"linux":case"darwin":return C(G,"XDG_STATE_HOME",[".local","state"],q);case"win32":{let K=G.env.LOCALAPPDATA,Q=K&&K.trim().length>0?K:J(G.homedir,"AppData","Local");return J(Q,q,"State")}default:D(G.platform)}}function Hq(q,H){j(q);let G=H??b();switch(G.platform){case"linux":case"darwin":return C(G,"XDG_CACHE_HOME",[".cache"],q);case"win32":{let K=G.env.LOCALAPPDATA,Q=K&&K.trim().length>0?K:J(G.homedir,"AppData","Local");return J(Q,q,"Cache")}default:D(G.platform)}}function S(q,H){a(q);let G=H??t;if(H!==void 0)r(H);return J(q,`${G}.json`)}import{coerceBooleanString as Iq,tryCoerceNumber as Wq}from"@crustjs/utils";function y(q,H,G=!0){let K={};for(let[Q,Y]of Object.entries(H))if(q&&Q in q){let Z=q[Q];K[Q]=Array.isArray(Z)?[...Z]:Z}else if("default"in Y&&Y.default!==void 0)K[Q]=Array.isArray(Y.default)?[...Y.default]:Y.default;if(!G&&q!==void 0){for(let[Q,Y]of Object.entries(q))if(!(Q in H))K[Q]=Array.isArray(Y)?[...Y]:Y}return K}import{randomUUID as Kq}from"crypto";import{chmod as g,mkdir as Qq,readFile as Xq,rename as Yq,rm as $q,unlink as Lq,writeFile as Bq}from"fs/promises";import{dirname as Zq,join as _q}from"path";async function E(q){let H;try{H=await Xq(q,"utf-8")}catch(G){if(v(G))return;throw new X("IO",`Failed to read config file: ${q}`,{path:q,operation:"read"}).withCause(G)}try{return JSON.parse(H)}catch(G){throw new X("PARSE",`Malformed JSON in config file: ${q}`,{path:q}).withCause(G)}}async function z(q,H,G={}){let K=process.platform==="win32",Q=K?void 0:G.fileMode,Y=K?void 0:G.directoryMode,Z=Zq(q),T=_q(Z,`.config-${Kq()}.tmp`),V;try{V=await Qq(Z,{recursive:!0,mode:Y})}catch(I){throw new X("IO",`Failed to create config directory: ${Z}`,{path:q,operation:"write"}).withCause(I)}if(Y!==void 0&&V!==void 0)try{await g(Z,Y)}catch(I){throw new X("IO",`Failed to set permissions on config directory: ${Z}`,{path:q,operation:"write"}).withCause(I)}try{let I=JSON.stringify(H,null,"\t");if(await Bq(T,I,{encoding:"utf-8",mode:Q}),Q!==void 0)await g(T,Q)}catch(I){throw await N(T),new X("IO",`Failed to write config file: ${q}`,{path:q,operation:"write"}).withCause(I)}try{await Yq(T,q)}catch(I){throw await N(T),new X("IO",`Failed to finalize config file: ${q}`,{path:q,operation:"write"}).withCause(I)}}async function P(q){try{await Lq(q)}catch(H){if(v(H))return;throw new X("IO",`Failed to delete config file: ${q}`,{path:q,operation:"delete"}).withCause(H)}}function v(q){return q!==null&&typeof q==="object"&&"code"in q&&q.code==="ENOENT"}async function N(q){try{await $q(q,{force:!0})}catch{}}function f(q){return typeof q==="object"&&q!==null&&"value"in q&&!("ok"in q)}function Jq(q){if(q==="private")return{fileMode:384,directoryMode:448};if(q===void 0||q==="default")return{};return{fileMode:q.file,directoryMode:q.directory}}function Oq(q){let{dirPath:H,name:G,fields:K,pruneUnknown:Q,access:Y}=q,Z=S(H,G),T=Q??!0,V=Jq(Y);function I($,L){if(L===void 0)return $;if(L==="number"&&typeof $==="string")return Wq($)??$;if(L==="boolean"&&typeof $==="string")return Iq($);return $}function R($){let B={...$};for(let[_,W]of Object.entries(K)){if(!(_ in B))continue;let O=B[_];if(W.array===!0&&Array.isArray(O)){B[_]=O.map((M)=>I(M,W.type));continue}B[_]=I(O,W.type)}return B}async function U($,L){let B=[],_=$;for(let[W,O]of Object.entries(K)){if(!O.validate)continue;let M=_[W];if(M===void 0&&O.validateMissing!==!0)continue;let A;try{A=await O.validate(M)}catch(x){let F=x instanceof Error?x.message:"Validation failed";B.push({message:F,path:W});continue}if(A===void 0)continue;if(f(A)){let x=A.value;if(L==="read"){if(M===void 0)_[W]=x;continue}if(!Bun.deepEquals(x,M)){let F;try{F=await O.validate(x)}catch(w){let n=w instanceof Error?w.message:"re-validation failed";B.push({message:`read-unstable transform: ${n}`,path:W});continue}if(F!==void 0&&(!f(F)||!Bun.deepEquals(F.value,x))){B.push({message:"read-unstable transform: output would be transformed again on re-read",path:W});continue}_[W]=x}continue}throw TypeError("FieldDef.validate must return void or { value } (see @crustjs/store validation docs).")}if(B.length>0){let W=B.map((M)=>` - ${M.path}: ${M.message}`),O=`Store validation failed (${L})
|
|
3
|
+
${W.join(`
|
|
4
|
+
`)}`;throw new X("VALIDATION",O,{operation:L,issues:B})}}async function k(){let $=await E(Z),L=y($,K,T);return R(L)}async function c(){let $=await k();return await U($,"read"),$}async function h($){let L=R($);await U(L,"write"),await z(Z,L,V)}async function u($){let L=await k(),B=$(L),_=R(B);await U(_,"update"),await z(Z,_,V)}async function m($){let B={...await k(),...$},_=R(B);await U(_,"patch"),await z(Z,_,V)}async function p(){await P(Z)}return{read:c,write:h,update:u,patch:m,reset:p}}export{Gq as stateDir,i as field,qq as dataDir,Oq as createStore,e as configDir,Hq as cacheDir,X as CrustStoreError};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crustjs/store",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "DX-first, typed persistence for CLI apps with config/data/state/cache separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"typescript": "^6.0.3"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@crustjs/utils": "0.0.
|
|
58
|
+
"@crustjs/utils": "0.0.3",
|
|
59
|
+
"@standard-schema/spec": "^1.1.0"
|
|
59
60
|
}
|
|
60
61
|
}
|