@noy-db/to-smb 0.1.0-pre.3

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vLannaAi
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,33 @@
1
+ # @noy-db/to-smb
2
+
3
+ [![npm](https://img.shields.io/npm/v/%40noy-db/to-smb.svg)](https://www.npmjs.com/package/@noy-db/to-smb)
4
+
5
+ > SMB/CIFS network file store for noy-db. Works against Windows file servers, NAS devices (Synology/QNAP/Netgear), and corporate shared drives via NTLM or Kerberos auth
6
+
7
+ Part of [**`@noy-db/hub`**](https://www.npmjs.com/package/@noy-db/hub) — the zero-knowledge, offline-first, encrypted document store.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @noy-db/hub @noy-db/to-smb
13
+ ```
14
+
15
+ ## What it is
16
+
17
+ SMB/CIFS network file store for noy-db. Works against Windows file servers, NAS devices (Synology/QNAP/Netgear), and corporate shared drives via NTLM or Kerberos auth — no OS mount required.
18
+
19
+ ## Status
20
+
21
+ **Pre-release** (`0.1.0-pre.1`). API may change before `1.0`.
22
+
23
+ ## Documentation
24
+
25
+ See the [main repository](https://github.com/vLannaAi/noy-db#readme) for setup, examples, and the full subsystem catalog.
26
+
27
+ - Source — [`packages/to-smb`](https://github.com/vLannaAi/noy-db/tree/main/packages/to-smb)
28
+ - Issues — [github.com/vLannaAi/noy-db/issues](https://github.com/vLannaAi/noy-db/issues)
29
+ - Spec — [`SPEC.md`](https://github.com/vLannaAi/noy-db/blob/main/SPEC.md)
30
+
31
+ ## License
32
+
33
+ [MIT](./LICENSE) © vLannaAi
package/dist/index.cjs ADDED
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ smb: () => smb
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ function join(...parts) {
27
+ return parts.filter((p) => p !== "").map((p) => p.replace(/^\/+|\/+$/g, "")).filter((p) => p !== "").join("/");
28
+ }
29
+ function decode(bytes) {
30
+ if (typeof Buffer !== "undefined" && bytes instanceof Buffer) return bytes.toString("utf-8");
31
+ return new TextDecoder().decode(bytes);
32
+ }
33
+ function smb(options) {
34
+ const { smb: client, remotePath = "noydb", name = "smb" } = options;
35
+ const root = remotePath.replace(/^\/+|\/+$/g, "");
36
+ const recordPath = (v, c, id) => join(root, v, c, `${id}.json`);
37
+ const collPath = (v, c) => join(root, v, c);
38
+ const vaultPath = (v) => join(root, v);
39
+ async function ensureDir(path) {
40
+ const parts = path.split("/").filter(Boolean);
41
+ let cur = "";
42
+ for (const seg of parts) {
43
+ cur = cur ? `${cur}/${seg}` : seg;
44
+ try {
45
+ await client.mkdir(cur);
46
+ } catch {
47
+ }
48
+ }
49
+ }
50
+ async function safeReaddir(path) {
51
+ try {
52
+ return await client.readdir(path);
53
+ } catch {
54
+ return [];
55
+ }
56
+ }
57
+ const store = {
58
+ name,
59
+ async get(vault, collection, id) {
60
+ const bytes = await client.readFile(recordPath(vault, collection, id));
61
+ if (bytes === null) return null;
62
+ return JSON.parse(decode(bytes));
63
+ },
64
+ async put(vault, collection, id, envelope) {
65
+ const target = recordPath(vault, collection, id);
66
+ const tmp = `${target}.tmp`;
67
+ await ensureDir(collPath(vault, collection));
68
+ await client.writeFile(tmp, JSON.stringify(envelope));
69
+ await client.rename(tmp, target);
70
+ },
71
+ async delete(vault, collection, id) {
72
+ try {
73
+ await client.unlink(recordPath(vault, collection, id));
74
+ } catch {
75
+ }
76
+ },
77
+ async list(vault, collection) {
78
+ const entries = await safeReaddir(collPath(vault, collection));
79
+ return entries.filter((e) => e.endsWith(".json") && !e.endsWith(".tmp")).map((e) => e.slice(0, -".json".length)).sort();
80
+ },
81
+ async loadAll(vault) {
82
+ const snap = {};
83
+ const collections = await safeReaddir(vaultPath(vault));
84
+ for (const collection of collections) {
85
+ if (collection.startsWith("_")) continue;
86
+ const ids = await store.list(vault, collection);
87
+ const bucket = {};
88
+ for (const id of ids) {
89
+ const env = await store.get(vault, collection, id);
90
+ if (env) bucket[id] = env;
91
+ }
92
+ if (Object.keys(bucket).length > 0) snap[collection] = bucket;
93
+ }
94
+ return snap;
95
+ },
96
+ async saveAll(vault, data) {
97
+ const existing = await safeReaddir(vaultPath(vault));
98
+ for (const collection of existing) {
99
+ const ids = await safeReaddir(collPath(vault, collection));
100
+ for (const fname of ids) {
101
+ await client.unlink(join(root, vault, collection, fname)).catch(() => void 0);
102
+ }
103
+ }
104
+ for (const [collection, recs] of Object.entries(data)) {
105
+ await ensureDir(collPath(vault, collection));
106
+ for (const [id, envelope] of Object.entries(recs)) {
107
+ await store.put(vault, collection, id, envelope);
108
+ }
109
+ }
110
+ },
111
+ async ping() {
112
+ if (client.ping) return client.ping();
113
+ try {
114
+ await client.readdir(root);
115
+ return true;
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+ };
121
+ return store;
122
+ }
123
+ // Annotate the CommonJS export names for ESM import in node:
124
+ 0 && (module.exports = {
125
+ smb
126
+ });
127
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/to-smb** — SMB / CIFS network file store for noy-db.\n *\n * Works against Windows file servers, NAS devices (Synology, QNAP,\n * Netgear), and corporate shared drives. Unlike pointing\n * `@noy-db/to-file` at an OS-mounted path, this package speaks SMB\n * directly — no mount step, explicit credential handling, richer\n * reconnection semantics.\n *\n * ## Driver — bring your own\n *\n * Any SMB2/3 client with a promise-style API works. Ready-made: the\n * `smb2` or `@marsaud/smb2` packages on npm. A duck-typed handle is\n * the injection seam — pass a connected client in, noy-db talks to it.\n *\n * ## Auth\n *\n * - **NTLM** — `username` + `password` (+ optional `domain`). Typical\n * for workgroup NAS.\n * - **Kerberos** — domain-joined environments, ticket from the OS\n * cache. Setting `authKind: 'kerberos'` tells the driver to leave\n * password empty and defer to `kinit`.\n *\n * Credentials never hit disk through this adapter — the caller is\n * responsible for sourcing them from a secrets manager. For\n * long-lived integrations, stash them in the vault's encrypted\n * `_sync_credentials` collection and pass them at open time.\n *\n * ## Atomicity\n *\n * SMB has rename but no atomic CAS — same caveat as WebDAV. Per-record\n * writes go through `{id}.json.tmp` + rename, avoiding the partial-write\n * failure mode. For multi-writer CAS, pair with a primary store that\n * implements it.\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\n\n/**\n * Duck-typed SMB client. Compatible with `smb2` / `@marsaud/smb2`\n * promise wrappers. File paths use Windows-style forward slashes\n * (the SMB client translates to backslashes on the wire).\n */\nexport interface SmbHandle {\n readFile(path: string): Promise<Uint8Array | Buffer | null>\n writeFile(path: string, data: Uint8Array | Buffer | string): Promise<void>\n unlink(path: string): Promise<void>\n mkdir(path: string): Promise<void>\n rename(from: string, to: string): Promise<void>\n readdir(path: string): Promise<string[]>\n /** Optional liveness check — returns true on a reachable share. */\n ping?(): Promise<boolean>\n}\n\nexport interface SmbStoreOptions {\n /** Connected SMB client — consumer supplies this. */\n readonly smb: SmbHandle\n /** Relative subdirectory within the share. Default `'noydb'`. */\n readonly remotePath?: string\n /** Diagnostic name. Default `'smb'`. */\n readonly name?: string\n}\n\nfunction join(...parts: string[]): string {\n return parts\n .filter(p => p !== '')\n .map(p => p.replace(/^\\/+|\\/+$/g, ''))\n .filter(p => p !== '')\n .join('/')\n}\n\nfunction decode(bytes: Uint8Array | Buffer): string {\n if (typeof Buffer !== 'undefined' && bytes instanceof Buffer) return bytes.toString('utf-8')\n return new TextDecoder().decode(bytes)\n}\n\nexport function smb(options: SmbStoreOptions): NoydbStore {\n const { smb: client, remotePath = 'noydb', name = 'smb' } = options\n const root = remotePath.replace(/^\\/+|\\/+$/g, '')\n\n const recordPath = (v: string, c: string, id: string): string => join(root, v, c, `${id}.json`)\n const collPath = (v: string, c: string): string => join(root, v, c)\n const vaultPath = (v: string): string => join(root, v)\n\n async function ensureDir(path: string): Promise<void> {\n // SMB mkdir is not always recursive — walk parents.\n const parts = path.split('/').filter(Boolean)\n let cur = ''\n for (const seg of parts) {\n cur = cur ? `${cur}/${seg}` : seg\n try {\n await client.mkdir(cur)\n } catch {\n // Exists or race — continue.\n }\n }\n }\n\n async function safeReaddir(path: string): Promise<string[]> {\n try {\n return await client.readdir(path)\n } catch {\n return []\n }\n }\n\n const store: NoydbStore = {\n name,\n\n async get(vault, collection, id) {\n const bytes = await client.readFile(recordPath(vault, collection, id))\n if (bytes === null) return null\n return JSON.parse(decode(bytes)) as EncryptedEnvelope\n },\n\n async put(vault, collection, id, envelope) {\n // casAtomic: false — expectedVersion ignored.\n const target = recordPath(vault, collection, id)\n const tmp = `${target}.tmp`\n await ensureDir(collPath(vault, collection))\n await client.writeFile(tmp, JSON.stringify(envelope))\n await client.rename(tmp, target)\n },\n\n async delete(vault, collection, id) {\n try {\n await client.unlink(recordPath(vault, collection, id))\n } catch {\n /* idempotent */\n }\n },\n\n async list(vault, collection) {\n const entries = await safeReaddir(collPath(vault, collection))\n return entries\n .filter(e => e.endsWith('.json') && !e.endsWith('.tmp'))\n .map(e => e.slice(0, -'.json'.length))\n .sort()\n },\n\n async loadAll(vault) {\n const snap: VaultSnapshot = {}\n const collections = await safeReaddir(vaultPath(vault))\n for (const collection of collections) {\n if (collection.startsWith('_')) continue\n const ids = await store.list(vault, collection)\n const bucket: Record<string, EncryptedEnvelope> = {}\n for (const id of ids) {\n const env = await store.get(vault, collection, id)\n if (env) bucket[id] = env\n }\n if (Object.keys(bucket).length > 0) snap[collection] = bucket\n }\n return snap\n },\n\n async saveAll(vault, data) {\n const existing = await safeReaddir(vaultPath(vault))\n for (const collection of existing) {\n const ids = await safeReaddir(collPath(vault, collection))\n for (const fname of ids) {\n await client.unlink(join(root, vault, collection, fname)).catch(() => undefined)\n }\n }\n for (const [collection, recs] of Object.entries(data)) {\n await ensureDir(collPath(vault, collection))\n for (const [id, envelope] of Object.entries(recs)) {\n await store.put(vault, collection, id, envelope)\n }\n }\n },\n\n async ping() {\n if (client.ping) return client.ping()\n try {\n await client.readdir(root)\n return true\n } catch {\n return false\n }\n },\n }\n\n return store\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAiEA,SAAS,QAAQ,OAAyB;AACxC,SAAO,MACJ,OAAO,OAAK,MAAM,EAAE,EACpB,IAAI,OAAK,EAAE,QAAQ,cAAc,EAAE,CAAC,EACpC,OAAO,OAAK,MAAM,EAAE,EACpB,KAAK,GAAG;AACb;AAEA,SAAS,OAAO,OAAoC;AAClD,MAAI,OAAO,WAAW,eAAe,iBAAiB,OAAQ,QAAO,MAAM,SAAS,OAAO;AAC3F,SAAO,IAAI,YAAY,EAAE,OAAO,KAAK;AACvC;AAEO,SAAS,IAAI,SAAsC;AACxD,QAAM,EAAE,KAAK,QAAQ,aAAa,SAAS,OAAO,MAAM,IAAI;AAC5D,QAAM,OAAO,WAAW,QAAQ,cAAc,EAAE;AAEhD,QAAM,aAAa,CAAC,GAAW,GAAW,OAAuB,KAAK,MAAM,GAAG,GAAG,GAAG,EAAE,OAAO;AAC9F,QAAM,WAAW,CAAC,GAAW,MAAsB,KAAK,MAAM,GAAG,CAAC;AAClE,QAAM,YAAY,CAAC,MAAsB,KAAK,MAAM,CAAC;AAErD,iBAAe,UAAU,MAA6B;AAEpD,UAAM,QAAQ,KAAK,MAAM,GAAG,EAAE,OAAO,OAAO;AAC5C,QAAI,MAAM;AACV,eAAW,OAAO,OAAO;AACvB,YAAM,MAAM,GAAG,GAAG,IAAI,GAAG,KAAK;AAC9B,UAAI;AACF,cAAM,OAAO,MAAM,GAAG;AAAA,MACxB,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,iBAAe,YAAY,MAAiC;AAC1D,QAAI;AACF,aAAO,MAAM,OAAO,QAAQ,IAAI;AAAA,IAClC,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAEA,QAAM,QAAoB;AAAA,IACxB;AAAA,IAEA,MAAM,IAAI,OAAO,YAAY,IAAI;AAC/B,YAAM,QAAQ,MAAM,OAAO,SAAS,WAAW,OAAO,YAAY,EAAE,CAAC;AACrE,UAAI,UAAU,KAAM,QAAO;AAC3B,aAAO,KAAK,MAAM,OAAO,KAAK,CAAC;AAAA,IACjC;AAAA,IAEA,MAAM,IAAI,OAAO,YAAY,IAAI,UAAU;AAEzC,YAAM,SAAS,WAAW,OAAO,YAAY,EAAE;AAC/C,YAAM,MAAM,GAAG,MAAM;AACrB,YAAM,UAAU,SAAS,OAAO,UAAU,CAAC;AAC3C,YAAM,OAAO,UAAU,KAAK,KAAK,UAAU,QAAQ,CAAC;AACpD,YAAM,OAAO,OAAO,KAAK,MAAM;AAAA,IACjC;AAAA,IAEA,MAAM,OAAO,OAAO,YAAY,IAAI;AAClC,UAAI;AACF,cAAM,OAAO,OAAO,WAAW,OAAO,YAAY,EAAE,CAAC;AAAA,MACvD,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IAEA,MAAM,KAAK,OAAO,YAAY;AAC5B,YAAM,UAAU,MAAM,YAAY,SAAS,OAAO,UAAU,CAAC;AAC7D,aAAO,QACJ,OAAO,OAAK,EAAE,SAAS,OAAO,KAAK,CAAC,EAAE,SAAS,MAAM,CAAC,EACtD,IAAI,OAAK,EAAE,MAAM,GAAG,CAAC,QAAQ,MAAM,CAAC,EACpC,KAAK;AAAA,IACV;AAAA,IAEA,MAAM,QAAQ,OAAO;AACnB,YAAM,OAAsB,CAAC;AAC7B,YAAM,cAAc,MAAM,YAAY,UAAU,KAAK,CAAC;AACtD,iBAAW,cAAc,aAAa;AACpC,YAAI,WAAW,WAAW,GAAG,EAAG;AAChC,cAAM,MAAM,MAAM,MAAM,KAAK,OAAO,UAAU;AAC9C,cAAM,SAA4C,CAAC;AACnD,mBAAW,MAAM,KAAK;AACpB,gBAAM,MAAM,MAAM,MAAM,IAAI,OAAO,YAAY,EAAE;AACjD,cAAI,IAAK,QAAO,EAAE,IAAI;AAAA,QACxB;AACA,YAAI,OAAO,KAAK,MAAM,EAAE,SAAS,EAAG,MAAK,UAAU,IAAI;AAAA,MACzD;AACA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,QAAQ,OAAO,MAAM;AACzB,YAAM,WAAW,MAAM,YAAY,UAAU,KAAK,CAAC;AACnD,iBAAW,cAAc,UAAU;AACjC,cAAM,MAAM,MAAM,YAAY,SAAS,OAAO,UAAU,CAAC;AACzD,mBAAW,SAAS,KAAK;AACvB,gBAAM,OAAO,OAAO,KAAK,MAAM,OAAO,YAAY,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AAAA,QACjF;AAAA,MACF;AACA,iBAAW,CAAC,YAAY,IAAI,KAAK,OAAO,QAAQ,IAAI,GAAG;AACrD,cAAM,UAAU,SAAS,OAAO,UAAU,CAAC;AAC3C,mBAAW,CAAC,IAAI,QAAQ,KAAK,OAAO,QAAQ,IAAI,GAAG;AACjD,gBAAM,MAAM,IAAI,OAAO,YAAY,IAAI,QAAQ;AAAA,QACjD;AAAA,MACF;AAAA,IACF;AAAA,IAEA,MAAM,OAAO;AACX,UAAI,OAAO,KAAM,QAAO,OAAO,KAAK;AACpC,UAAI;AACF,cAAM,OAAO,QAAQ,IAAI;AACzB,eAAO;AAAA,MACT,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
@@ -0,0 +1,66 @@
1
+ import { NoydbStore } from '@noy-db/hub';
2
+
3
+ /**
4
+ * **@noy-db/to-smb** — SMB / CIFS network file store for noy-db.
5
+ *
6
+ * Works against Windows file servers, NAS devices (Synology, QNAP,
7
+ * Netgear), and corporate shared drives. Unlike pointing
8
+ * `@noy-db/to-file` at an OS-mounted path, this package speaks SMB
9
+ * directly — no mount step, explicit credential handling, richer
10
+ * reconnection semantics.
11
+ *
12
+ * ## Driver — bring your own
13
+ *
14
+ * Any SMB2/3 client with a promise-style API works. Ready-made: the
15
+ * `smb2` or `@marsaud/smb2` packages on npm. A duck-typed handle is
16
+ * the injection seam — pass a connected client in, noy-db talks to it.
17
+ *
18
+ * ## Auth
19
+ *
20
+ * - **NTLM** — `username` + `password` (+ optional `domain`). Typical
21
+ * for workgroup NAS.
22
+ * - **Kerberos** — domain-joined environments, ticket from the OS
23
+ * cache. Setting `authKind: 'kerberos'` tells the driver to leave
24
+ * password empty and defer to `kinit`.
25
+ *
26
+ * Credentials never hit disk through this adapter — the caller is
27
+ * responsible for sourcing them from a secrets manager. For
28
+ * long-lived integrations, stash them in the vault's encrypted
29
+ * `_sync_credentials` collection and pass them at open time.
30
+ *
31
+ * ## Atomicity
32
+ *
33
+ * SMB has rename but no atomic CAS — same caveat as WebDAV. Per-record
34
+ * writes go through `{id}.json.tmp` + rename, avoiding the partial-write
35
+ * failure mode. For multi-writer CAS, pair with a primary store that
36
+ * implements it.
37
+ *
38
+ * @packageDocumentation
39
+ */
40
+
41
+ /**
42
+ * Duck-typed SMB client. Compatible with `smb2` / `@marsaud/smb2`
43
+ * promise wrappers. File paths use Windows-style forward slashes
44
+ * (the SMB client translates to backslashes on the wire).
45
+ */
46
+ interface SmbHandle {
47
+ readFile(path: string): Promise<Uint8Array | Buffer | null>;
48
+ writeFile(path: string, data: Uint8Array | Buffer | string): Promise<void>;
49
+ unlink(path: string): Promise<void>;
50
+ mkdir(path: string): Promise<void>;
51
+ rename(from: string, to: string): Promise<void>;
52
+ readdir(path: string): Promise<string[]>;
53
+ /** Optional liveness check — returns true on a reachable share. */
54
+ ping?(): Promise<boolean>;
55
+ }
56
+ interface SmbStoreOptions {
57
+ /** Connected SMB client — consumer supplies this. */
58
+ readonly smb: SmbHandle;
59
+ /** Relative subdirectory within the share. Default `'noydb'`. */
60
+ readonly remotePath?: string;
61
+ /** Diagnostic name. Default `'smb'`. */
62
+ readonly name?: string;
63
+ }
64
+ declare function smb(options: SmbStoreOptions): NoydbStore;
65
+
66
+ export { type SmbHandle, type SmbStoreOptions, smb };
@@ -0,0 +1,66 @@
1
+ import { NoydbStore } from '@noy-db/hub';
2
+
3
+ /**
4
+ * **@noy-db/to-smb** — SMB / CIFS network file store for noy-db.
5
+ *
6
+ * Works against Windows file servers, NAS devices (Synology, QNAP,
7
+ * Netgear), and corporate shared drives. Unlike pointing
8
+ * `@noy-db/to-file` at an OS-mounted path, this package speaks SMB
9
+ * directly — no mount step, explicit credential handling, richer
10
+ * reconnection semantics.
11
+ *
12
+ * ## Driver — bring your own
13
+ *
14
+ * Any SMB2/3 client with a promise-style API works. Ready-made: the
15
+ * `smb2` or `@marsaud/smb2` packages on npm. A duck-typed handle is
16
+ * the injection seam — pass a connected client in, noy-db talks to it.
17
+ *
18
+ * ## Auth
19
+ *
20
+ * - **NTLM** — `username` + `password` (+ optional `domain`). Typical
21
+ * for workgroup NAS.
22
+ * - **Kerberos** — domain-joined environments, ticket from the OS
23
+ * cache. Setting `authKind: 'kerberos'` tells the driver to leave
24
+ * password empty and defer to `kinit`.
25
+ *
26
+ * Credentials never hit disk through this adapter — the caller is
27
+ * responsible for sourcing them from a secrets manager. For
28
+ * long-lived integrations, stash them in the vault's encrypted
29
+ * `_sync_credentials` collection and pass them at open time.
30
+ *
31
+ * ## Atomicity
32
+ *
33
+ * SMB has rename but no atomic CAS — same caveat as WebDAV. Per-record
34
+ * writes go through `{id}.json.tmp` + rename, avoiding the partial-write
35
+ * failure mode. For multi-writer CAS, pair with a primary store that
36
+ * implements it.
37
+ *
38
+ * @packageDocumentation
39
+ */
40
+
41
+ /**
42
+ * Duck-typed SMB client. Compatible with `smb2` / `@marsaud/smb2`
43
+ * promise wrappers. File paths use Windows-style forward slashes
44
+ * (the SMB client translates to backslashes on the wire).
45
+ */
46
+ interface SmbHandle {
47
+ readFile(path: string): Promise<Uint8Array | Buffer | null>;
48
+ writeFile(path: string, data: Uint8Array | Buffer | string): Promise<void>;
49
+ unlink(path: string): Promise<void>;
50
+ mkdir(path: string): Promise<void>;
51
+ rename(from: string, to: string): Promise<void>;
52
+ readdir(path: string): Promise<string[]>;
53
+ /** Optional liveness check — returns true on a reachable share. */
54
+ ping?(): Promise<boolean>;
55
+ }
56
+ interface SmbStoreOptions {
57
+ /** Connected SMB client — consumer supplies this. */
58
+ readonly smb: SmbHandle;
59
+ /** Relative subdirectory within the share. Default `'noydb'`. */
60
+ readonly remotePath?: string;
61
+ /** Diagnostic name. Default `'smb'`. */
62
+ readonly name?: string;
63
+ }
64
+ declare function smb(options: SmbStoreOptions): NoydbStore;
65
+
66
+ export { type SmbHandle, type SmbStoreOptions, smb };
package/dist/index.js ADDED
@@ -0,0 +1,102 @@
1
+ // src/index.ts
2
+ function join(...parts) {
3
+ return parts.filter((p) => p !== "").map((p) => p.replace(/^\/+|\/+$/g, "")).filter((p) => p !== "").join("/");
4
+ }
5
+ function decode(bytes) {
6
+ if (typeof Buffer !== "undefined" && bytes instanceof Buffer) return bytes.toString("utf-8");
7
+ return new TextDecoder().decode(bytes);
8
+ }
9
+ function smb(options) {
10
+ const { smb: client, remotePath = "noydb", name = "smb" } = options;
11
+ const root = remotePath.replace(/^\/+|\/+$/g, "");
12
+ const recordPath = (v, c, id) => join(root, v, c, `${id}.json`);
13
+ const collPath = (v, c) => join(root, v, c);
14
+ const vaultPath = (v) => join(root, v);
15
+ async function ensureDir(path) {
16
+ const parts = path.split("/").filter(Boolean);
17
+ let cur = "";
18
+ for (const seg of parts) {
19
+ cur = cur ? `${cur}/${seg}` : seg;
20
+ try {
21
+ await client.mkdir(cur);
22
+ } catch {
23
+ }
24
+ }
25
+ }
26
+ async function safeReaddir(path) {
27
+ try {
28
+ return await client.readdir(path);
29
+ } catch {
30
+ return [];
31
+ }
32
+ }
33
+ const store = {
34
+ name,
35
+ async get(vault, collection, id) {
36
+ const bytes = await client.readFile(recordPath(vault, collection, id));
37
+ if (bytes === null) return null;
38
+ return JSON.parse(decode(bytes));
39
+ },
40
+ async put(vault, collection, id, envelope) {
41
+ const target = recordPath(vault, collection, id);
42
+ const tmp = `${target}.tmp`;
43
+ await ensureDir(collPath(vault, collection));
44
+ await client.writeFile(tmp, JSON.stringify(envelope));
45
+ await client.rename(tmp, target);
46
+ },
47
+ async delete(vault, collection, id) {
48
+ try {
49
+ await client.unlink(recordPath(vault, collection, id));
50
+ } catch {
51
+ }
52
+ },
53
+ async list(vault, collection) {
54
+ const entries = await safeReaddir(collPath(vault, collection));
55
+ return entries.filter((e) => e.endsWith(".json") && !e.endsWith(".tmp")).map((e) => e.slice(0, -".json".length)).sort();
56
+ },
57
+ async loadAll(vault) {
58
+ const snap = {};
59
+ const collections = await safeReaddir(vaultPath(vault));
60
+ for (const collection of collections) {
61
+ if (collection.startsWith("_")) continue;
62
+ const ids = await store.list(vault, collection);
63
+ const bucket = {};
64
+ for (const id of ids) {
65
+ const env = await store.get(vault, collection, id);
66
+ if (env) bucket[id] = env;
67
+ }
68
+ if (Object.keys(bucket).length > 0) snap[collection] = bucket;
69
+ }
70
+ return snap;
71
+ },
72
+ async saveAll(vault, data) {
73
+ const existing = await safeReaddir(vaultPath(vault));
74
+ for (const collection of existing) {
75
+ const ids = await safeReaddir(collPath(vault, collection));
76
+ for (const fname of ids) {
77
+ await client.unlink(join(root, vault, collection, fname)).catch(() => void 0);
78
+ }
79
+ }
80
+ for (const [collection, recs] of Object.entries(data)) {
81
+ await ensureDir(collPath(vault, collection));
82
+ for (const [id, envelope] of Object.entries(recs)) {
83
+ await store.put(vault, collection, id, envelope);
84
+ }
85
+ }
86
+ },
87
+ async ping() {
88
+ if (client.ping) return client.ping();
89
+ try {
90
+ await client.readdir(root);
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+ };
97
+ return store;
98
+ }
99
+ export {
100
+ smb
101
+ };
102
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/to-smb** — SMB / CIFS network file store for noy-db.\n *\n * Works against Windows file servers, NAS devices (Synology, QNAP,\n * Netgear), and corporate shared drives. Unlike pointing\n * `@noy-db/to-file` at an OS-mounted path, this package speaks SMB\n * directly — no mount step, explicit credential handling, richer\n * reconnection semantics.\n *\n * ## Driver — bring your own\n *\n * Any SMB2/3 client with a promise-style API works. Ready-made: the\n * `smb2` or `@marsaud/smb2` packages on npm. A duck-typed handle is\n * the injection seam — pass a connected client in, noy-db talks to it.\n *\n * ## Auth\n *\n * - **NTLM** — `username` + `password` (+ optional `domain`). Typical\n * for workgroup NAS.\n * - **Kerberos** — domain-joined environments, ticket from the OS\n * cache. Setting `authKind: 'kerberos'` tells the driver to leave\n * password empty and defer to `kinit`.\n *\n * Credentials never hit disk through this adapter — the caller is\n * responsible for sourcing them from a secrets manager. For\n * long-lived integrations, stash them in the vault's encrypted\n * `_sync_credentials` collection and pass them at open time.\n *\n * ## Atomicity\n *\n * SMB has rename but no atomic CAS — same caveat as WebDAV. Per-record\n * writes go through `{id}.json.tmp` + rename, avoiding the partial-write\n * failure mode. For multi-writer CAS, pair with a primary store that\n * implements it.\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\n\n/**\n * Duck-typed SMB client. Compatible with `smb2` / `@marsaud/smb2`\n * promise wrappers. File paths use Windows-style forward slashes\n * (the SMB client translates to backslashes on the wire).\n */\nexport interface SmbHandle {\n readFile(path: string): Promise<Uint8Array | Buffer | null>\n writeFile(path: string, data: Uint8Array | Buffer | string): Promise<void>\n unlink(path: string): Promise<void>\n mkdir(path: string): Promise<void>\n rename(from: string, to: string): Promise<void>\n readdir(path: string): Promise<string[]>\n /** Optional liveness check — returns true on a reachable share. */\n ping?(): Promise<boolean>\n}\n\nexport interface SmbStoreOptions {\n /** Connected SMB client — consumer supplies this. */\n readonly smb: SmbHandle\n /** Relative subdirectory within the share. Default `'noydb'`. */\n readonly remotePath?: string\n /** Diagnostic name. Default `'smb'`. */\n readonly name?: string\n}\n\nfunction join(...parts: string[]): string {\n return parts\n .filter(p => p !== '')\n .map(p => p.replace(/^\\/+|\\/+$/g, ''))\n .filter(p => p !== '')\n .join('/')\n}\n\nfunction decode(bytes: Uint8Array | Buffer): string {\n if (typeof Buffer !== 'undefined' && bytes instanceof Buffer) return bytes.toString('utf-8')\n return new TextDecoder().decode(bytes)\n}\n\nexport function smb(options: SmbStoreOptions): NoydbStore {\n const { smb: client, remotePath = 'noydb', name = 'smb' } = options\n const root = remotePath.replace(/^\\/+|\\/+$/g, '')\n\n const recordPath = (v: string, c: string, id: string): string => join(root, v, c, `${id}.json`)\n const collPath = (v: string, c: string): string => join(root, v, c)\n const vaultPath = (v: string): string => join(root, v)\n\n async function ensureDir(path: string): Promise<void> {\n // SMB mkdir is not always recursive — walk parents.\n const parts = path.split('/').filter(Boolean)\n let cur = ''\n for (const seg of parts) {\n cur = cur ? `${cur}/${seg}` : seg\n try {\n await client.mkdir(cur)\n } catch {\n // Exists or race — continue.\n }\n }\n }\n\n async function safeReaddir(path: string): Promise<string[]> {\n try {\n return await client.readdir(path)\n } catch {\n return []\n }\n }\n\n const store: NoydbStore = {\n name,\n\n async get(vault, collection, id) {\n const bytes = await client.readFile(recordPath(vault, collection, id))\n if (bytes === null) return null\n return JSON.parse(decode(bytes)) as EncryptedEnvelope\n },\n\n async put(vault, collection, id, envelope) {\n // casAtomic: false — expectedVersion ignored.\n const target = recordPath(vault, collection, id)\n const tmp = `${target}.tmp`\n await ensureDir(collPath(vault, collection))\n await client.writeFile(tmp, JSON.stringify(envelope))\n await client.rename(tmp, target)\n },\n\n async delete(vault, collection, id) {\n try {\n await client.unlink(recordPath(vault, collection, id))\n } catch {\n /* idempotent */\n }\n },\n\n async list(vault, collection) {\n const entries = await safeReaddir(collPath(vault, collection))\n return entries\n .filter(e => e.endsWith('.json') && !e.endsWith('.tmp'))\n .map(e => e.slice(0, -'.json'.length))\n .sort()\n },\n\n async loadAll(vault) {\n const snap: VaultSnapshot = {}\n const collections = await safeReaddir(vaultPath(vault))\n for (const collection of collections) {\n if (collection.startsWith('_')) continue\n const ids = await store.list(vault, collection)\n const bucket: Record<string, EncryptedEnvelope> = {}\n for (const id of ids) {\n const env = await store.get(vault, collection, id)\n if (env) bucket[id] = env\n }\n if (Object.keys(bucket).length > 0) snap[collection] = bucket\n }\n return snap\n },\n\n async saveAll(vault, data) {\n const existing = await safeReaddir(vaultPath(vault))\n for (const collection of existing) {\n const ids = await safeReaddir(collPath(vault, collection))\n for (const fname of ids) {\n await client.unlink(join(root, vault, collection, fname)).catch(() => undefined)\n }\n }\n for (const [collection, recs] of Object.entries(data)) {\n await ensureDir(collPath(vault, collection))\n for (const [id, envelope] of Object.entries(recs)) {\n await store.put(vault, collection, id, envelope)\n }\n }\n },\n\n async ping() {\n if (client.ping) return client.ping()\n try {\n await client.readdir(root)\n return true\n } catch {\n return false\n }\n },\n }\n\n return store\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@noy-db/to-smb",
3
+ "version": "0.1.0-pre.3",
4
+ "description": "SMB/CIFS network file store for noy-db. Works against Windows file servers, NAS devices (Synology/QNAP/Netgear), and corporate shared drives via NTLM or Kerberos auth — no OS mount required.",
5
+ "license": "MIT",
6
+ "author": "vLannaAi <vicio@lanna.ai>",
7
+ "homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/to-smb#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/vLannaAi/noy-db.git",
11
+ "directory": "packages/to-smb"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/vLannaAi/noy-db/issues"
15
+ },
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "exports": {
19
+ ".": {
20
+ "import": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/index.d.cts",
26
+ "default": "./dist/index.cjs"
27
+ }
28
+ }
29
+ },
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ },
41
+ "peerDependencies": {
42
+ "@noy-db/hub": "0.1.0-pre.3"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^22.0.0",
46
+ "@noy-db/hub": "0.1.0-pre.3"
47
+ },
48
+ "keywords": [
49
+ "noy-db",
50
+ "to-smb",
51
+ "smb",
52
+ "cifs",
53
+ "nas",
54
+ "storage"
55
+ ],
56
+ "publishConfig": {
57
+ "access": "public",
58
+ "tag": "latest"
59
+ },
60
+ "scripts": {
61
+ "build": "tsup",
62
+ "test": "vitest run",
63
+ "lint": "eslint src/",
64
+ "typecheck": "tsc --noEmit"
65
+ }
66
+ }