zen-fs-config 0.5.21 → 0.5.24

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 CHANGED
@@ -26,6 +26,28 @@ npm install zen-fs-config @zenfs/core @zenfs/dom zen-fs-sync
26
26
 
27
27
  > `@zenfs/dom` provides the IndexedDB backend (required in browser environments). `zen-fs-cache` is an optional dependency for remote request caching.
28
28
 
29
+ ## Usage via `<script>` tag (no build step)
30
+
31
+ A self-contained browser bundle is published at `dist/zen-fs-config.js`. It bundles **all** dependencies (`@zenfs/core`, `@zenfs/dom`, `zen-fs-sync`, `zen-fs-cache`) and exposes the library on the global `window.ZenFSConfig`. No npm install, no bundler — just drop it into any HTML page:
32
+
33
+ ```html
34
+ <script src="https://unpkg.com/zen-fs-config/dist/zen-fs-config.js"></script>
35
+ <script>
36
+ (async () => {
37
+ const { createConfigRepo } = window.ZenFSConfig;
38
+
39
+ // IndexedDB primary backend is created automatically
40
+ const repo = await createConfigRepo('my-app');
41
+
42
+ repo.setConfig('greeting.json', { msg: 'hello' });
43
+ const cfg = await repo.getConfig('greeting.json');
44
+ console.log(cfg); // { msg: 'hello' }
45
+ })();
46
+ </script>
47
+ ```
48
+
49
+ > The browser bundle includes a pure-JS SHA-256 fallback, so version tracking works even in non-secure contexts (plain HTTP) where `crypto.subtle` is unavailable.
50
+
29
51
  ## Quick Start
30
52
 
31
53
  ### 1. Initialize (zero-configuration)
package/README.zh-CN.md CHANGED
@@ -14,6 +14,28 @@ npm install zen-fs-config @zenfs/core @zenfs/dom zen-fs-sync
14
14
 
15
15
  > `@zenfs/dom` 提供 IndexedDB 后端(浏览器环境必需)。`zen-fs-cache` 为可选依赖。
16
16
 
17
+ ## 通过 `<script>` 标签直接使用(无需构建)
18
+
19
+ 包内置了一个自包含的浏览器构建 `dist/zen-fs-config.js`,它把**所有**依赖(`@zenfs/core`、`@zenfs/dom`、`zen-fs-sync`、`zen-fs-cache`)打包在一起,并将库挂载到全局变量 `window.ZenFSConfig`。无需 npm install、无需打包工具,直接放入任意 HTML 页面即可:
20
+
21
+ ```html
22
+ <script src="https://unpkg.com/zen-fs-config/dist/zen-fs-config.js"></script>
23
+ <script>
24
+ (async () => {
25
+ const { createConfigRepo } = window.ZenFSConfig;
26
+
27
+ // 自动创建 IndexedDB 主后端
28
+ const repo = await createConfigRepo('my-app');
29
+
30
+ repo.setConfig('greeting.json', { msg: 'hello' });
31
+ const cfg = await repo.getConfig('greeting.json');
32
+ console.log(cfg); // { msg: 'hello' }
33
+ })();
34
+ </script>
35
+ ```
36
+
37
+ > 浏览器构建包含纯 JS 实现的 SHA-256 回退,因此即使在 `crypto.subtle` 不可用的非安全上下文(普通 HTTP)中,版本追踪也能正常工作。
38
+
17
39
  ## 快速开始
18
40
 
19
41
  ### 1. 初始化(零参数)
package/dist/index.d.mts CHANGED
@@ -349,8 +349,12 @@ declare function createSerializerChain(custom?: ConfigSerializer): PathAwareSeri
349
349
  /**
350
350
  * Map a config key to a file path.
351
351
  *
352
- * - `/db/host` → `/db/host.json` (append .json if no extension)
353
- * - `/readme.md` → `/readme.md` (preserve existing extension)
352
+ * Always returns a path with a leading `/` so that callers can safely
353
+ * concatenate it with a directory prefix (e.g. `/${appId}${filePath}`).
354
+ *
355
+ * - `db/host` → `/db/host.json` (append .json if no extension)
356
+ * - `/readme.md` → `/readme.md` (preserve existing extension)
357
+ * - `notes` → `/notes.json`
354
358
  */
355
359
  declare function configKeyToFilePath(configPath: string): string;
356
360
  /**
@@ -758,6 +762,11 @@ declare function versionPathFor(configFilePath: string): string | null;
758
762
  /**
759
763
  * Compute SHA-256 hash of a Uint8Array.
760
764
  * Returns "sha256:" prefix + hex digest.
765
+ *
766
+ * Priority:
767
+ * 1. Web Crypto (crypto.subtle) — secure contexts (HTTPS, localhost)
768
+ * 2. Node.js crypto — server-side
769
+ * 3. Pure-JS fallback — non-secure browser contexts (HTTP, some WebViews)
761
770
  */
762
771
  declare function sha256(data: Uint8Array): Promise<string>;
763
772
  /**
package/dist/index.d.ts CHANGED
@@ -349,8 +349,12 @@ declare function createSerializerChain(custom?: ConfigSerializer): PathAwareSeri
349
349
  /**
350
350
  * Map a config key to a file path.
351
351
  *
352
- * - `/db/host` → `/db/host.json` (append .json if no extension)
353
- * - `/readme.md` → `/readme.md` (preserve existing extension)
352
+ * Always returns a path with a leading `/` so that callers can safely
353
+ * concatenate it with a directory prefix (e.g. `/${appId}${filePath}`).
354
+ *
355
+ * - `db/host` → `/db/host.json` (append .json if no extension)
356
+ * - `/readme.md` → `/readme.md` (preserve existing extension)
357
+ * - `notes` → `/notes.json`
354
358
  */
355
359
  declare function configKeyToFilePath(configPath: string): string;
356
360
  /**
@@ -758,6 +762,11 @@ declare function versionPathFor(configFilePath: string): string | null;
758
762
  /**
759
763
  * Compute SHA-256 hash of a Uint8Array.
760
764
  * Returns "sha256:" prefix + hex digest.
765
+ *
766
+ * Priority:
767
+ * 1. Web Crypto (crypto.subtle) — secure contexts (HTTPS, localhost)
768
+ * 2. Node.js crypto — server-side
769
+ * 3. Pure-JS fallback — non-secure browser contexts (HTTP, some WebViews)
761
770
  */
762
771
  declare function sha256(data: Uint8Array): Promise<string>;
763
772
  /**
package/dist/index.js CHANGED
@@ -144,9 +144,11 @@ function createSerializerChain(custom) {
144
144
  };
145
145
  }
146
146
  function configKeyToFilePath(configPath) {
147
- const ext = getExtension(configPath);
148
- if (ext !== "") return configPath;
149
- return configPath.endsWith("/") ? configPath : `${configPath}.json`;
147
+ let p = configPath;
148
+ if (!p.startsWith("/")) p = "/" + p;
149
+ const ext = getExtension(p);
150
+ if (ext !== "") return p;
151
+ return p.endsWith("/") ? p : `${p}.json`;
150
152
  }
151
153
  function getExtension(path) {
152
154
  const lastSlash = path.lastIndexOf("/");
@@ -262,6 +264,20 @@ async function ensureParentDir(absolutePath) {
262
264
  }
263
265
 
264
266
  // src/adapters.ts
267
+ function toUint8Array(data) {
268
+ if (data instanceof Uint8Array) return data;
269
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
270
+ if (typeof data === "string") return new TextEncoder().encode(data);
271
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
272
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
273
+ }
274
+ return new Uint8Array(data);
275
+ }
276
+ function toString(data, encoding) {
277
+ if (typeof data === "string") return data;
278
+ const bytes = toUint8Array(data);
279
+ return new TextDecoder().decode(bytes);
280
+ }
265
281
  function backendToSyncableFS(backend, name) {
266
282
  const syncable = {
267
283
  async readdir(path) {
@@ -270,15 +286,9 @@ function backendToSyncableFS(backend, name) {
270
286
  async readFile(path, encoding) {
271
287
  const result = await backend.readFile(path, encoding);
272
288
  if (encoding) {
273
- if (typeof result === "string") return result;
274
- if (result instanceof Uint8Array) return new TextDecoder().decode(result);
275
- if (Buffer.isBuffer(result)) return result.toString(encoding);
276
- return String(result);
289
+ return toString(result, encoding);
277
290
  }
278
- if (typeof result === "string") return Buffer.from(result);
279
- if (result instanceof Uint8Array) return Buffer.from(result);
280
- if (Buffer.isBuffer(result)) return result;
281
- return Buffer.from(String(result));
291
+ return toUint8Array(result);
282
292
  },
283
293
  async writeFile(path, data) {
284
294
  return backend.writeFile(path, data);
@@ -499,20 +509,150 @@ function versionPathFor(configFilePath) {
499
509
  const versionFileName = fileName.startsWith(".") ? `${fileName}.version` : `.${fileName}.version`;
500
510
  return dir ? `${dir}/${versionFileName}` : versionFileName;
501
511
  }
512
+ function sha256PureJS(data) {
513
+ const K = new Uint32Array([
514
+ 1116352408,
515
+ 1899447441,
516
+ 3049323471,
517
+ 3921009573,
518
+ 961987163,
519
+ 1508970993,
520
+ 2453635748,
521
+ 2870763221,
522
+ 3624381080,
523
+ 310598401,
524
+ 607225278,
525
+ 1426881987,
526
+ 1925078388,
527
+ 2162078206,
528
+ 2614888103,
529
+ 3248222580,
530
+ 3835390401,
531
+ 4022224774,
532
+ 264347078,
533
+ 604807628,
534
+ 770255983,
535
+ 1249150122,
536
+ 1555081692,
537
+ 1996064986,
538
+ 2554220882,
539
+ 2821834349,
540
+ 2952996808,
541
+ 3210313671,
542
+ 3336571891,
543
+ 3584528711,
544
+ 113926993,
545
+ 338241895,
546
+ 666307205,
547
+ 773529912,
548
+ 1294757372,
549
+ 1396182291,
550
+ 1695183700,
551
+ 1986661051,
552
+ 2177026350,
553
+ 2456956037,
554
+ 2730485921,
555
+ 2820302411,
556
+ 3259730800,
557
+ 3345764771,
558
+ 3516065817,
559
+ 3600352804,
560
+ 4094571909,
561
+ 275423344,
562
+ 430227734,
563
+ 506948616,
564
+ 659060556,
565
+ 883997877,
566
+ 958139571,
567
+ 1322822218,
568
+ 1537002063,
569
+ 1747873779,
570
+ 1955562222,
571
+ 2024104815,
572
+ 2227730452,
573
+ 2361852424,
574
+ 2428436474,
575
+ 2756734187,
576
+ 3204031479,
577
+ 3329325298
578
+ ]);
579
+ const H = new Uint32Array([
580
+ 1779033703,
581
+ 3144134277,
582
+ 1013904242,
583
+ 2773480762,
584
+ 1359893119,
585
+ 2600822924,
586
+ 528734635,
587
+ 1541459225
588
+ ]);
589
+ const len = data.length;
590
+ const bitLen = len * 8;
591
+ const padLen = (len + 8 >> 6 << 6) + 64;
592
+ const buf = new Uint8Array(padLen);
593
+ buf.set(data);
594
+ buf[len] = 128;
595
+ const dv = new DataView(buf.buffer);
596
+ dv.setUint32(padLen - 4, bitLen >>> 0, false);
597
+ dv.setUint32(padLen - 8, Math.floor(bitLen / 4294967296) >>> 0, false);
598
+ const W = new Uint32Array(64);
599
+ for (let i = 0; i < padLen; i += 64) {
600
+ for (let t = 0; t < 16; t++) {
601
+ W[t] = dv.getUint32(i + t * 4, false);
602
+ }
603
+ for (let t = 16; t < 64; t++) {
604
+ const s0 = (W[t - 15] >>> 7 | W[t - 15] << 25) ^ (W[t - 15] >>> 18 | W[t - 15] << 14) ^ W[t - 15] >>> 3;
605
+ const s1 = (W[t - 2] >>> 17 | W[t - 2] << 15) ^ (W[t - 2] >>> 19 | W[t - 2] << 13) ^ W[t - 2] >>> 10;
606
+ W[t] = W[t - 16] + s0 + W[t - 7] + s1 >>> 0;
607
+ }
608
+ let a = H[0], b = H[1], c = H[2], d = H[3], e = H[4], f = H[5], g = H[6], h = H[7];
609
+ for (let t = 0; t < 64; t++) {
610
+ const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
611
+ const ch = e & f ^ ~e & g;
612
+ const temp1 = h + S1 + ch + K[t] + W[t] >>> 0;
613
+ const S0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10);
614
+ const maj = a & b ^ a & c ^ b & c;
615
+ const temp2 = S0 + maj >>> 0;
616
+ h = g;
617
+ g = f;
618
+ f = e;
619
+ e = d + temp1 >>> 0;
620
+ d = c;
621
+ c = b;
622
+ b = a;
623
+ a = temp1 + temp2 >>> 0;
624
+ }
625
+ H[0] = H[0] + a >>> 0;
626
+ H[1] = H[1] + b >>> 0;
627
+ H[2] = H[2] + c >>> 0;
628
+ H[3] = H[3] + d >>> 0;
629
+ H[4] = H[4] + e >>> 0;
630
+ H[5] = H[5] + f >>> 0;
631
+ H[6] = H[6] + g >>> 0;
632
+ H[7] = H[7] + h >>> 0;
633
+ }
634
+ const out = new Uint8Array(32);
635
+ const outDv = new DataView(out.buffer);
636
+ for (let i = 0; i < 8; i++) outDv.setUint32(i * 4, H[i], false);
637
+ return out;
638
+ }
502
639
  async function sha256(data) {
503
640
  const buffer = data.byteLength === data.buffer.byteLength ? data.buffer : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
504
- if (typeof crypto !== "undefined" && typeof crypto.subtle?.digest === "function") {
505
- const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
641
+ const webCrypto = globalThis.crypto;
642
+ if (webCrypto && typeof webCrypto.subtle?.digest === "function") {
643
+ const hashBuffer = await webCrypto.subtle.digest("SHA-256", buffer);
506
644
  const hashArray = Array.from(new Uint8Array(hashBuffer));
507
- const hex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
508
- return `sha256:${hex}`;
645
+ const hex2 = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
646
+ return `sha256:${hex2}`;
509
647
  }
510
648
  if (typeof globalThis.window === "undefined") {
511
649
  const nodeCrypto = await new Function("return import('node:crypto')")();
512
650
  const hash = nodeCrypto.createHash("sha256").update(Buffer.from(buffer)).digest("hex");
513
651
  return `sha256:${hash}`;
514
652
  }
515
- throw new Error("SHA-256 not available: neither Web Crypto nor Node.js crypto module found");
653
+ const digest = sha256PureJS(data);
654
+ const hex = Array.from(digest).map((b) => b.toString(16).padStart(2, "0")).join("");
655
+ return `sha256:${hex}`;
516
656
  }
517
657
  async function readVersion(fs, versionFilePath) {
518
658
  try {
@@ -553,7 +693,7 @@ async function verifyOrRepairVersion(fs, configFilePath, author) {
553
693
  data = new TextEncoder().encode(content);
554
694
  } else if (content instanceof Uint8Array) {
555
695
  data = content;
556
- } else if (Buffer.isBuffer(content)) {
696
+ } else if (typeof Buffer !== "undefined" && Buffer.isBuffer(content)) {
557
697
  data = new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
558
698
  } else {
559
699
  data = new Uint8Array(content);
@@ -704,7 +844,7 @@ var ConfigRepo = class {
704
844
  const fullPath = `${NODES_DIR}/${nodeId}${filePath}`;
705
845
  try {
706
846
  const raw = await this.cachedFS.readFile(fullPath);
707
- return this.serializer.deserialize(toUint8Array(raw), fullPath);
847
+ return this.serializer.deserialize(toUint8Array2(raw), fullPath);
708
848
  } catch {
709
849
  throw new Error(`Node config not found: ${nodeId}${path}`);
710
850
  }
@@ -781,7 +921,7 @@ var ConfigRepo = class {
781
921
  const fullPath = `${NODES_DIR}/${nodeId}${filePath}`;
782
922
  try {
783
923
  const raw = await this.cachedFS.readFile(fullPath);
784
- return this.serializer.deserialize(toUint8Array(raw), fullPath);
924
+ return this.serializer.deserialize(toUint8Array2(raw), fullPath);
785
925
  } catch {
786
926
  throw new Error(`Node config not found: ${nodeId}${path}`);
787
927
  }
@@ -875,7 +1015,7 @@ var ConfigRepo = class {
875
1015
  if (!entry.endsWith(".json")) continue;
876
1016
  try {
877
1017
  const raw = await this.cachedFS.readFile(`${DELETIONS_DIR}/${entry}`);
878
- const data = JSON.parse(new TextDecoder().decode(toUint8Array(raw)));
1018
+ const data = JSON.parse(new TextDecoder().decode(toUint8Array2(raw)));
879
1019
  tombstones.push(data);
880
1020
  } catch {
881
1021
  }
@@ -1118,7 +1258,7 @@ var ConfigRepo = class {
1118
1258
  try {
1119
1259
  const raw = await this.cachedFS.readFile(metaPath);
1120
1260
  const archive = JSON.parse(
1121
- new TextDecoder().decode(toUint8Array(raw))
1261
+ new TextDecoder().decode(toUint8Array2(raw))
1122
1262
  );
1123
1263
  const configPath = archive.conflictPath;
1124
1264
  const bytes = this.serializer.serialize(mergedContent, configPath);
@@ -1154,7 +1294,7 @@ var ConfigRepo = class {
1154
1294
  try {
1155
1295
  const raw = await this.cachedFS.readFile(metaPath);
1156
1296
  const archive = JSON.parse(
1157
- new TextDecoder().decode(toUint8Array(raw))
1297
+ new TextDecoder().decode(toUint8Array2(raw))
1158
1298
  );
1159
1299
  archives.push(archive);
1160
1300
  } catch {
@@ -1169,7 +1309,7 @@ var ConfigRepo = class {
1169
1309
  const conflictDir = `${CONFLICTS_DIR}/${conflictId}`.replace(/\/meta\.json$/, "");
1170
1310
  const filePath = `${conflictDir}/${fileType}`;
1171
1311
  const raw = await this.cachedFS.readFile(filePath);
1172
- return new TextDecoder().decode(toUint8Array(raw));
1312
+ return new TextDecoder().decode(toUint8Array2(raw));
1173
1313
  }
1174
1314
  // -----------------------------------------------------------------------
1175
1315
  // IConfigRepo — Lifecycle
@@ -1300,7 +1440,7 @@ var ConfigRepo = class {
1300
1440
  files.map(async (filePath) => {
1301
1441
  try {
1302
1442
  const raw = await this.cachedFS.readFile(filePath);
1303
- const data = this.serializer.deserialize(toUint8Array(raw), filePath);
1443
+ const data = this.serializer.deserialize(toUint8Array2(raw), filePath);
1304
1444
  return { filePath, data };
1305
1445
  } catch {
1306
1446
  return null;
@@ -1430,7 +1570,7 @@ var ConfigRepo = class {
1430
1570
  async readMetaFile(path) {
1431
1571
  try {
1432
1572
  const raw = await this.cachedFS.readFile(path);
1433
- return JSON.parse(new TextDecoder().decode(toUint8Array(raw)));
1573
+ return JSON.parse(new TextDecoder().decode(toUint8Array2(raw)));
1434
1574
  } catch {
1435
1575
  return null;
1436
1576
  }
@@ -1458,7 +1598,7 @@ var ConfigRepo = class {
1458
1598
  const filePath = `${BACKENDS_DIR}/${entry}`;
1459
1599
  try {
1460
1600
  const raw = await this.cachedFS.readFile(filePath);
1461
- const desc = JSON.parse(new TextDecoder().decode(toUint8Array(raw)));
1601
+ const desc = JSON.parse(new TextDecoder().decode(toUint8Array2(raw)));
1462
1602
  if (desc.id && desc.type) {
1463
1603
  let mtime = 0;
1464
1604
  try {
@@ -1793,7 +1933,7 @@ var ConfigRepo = class {
1793
1933
  const descPath = this.appDataGroupFilePath(id);
1794
1934
  try {
1795
1935
  const raw = await this.cachedFS.readFile(descPath);
1796
- const descriptor = JSON.parse(new TextDecoder().decode(toUint8Array(raw)));
1936
+ const descriptor = JSON.parse(new TextDecoder().decode(toUint8Array2(raw)));
1797
1937
  const group = new AppDataGroupImpl(
1798
1938
  id,
1799
1939
  this.appId,
@@ -1817,7 +1957,7 @@ var ConfigRepo = class {
1817
1957
  if (!entry.endsWith(".json")) continue;
1818
1958
  try {
1819
1959
  const raw = await this.cachedFS.readFile(`${dir}/${entry}`);
1820
- const desc = JSON.parse(new TextDecoder().decode(toUint8Array(raw)));
1960
+ const desc = JSON.parse(new TextDecoder().decode(toUint8Array2(raw)));
1821
1961
  if (desc.id && desc.groupType === "data-sync") {
1822
1962
  descriptors.push(desc);
1823
1963
  }
@@ -1983,11 +2123,13 @@ var AppDataGroupImpl = class {
1983
2123
  this.dataBackends.clear();
1984
2124
  }
1985
2125
  };
1986
- function toUint8Array(raw) {
2126
+ function toUint8Array2(raw) {
1987
2127
  if (raw instanceof ArrayBuffer) return new Uint8Array(raw);
1988
2128
  if (raw instanceof Uint8Array) return raw;
1989
2129
  if (typeof raw === "string") return new TextEncoder().encode(raw);
1990
- if (Buffer.isBuffer(raw)) return new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
2130
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(raw)) {
2131
+ return new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
2132
+ }
1991
2133
  return new Uint8Array(raw);
1992
2134
  }
1993
2135
  async function createConfigRepo(appId, options = {}) {