@kb-labs/adapters 0.5.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.
Files changed (276) hide show
  1. package/.cursorrules +32 -0
  2. package/.github/workflows/ci.yml +13 -0
  3. package/.github/workflows/deploy.yml +28 -0
  4. package/.github/workflows/docker-build.yml +25 -0
  5. package/.github/workflows/drift-check.yml +10 -0
  6. package/.github/workflows/profiles-validate.yml +16 -0
  7. package/.github/workflows/release.yml +8 -0
  8. package/.kb/devkit/agents/devkit-maintainer/context.globs +15 -0
  9. package/.kb/devkit/agents/devkit-maintainer/permissions.yml +17 -0
  10. package/.kb/devkit/agents/devkit-maintainer/prompt.md +28 -0
  11. package/.kb/devkit/agents/devkit-maintainer/runbook.md +31 -0
  12. package/.kb/devkit/agents/docs-crafter/prompt.md +24 -0
  13. package/.kb/devkit/agents/docs-crafter/runbook.md +18 -0
  14. package/.kb/devkit/agents/release-manager/context.globs +7 -0
  15. package/.kb/devkit/agents/release-manager/prompt.md +27 -0
  16. package/.kb/devkit/agents/release-manager/runbook.md +17 -0
  17. package/.kb/devkit/agents/test-generator/context.globs +7 -0
  18. package/.kb/devkit/agents/test-generator/prompt.md +27 -0
  19. package/.kb/devkit/agents/test-generator/runbook.md +18 -0
  20. package/CONTRIBUTING.md +90 -0
  21. package/IMPLEMENTATION_COMPLETE.md +416 -0
  22. package/LICENSE +186 -0
  23. package/README-TEMPLATE.md +179 -0
  24. package/README.md +306 -0
  25. package/docs/DOCUMENTATION.md +74 -0
  26. package/docs/adr/0000-template.md +49 -0
  27. package/docs/adr/0001-architecture-and-repository-layout.md +33 -0
  28. package/docs/adr/0002-plugins-and-extensibility.md +46 -0
  29. package/docs/adr/0003-package-and-module-boundaries.md +37 -0
  30. package/docs/adr/0004-versioning-and-release-policy.md +38 -0
  31. package/docs/adr/0005-use-devkit-for-shared-tooling.md +48 -0
  32. package/docs/adr/0006-adopt-devkit-sync.md +47 -0
  33. package/docs/adr/0007-drift-kit-check.md +72 -0
  34. package/docs/adr/0008-devkit-sync-wrapper-strategy.md +67 -0
  35. package/docs/naming-convention.md +272 -0
  36. package/eslint.config.js +27 -0
  37. package/kb-labs.config.json +5 -0
  38. package/package.json +84 -0
  39. package/package.json.bin +25 -0
  40. package/package.json.lib +30 -0
  41. package/packages/adapters-analytics-duckdb/package.json +54 -0
  42. package/packages/adapters-analytics-duckdb/scripts/migrate-from-jsonl.mjs +253 -0
  43. package/packages/adapters-analytics-duckdb/src/index.ts +380 -0
  44. package/packages/adapters-analytics-duckdb/src/manifest.ts +36 -0
  45. package/packages/adapters-analytics-duckdb/src/schema.ts +161 -0
  46. package/packages/adapters-analytics-duckdb/tsconfig.build.json +15 -0
  47. package/packages/adapters-analytics-duckdb/tsconfig.json +9 -0
  48. package/packages/adapters-analytics-duckdb/tsup.config.ts +9 -0
  49. package/packages/adapters-analytics-file/README.md +32 -0
  50. package/packages/adapters-analytics-file/eslint.config.js +27 -0
  51. package/packages/adapters-analytics-file/package.json +50 -0
  52. package/packages/adapters-analytics-file/src/__tests__/daily-stats.spec.ts +287 -0
  53. package/packages/adapters-analytics-file/src/__tests__/scoped-analytics.test.ts +233 -0
  54. package/packages/adapters-analytics-file/src/index.test.ts +214 -0
  55. package/packages/adapters-analytics-file/src/index.ts +830 -0
  56. package/packages/adapters-analytics-file/src/manifest.ts +45 -0
  57. package/packages/adapters-analytics-file/tsconfig.build.json +15 -0
  58. package/packages/adapters-analytics-file/tsconfig.json +9 -0
  59. package/packages/adapters-analytics-file/tsup.config.ts +9 -0
  60. package/packages/adapters-analytics-sqlite/package.json +55 -0
  61. package/packages/adapters-analytics-sqlite/scripts/migrate-from-jsonl.mjs +194 -0
  62. package/packages/adapters-analytics-sqlite/src/index.ts +460 -0
  63. package/packages/adapters-analytics-sqlite/src/manifest.ts +41 -0
  64. package/packages/adapters-analytics-sqlite/tsconfig.build.json +15 -0
  65. package/packages/adapters-analytics-sqlite/tsconfig.json +9 -0
  66. package/packages/adapters-analytics-sqlite/tsup.config.ts +9 -0
  67. package/packages/adapters-environment-docker/README.md +28 -0
  68. package/packages/adapters-environment-docker/eslint.config.js +5 -0
  69. package/packages/adapters-environment-docker/package.json +49 -0
  70. package/packages/adapters-environment-docker/src/index.test.ts +138 -0
  71. package/packages/adapters-environment-docker/src/index.ts +439 -0
  72. package/packages/adapters-environment-docker/src/manifest.ts +65 -0
  73. package/packages/adapters-environment-docker/tsconfig.build.json +15 -0
  74. package/packages/adapters-environment-docker/tsconfig.json +16 -0
  75. package/packages/adapters-environment-docker/tsup.config.ts +9 -0
  76. package/packages/adapters-eventbus-cache/README.md +242 -0
  77. package/packages/adapters-eventbus-cache/eslint.config.js +27 -0
  78. package/packages/adapters-eventbus-cache/package.json +46 -0
  79. package/packages/adapters-eventbus-cache/src/index.test.ts +235 -0
  80. package/packages/adapters-eventbus-cache/src/index.ts +215 -0
  81. package/packages/adapters-eventbus-cache/src/manifest.ts +50 -0
  82. package/packages/adapters-eventbus-cache/src/types.ts +58 -0
  83. package/packages/adapters-eventbus-cache/tsconfig.build.json +15 -0
  84. package/packages/adapters-eventbus-cache/tsconfig.json +9 -0
  85. package/packages/adapters-eventbus-cache/tsup.config.ts +9 -0
  86. package/packages/adapters-fs/README.md +171 -0
  87. package/packages/adapters-fs/allowed.txt +1 -0
  88. package/packages/adapters-fs/conflict.txt +1 -0
  89. package/packages/adapters-fs/dest.txt +1 -0
  90. package/packages/adapters-fs/eslint.config.js +27 -0
  91. package/packages/adapters-fs/exists.txt +1 -0
  92. package/packages/adapters-fs/not-allowed.txt +1 -0
  93. package/packages/adapters-fs/other.txt +1 -0
  94. package/packages/adapters-fs/package.json +55 -0
  95. package/packages/adapters-fs/public/file1.txt +1 -0
  96. package/packages/adapters-fs/public/file2.txt +1 -0
  97. package/packages/adapters-fs/secret.txt +1 -0
  98. package/packages/adapters-fs/secrets/key.txt +1 -0
  99. package/packages/adapters-fs/src/index.test.ts +243 -0
  100. package/packages/adapters-fs/src/index.ts +258 -0
  101. package/packages/adapters-fs/src/manifest.ts +35 -0
  102. package/packages/adapters-fs/src/secure-storage.test.ts +380 -0
  103. package/packages/adapters-fs/src/secure-storage.ts +268 -0
  104. package/packages/adapters-fs/test.json +1 -0
  105. package/packages/adapters-fs/test.txt +1 -0
  106. package/packages/adapters-fs/test.xyz +1 -0
  107. package/packages/adapters-fs/test1.txt +1 -0
  108. package/packages/adapters-fs/test2.txt +1 -0
  109. package/packages/adapters-fs/tsconfig.build.json +15 -0
  110. package/packages/adapters-fs/tsconfig.json +9 -0
  111. package/packages/adapters-fs/tsup.config.ts +8 -0
  112. package/packages/adapters-fs/vitest.config.ts +19 -0
  113. package/packages/adapters-log-ringbuffer/README.md +228 -0
  114. package/packages/adapters-log-ringbuffer/eslint.config.js +27 -0
  115. package/packages/adapters-log-ringbuffer/package.json +47 -0
  116. package/packages/adapters-log-ringbuffer/src/__tests__/ring-buffer.test.ts +450 -0
  117. package/packages/adapters-log-ringbuffer/src/index.ts +212 -0
  118. package/packages/adapters-log-ringbuffer/src/manifest.ts +30 -0
  119. package/packages/adapters-log-ringbuffer/tsconfig.build.json +15 -0
  120. package/packages/adapters-log-ringbuffer/tsconfig.json +9 -0
  121. package/packages/adapters-log-ringbuffer/tsup.config.ts +9 -0
  122. package/packages/adapters-log-ringbuffer/vitest.config.ts +14 -0
  123. package/packages/adapters-log-sqlite/README.md +396 -0
  124. package/packages/adapters-log-sqlite/eslint.config.js +27 -0
  125. package/packages/adapters-log-sqlite/package.json +49 -0
  126. package/packages/adapters-log-sqlite/src/__tests__/log-persistence.test.ts +718 -0
  127. package/packages/adapters-log-sqlite/src/index.ts +1068 -0
  128. package/packages/adapters-log-sqlite/src/manifest.ts +36 -0
  129. package/packages/adapters-log-sqlite/src/schema.sql +46 -0
  130. package/packages/adapters-log-sqlite/tsconfig.build.json +15 -0
  131. package/packages/adapters-log-sqlite/tsconfig.json +9 -0
  132. package/packages/adapters-log-sqlite/tsup.config.ts +9 -0
  133. package/packages/adapters-log-sqlite/vitest.config.ts +15 -0
  134. package/packages/adapters-mongodb/README.md +147 -0
  135. package/packages/adapters-mongodb/eslint.config.js +27 -0
  136. package/packages/adapters-mongodb/package.json +53 -0
  137. package/packages/adapters-mongodb/src/index.ts +428 -0
  138. package/packages/adapters-mongodb/src/manifest.ts +45 -0
  139. package/packages/adapters-mongodb/src/secure-document.ts +231 -0
  140. package/packages/adapters-mongodb/tsconfig.build.json +15 -0
  141. package/packages/adapters-mongodb/tsconfig.json +9 -0
  142. package/packages/adapters-mongodb/tsup.config.ts +8 -0
  143. package/packages/adapters-openai/README.md +151 -0
  144. package/packages/adapters-openai/embeddings.ts +37 -0
  145. package/packages/adapters-openai/eslint.config.js +26 -0
  146. package/packages/adapters-openai/index.ts +22 -0
  147. package/packages/adapters-openai/package.json +57 -0
  148. package/packages/adapters-openai/src/embeddings-manifest.ts +45 -0
  149. package/packages/adapters-openai/src/embeddings.ts +104 -0
  150. package/packages/adapters-openai/src/index.ts +13 -0
  151. package/packages/adapters-openai/src/llm.ts +304 -0
  152. package/packages/adapters-openai/src/manifest.ts +47 -0
  153. package/packages/adapters-openai/tsconfig.build.json +15 -0
  154. package/packages/adapters-openai/tsconfig.json +9 -0
  155. package/packages/adapters-openai/tsup.config.ts +8 -0
  156. package/packages/adapters-pino/README.md +152 -0
  157. package/packages/adapters-pino/eslint.config.js +27 -0
  158. package/packages/adapters-pino/package.json +49 -0
  159. package/packages/adapters-pino/src/index.test.ts +44 -0
  160. package/packages/adapters-pino/src/index.ts +322 -0
  161. package/packages/adapters-pino/src/log-ring-buffer.ts +142 -0
  162. package/packages/adapters-pino/src/manifest.ts +49 -0
  163. package/packages/adapters-pino/tsconfig.build.json +15 -0
  164. package/packages/adapters-pino/tsconfig.json +9 -0
  165. package/packages/adapters-pino/tsup.config.ts +9 -0
  166. package/packages/adapters-pino-http/README.md +141 -0
  167. package/packages/adapters-pino-http/eslint.config.js +27 -0
  168. package/packages/adapters-pino-http/package.json +46 -0
  169. package/packages/adapters-pino-http/src/index.ts +229 -0
  170. package/packages/adapters-pino-http/tsconfig.build.json +15 -0
  171. package/packages/adapters-pino-http/tsconfig.json +9 -0
  172. package/packages/adapters-pino-http/tsup.config.ts +9 -0
  173. package/packages/adapters-qdrant/README.md +166 -0
  174. package/packages/adapters-qdrant/eslint.config.js +27 -0
  175. package/packages/adapters-qdrant/package.json +49 -0
  176. package/packages/adapters-qdrant/src/index.ts +490 -0
  177. package/packages/adapters-qdrant/src/manifest.ts +54 -0
  178. package/packages/adapters-qdrant/src/retry.ts +204 -0
  179. package/packages/adapters-qdrant/tsconfig.build.json +15 -0
  180. package/packages/adapters-qdrant/tsconfig.json +9 -0
  181. package/packages/adapters-qdrant/tsup.config.ts +9 -0
  182. package/packages/adapters-redis/README.md +159 -0
  183. package/packages/adapters-redis/eslint.config.js +27 -0
  184. package/packages/adapters-redis/package.json +49 -0
  185. package/packages/adapters-redis/src/index.ts +164 -0
  186. package/packages/adapters-redis/src/manifest.ts +49 -0
  187. package/packages/adapters-redis/tsconfig.build.json +15 -0
  188. package/packages/adapters-redis/tsconfig.json +9 -0
  189. package/packages/adapters-redis/tsup.config.ts +9 -0
  190. package/packages/adapters-snapshot-localfs/README.md +10 -0
  191. package/packages/adapters-snapshot-localfs/eslint.config.js +2 -0
  192. package/packages/adapters-snapshot-localfs/package.json +46 -0
  193. package/packages/adapters-snapshot-localfs/src/index.test.ts +40 -0
  194. package/packages/adapters-snapshot-localfs/src/index.ts +292 -0
  195. package/packages/adapters-snapshot-localfs/src/manifest.ts +32 -0
  196. package/packages/adapters-snapshot-localfs/tsconfig.build.json +15 -0
  197. package/packages/adapters-snapshot-localfs/tsconfig.json +16 -0
  198. package/packages/adapters-snapshot-localfs/tsup.config.ts +11 -0
  199. package/packages/adapters-sqlite/README.md +163 -0
  200. package/packages/adapters-sqlite/eslint.config.js +27 -0
  201. package/packages/adapters-sqlite/package.json +54 -0
  202. package/packages/adapters-sqlite/src/index.test.ts +245 -0
  203. package/packages/adapters-sqlite/src/index.ts +382 -0
  204. package/packages/adapters-sqlite/src/manifest.ts +47 -0
  205. package/packages/adapters-sqlite/src/secure-sql.test.ts +290 -0
  206. package/packages/adapters-sqlite/src/secure-sql.ts +281 -0
  207. package/packages/adapters-sqlite/tsconfig.build.json +15 -0
  208. package/packages/adapters-sqlite/tsconfig.json +9 -0
  209. package/packages/adapters-sqlite/tsup.config.ts +8 -0
  210. package/packages/adapters-sqlite/vitest.config.ts +19 -0
  211. package/packages/adapters-transport/README.md +170 -0
  212. package/packages/adapters-transport/eslint.config.js +27 -0
  213. package/packages/adapters-transport/package.json +49 -0
  214. package/packages/adapters-transport/src/__tests__/unix-socket-server.test.ts +550 -0
  215. package/packages/adapters-transport/src/index.ts +101 -0
  216. package/packages/adapters-transport/src/ipc-transport.ts +228 -0
  217. package/packages/adapters-transport/src/transport.ts +224 -0
  218. package/packages/adapters-transport/src/types.ts +92 -0
  219. package/packages/adapters-transport/src/unix-socket-server.ts +193 -0
  220. package/packages/adapters-transport/src/unix-socket-transport.ts +280 -0
  221. package/packages/adapters-transport/tsconfig.build.json +15 -0
  222. package/packages/adapters-transport/tsconfig.json +9 -0
  223. package/packages/adapters-transport/tsup.config.ts +9 -0
  224. package/packages/adapters-vibeproxy/README.md +159 -0
  225. package/packages/adapters-vibeproxy/eslint.config.js +27 -0
  226. package/packages/adapters-vibeproxy/package.json +51 -0
  227. package/packages/adapters-vibeproxy/src/index.ts +13 -0
  228. package/packages/adapters-vibeproxy/src/llm.ts +437 -0
  229. package/packages/adapters-vibeproxy/src/manifest.ts +51 -0
  230. package/packages/adapters-vibeproxy/tsconfig.build.json +15 -0
  231. package/packages/adapters-vibeproxy/tsconfig.json +9 -0
  232. package/packages/adapters-vibeproxy/tsup.config.ts +8 -0
  233. package/packages/adapters-workspace-agent/package.json +46 -0
  234. package/packages/adapters-workspace-agent/src/__tests__/adapter.test.ts +212 -0
  235. package/packages/adapters-workspace-agent/src/index.ts +220 -0
  236. package/packages/adapters-workspace-agent/src/manifest.ts +36 -0
  237. package/packages/adapters-workspace-agent/tsconfig.build.json +15 -0
  238. package/packages/adapters-workspace-agent/tsconfig.json +16 -0
  239. package/packages/adapters-workspace-agent/tsup.config.ts +11 -0
  240. package/packages/adapters-workspace-localfs/README.md +9 -0
  241. package/packages/adapters-workspace-localfs/eslint.config.js +2 -0
  242. package/packages/adapters-workspace-localfs/package.json +46 -0
  243. package/packages/adapters-workspace-localfs/src/index.test.ts +27 -0
  244. package/packages/adapters-workspace-localfs/src/index.ts +172 -0
  245. package/packages/adapters-workspace-localfs/src/manifest.ts +32 -0
  246. package/packages/adapters-workspace-localfs/tsconfig.build.json +15 -0
  247. package/packages/adapters-workspace-localfs/tsconfig.json +16 -0
  248. package/packages/adapters-workspace-localfs/tsup.config.ts +11 -0
  249. package/packages/adapters-workspace-worktree/README.md +9 -0
  250. package/packages/adapters-workspace-worktree/eslint.config.js +2 -0
  251. package/packages/adapters-workspace-worktree/package.json +46 -0
  252. package/packages/adapters-workspace-worktree/src/index.test.ts +38 -0
  253. package/packages/adapters-workspace-worktree/src/index.ts +245 -0
  254. package/packages/adapters-workspace-worktree/src/manifest.ts +38 -0
  255. package/packages/adapters-workspace-worktree/tsconfig.build.json +15 -0
  256. package/packages/adapters-workspace-worktree/tsconfig.json +16 -0
  257. package/packages/adapters-workspace-worktree/tsup.config.ts +11 -0
  258. package/pnpm-workspace.yaml +2800 -0
  259. package/prettierrc.json +1 -0
  260. package/scripts/devkit-sync.mjs +37 -0
  261. package/scripts/hooks/post-push +9 -0
  262. package/scripts/hooks/pre-commit +9 -0
  263. package/scripts/hooks/pre-push +9 -0
  264. package/test-integration.ts +242 -0
  265. package/test.txt +1 -0
  266. package/tsconfig.base.json +6 -0
  267. package/tsconfig.build.json +15 -0
  268. package/tsconfig.json +9 -0
  269. package/tsconfig.paths.json +26 -0
  270. package/tsconfig.tools.json +17 -0
  271. package/tsup.config.bin.ts +34 -0
  272. package/tsup.config.cli.ts +41 -0
  273. package/tsup.config.dual.ts +46 -0
  274. package/tsup.config.ts +36 -0
  275. package/tsup.external.json +103 -0
  276. package/vitest.config.ts +2 -0
@@ -0,0 +1,380 @@
1
+ /**
2
+ * @module @kb-labs/adapters-fs/__tests__/secure-storage
3
+ * Unit tests for SecureStorageAdapter
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { mkdtemp, rm } from "node:fs/promises";
10
+ import { createAdapter } from "./index.js";
11
+ import {
12
+ SecureStorageAdapter,
13
+ StoragePermissionError,
14
+ } from "./secure-storage.js";
15
+
16
+ describe("SecureStorageAdapter", () => {
17
+ let tmpDir: string;
18
+ let baseStorage: ReturnType<typeof createAdapter>;
19
+
20
+ beforeEach(async () => {
21
+ tmpDir = await mkdtemp(join(tmpdir(), "kb-test-secure-"));
22
+ baseStorage = createAdapter({ baseDir: tmpDir });
23
+
24
+ // Create some test files
25
+ await baseStorage.write("public/file.txt", Buffer.from("public"));
26
+ await baseStorage.write("private/secret.txt", Buffer.from("secret"));
27
+ await baseStorage.write("docs/readme.md", Buffer.from("readme"));
28
+ });
29
+
30
+ afterEach(async () => {
31
+ await rm(tmpDir, { recursive: true, force: true });
32
+ });
33
+
34
+ describe("Operation Permissions", () => {
35
+ it("should allow read when permission granted", async () => {
36
+ const secure = new SecureStorageAdapter(baseStorage, { read: true });
37
+
38
+ const content = await secure.read("public/file.txt");
39
+
40
+ expect(content?.toString()).toBe("public");
41
+ });
42
+
43
+ it("should deny read when permission not granted", async () => {
44
+ const secure = new SecureStorageAdapter(baseStorage, { read: false });
45
+
46
+ await expect(secure.read("public/file.txt")).rejects.toThrow(
47
+ StoragePermissionError,
48
+ );
49
+ await expect(secure.read("public/file.txt")).rejects.toThrow(
50
+ "read operations are disabled",
51
+ );
52
+ });
53
+
54
+ it("should allow write when permission granted", async () => {
55
+ const secure = new SecureStorageAdapter(baseStorage, { write: true });
56
+
57
+ await secure.write("new/file.txt", Buffer.from("new content"));
58
+
59
+ const content = await baseStorage.read("new/file.txt");
60
+ expect(content?.toString()).toBe("new content");
61
+ });
62
+
63
+ it("should deny write when permission not granted", async () => {
64
+ const secure = new SecureStorageAdapter(baseStorage, { write: false });
65
+
66
+ await expect(
67
+ secure.write("new/file.txt", Buffer.from("test")),
68
+ ).rejects.toThrow(StoragePermissionError);
69
+ });
70
+
71
+ it("should allow delete when permission granted", async () => {
72
+ const secure = new SecureStorageAdapter(baseStorage, { delete: true });
73
+
74
+ await secure.delete("public/file.txt");
75
+
76
+ expect(await baseStorage.exists("public/file.txt")).toBe(false);
77
+ });
78
+
79
+ it("should deny delete when permission explicitly set to false", async () => {
80
+ const secure = new SecureStorageAdapter(baseStorage, { delete: false });
81
+
82
+ await expect(secure.delete("public/file.txt")).rejects.toThrow(
83
+ StoragePermissionError,
84
+ );
85
+ await expect(secure.delete("public/file.txt")).rejects.toThrow(
86
+ "delete operations are disabled",
87
+ );
88
+ });
89
+ });
90
+
91
+ describe("Path Allowlist", () => {
92
+ it("should allow access to allowlisted paths", async () => {
93
+ const secure = new SecureStorageAdapter(baseStorage, {
94
+ allowlist: ["public/", "docs/"],
95
+ read: true,
96
+ });
97
+
98
+ const content1 = await secure.read("public/file.txt");
99
+ const content2 = await secure.read("docs/readme.md");
100
+
101
+ expect(content1?.toString()).toBe("public");
102
+ expect(content2?.toString()).toBe("readme");
103
+ });
104
+
105
+ it("should deny access to non-allowlisted paths", async () => {
106
+ const secure = new SecureStorageAdapter(baseStorage, {
107
+ allowlist: ["public/"],
108
+ read: true,
109
+ });
110
+
111
+ await expect(secure.read("private/secret.txt")).rejects.toThrow(
112
+ StoragePermissionError,
113
+ );
114
+ await expect(secure.read("private/secret.txt")).rejects.toThrow(
115
+ "not in allowlist",
116
+ );
117
+ });
118
+
119
+ it("should work with empty allowlist (allow all)", async () => {
120
+ const secure = new SecureStorageAdapter(baseStorage, {
121
+ allowlist: [],
122
+ read: true,
123
+ });
124
+
125
+ const content = await secure.read("private/secret.txt");
126
+
127
+ expect(content?.toString()).toBe("secret");
128
+ });
129
+
130
+ it("should work with undefined allowlist (allow all)", async () => {
131
+ const secure = new SecureStorageAdapter(baseStorage, {
132
+ read: true,
133
+ });
134
+
135
+ const content = await secure.read("private/secret.txt");
136
+
137
+ expect(content?.toString()).toBe("secret");
138
+ });
139
+ });
140
+
141
+ describe("Path Denylist", () => {
142
+ it("should deny access to denylisted paths", async () => {
143
+ const secure = new SecureStorageAdapter(baseStorage, {
144
+ denylist: ["private/"],
145
+ read: true,
146
+ });
147
+
148
+ await expect(secure.read("private/secret.txt")).rejects.toThrow(
149
+ StoragePermissionError,
150
+ );
151
+ await expect(secure.read("private/secret.txt")).rejects.toThrow(
152
+ "path matches denylist",
153
+ );
154
+ });
155
+
156
+ it("should allow access to non-denylisted paths", async () => {
157
+ const secure = new SecureStorageAdapter(baseStorage, {
158
+ denylist: ["private/"],
159
+ read: true,
160
+ });
161
+
162
+ const content = await secure.read("public/file.txt");
163
+
164
+ expect(content?.toString()).toBe("public");
165
+ });
166
+
167
+ it("should give denylist precedence over allowlist", async () => {
168
+ const secure = new SecureStorageAdapter(baseStorage, {
169
+ allowlist: ["private/"],
170
+ denylist: ["private/"],
171
+ read: true,
172
+ });
173
+
174
+ await expect(secure.read("private/secret.txt")).rejects.toThrow(
175
+ StoragePermissionError,
176
+ );
177
+ await expect(secure.read("private/secret.txt")).rejects.toThrow(
178
+ "path matches denylist",
179
+ );
180
+ });
181
+ });
182
+
183
+ describe("List Operations with Permissions", () => {
184
+ it("should list files respecting permissions", async () => {
185
+ const secure = new SecureStorageAdapter(baseStorage, {
186
+ allowlist: ["public/"],
187
+ read: true,
188
+ });
189
+
190
+ const files = await secure.list("public/");
191
+
192
+ expect(files).toContain("public/file.txt");
193
+ });
194
+
195
+ it("should deny list for non-allowlisted paths", async () => {
196
+ const secure = new SecureStorageAdapter(baseStorage, {
197
+ allowlist: ["public/"],
198
+ read: true,
199
+ });
200
+
201
+ await expect(secure.list("private/")).rejects.toThrow(
202
+ StoragePermissionError,
203
+ );
204
+ });
205
+ });
206
+
207
+ describe("Exists with Permissions", () => {
208
+ it("should check existence when allowed", async () => {
209
+ const secure = new SecureStorageAdapter(baseStorage, {
210
+ allowlist: ["public/"],
211
+ read: true,
212
+ });
213
+
214
+ expect(await secure.exists("public/file.txt")).toBe(true);
215
+ });
216
+
217
+ it("should return false for denied paths (security by obscurity)", async () => {
218
+ const secure = new SecureStorageAdapter(baseStorage, {
219
+ allowlist: ["public/"],
220
+ read: true,
221
+ });
222
+
223
+ // exists() returns false for denied paths instead of throwing error
224
+ // This prevents information leakage about file existence
225
+ expect(await secure.exists("private/secret.txt")).toBe(false);
226
+ });
227
+ });
228
+
229
+ describe("Extended Methods with Permissions", () => {
230
+ it("should allow stat when permitted", async () => {
231
+ const secure = new SecureStorageAdapter(baseStorage, {
232
+ allowlist: ["public/"],
233
+ read: true,
234
+ });
235
+
236
+ const metadata = await secure.stat?.("public/file.txt");
237
+
238
+ expect(metadata).not.toBeNull();
239
+ expect(metadata?.path).toBe("public/file.txt");
240
+ });
241
+
242
+ it("should deny stat when not permitted", async () => {
243
+ const secure = new SecureStorageAdapter(baseStorage, {
244
+ allowlist: ["public/"],
245
+ read: true,
246
+ });
247
+
248
+ await expect(secure.stat?.("private/secret.txt")).rejects.toThrow(
249
+ StoragePermissionError,
250
+ );
251
+ });
252
+
253
+ it("should allow copy when both paths permitted", async () => {
254
+ const secure = new SecureStorageAdapter(baseStorage, {
255
+ allowlist: ["public/"],
256
+ read: true,
257
+ write: true,
258
+ });
259
+
260
+ await secure.copy?.("public/file.txt", "public/copy.txt");
261
+
262
+ expect(await baseStorage.exists("public/copy.txt")).toBe(true);
263
+ });
264
+
265
+ it("should deny copy when source not permitted", async () => {
266
+ const secure = new SecureStorageAdapter(baseStorage, {
267
+ allowlist: ["public/"],
268
+ read: true,
269
+ write: true,
270
+ });
271
+
272
+ await expect(
273
+ secure.copy?.("private/secret.txt", "public/copy.txt"),
274
+ ).rejects.toThrow(StoragePermissionError);
275
+ });
276
+
277
+ it("should deny copy when destination not permitted", async () => {
278
+ const secure = new SecureStorageAdapter(baseStorage, {
279
+ allowlist: ["public/"],
280
+ read: true,
281
+ write: true,
282
+ });
283
+
284
+ await expect(
285
+ secure.copy?.("public/file.txt", "private/copy.txt"),
286
+ ).rejects.toThrow(StoragePermissionError);
287
+ });
288
+
289
+ it("should allow move when both paths permitted", async () => {
290
+ const secure = new SecureStorageAdapter(baseStorage, {
291
+ allowlist: ["public/"],
292
+ read: true,
293
+ write: true,
294
+ });
295
+
296
+ await secure.move?.("public/file.txt", "public/moved.txt");
297
+
298
+ expect(await baseStorage.exists("public/file.txt")).toBe(false);
299
+ expect(await baseStorage.exists("public/moved.txt")).toBe(true);
300
+ });
301
+
302
+ it("should allow listWithMetadata when permitted", async () => {
303
+ const secure = new SecureStorageAdapter(baseStorage, {
304
+ allowlist: ["public/"],
305
+ read: true,
306
+ });
307
+
308
+ const files = await secure.listWithMetadata?.("public/");
309
+
310
+ expect(files).toBeDefined();
311
+ expect(files!.length).toBeGreaterThan(0);
312
+ expect(files![0]!.path).toBeDefined();
313
+ });
314
+ });
315
+
316
+ describe("Complex Permission Scenarios", () => {
317
+ it("should handle multiple allowlist patterns", async () => {
318
+ const secure = new SecureStorageAdapter(baseStorage, {
319
+ allowlist: ["public/", "docs/"],
320
+ read: true,
321
+ });
322
+
323
+ const content1 = await secure.read("public/file.txt");
324
+ const content2 = await secure.read("docs/readme.md");
325
+
326
+ expect(content1).not.toBeNull();
327
+ expect(content2).not.toBeNull();
328
+ });
329
+
330
+ it("should handle multiple denylist patterns", async () => {
331
+ const secure = new SecureStorageAdapter(baseStorage, {
332
+ denylist: ["private/", "secrets/"],
333
+ read: true,
334
+ });
335
+
336
+ await expect(secure.read("private/secret.txt")).rejects.toThrow(
337
+ StoragePermissionError,
338
+ );
339
+ });
340
+
341
+ it("should combine allowlist and denylist correctly", async () => {
342
+ const secure = new SecureStorageAdapter(baseStorage, {
343
+ allowlist: ["public/", "private/"],
344
+ denylist: ["private/"],
345
+ read: true,
346
+ });
347
+
348
+ // public/ is in allowlist and not in denylist - allowed
349
+ const content = await secure.read("public/file.txt");
350
+ expect(content).not.toBeNull();
351
+
352
+ // private/ is in both - denylist takes precedence - denied
353
+ await expect(secure.read("private/secret.txt")).rejects.toThrow(
354
+ StoragePermissionError,
355
+ );
356
+ });
357
+
358
+ it("should handle read-only permissions", async () => {
359
+ const secure = new SecureStorageAdapter(baseStorage, {
360
+ read: true,
361
+ write: false,
362
+ delete: false,
363
+ });
364
+
365
+ // Can read
366
+ const content = await secure.read("public/file.txt");
367
+ expect(content).not.toBeNull();
368
+
369
+ // Cannot write
370
+ await expect(
371
+ secure.write("new.txt", Buffer.from("test")),
372
+ ).rejects.toThrow(StoragePermissionError);
373
+
374
+ // Cannot delete
375
+ await expect(secure.delete("public/file.txt")).rejects.toThrow(
376
+ StoragePermissionError,
377
+ );
378
+ });
379
+ });
380
+ });
@@ -0,0 +1,268 @@
1
+ /**
2
+ * @module @kb-labs/adapters-fs/secure-storage
3
+ * SecureStorageAdapter - IStorage wrapper with permission validation.
4
+ *
5
+ * Design Philosophy: Validation-only security (like fs-shim)
6
+ * - Validates paths against allowlists/denylists
7
+ * - Does NOT rewrite paths or queries
8
+ * - Fails fast with clear errors
9
+ * - Transparent pass-through when permitted
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { createAdapter } from '@kb-labs/adapters-fs';
14
+ * import { SecureStorageAdapter } from '@kb-labs/adapters-fs/secure-storage';
15
+ *
16
+ * const base = createAdapter({ baseDir: '/var/data' });
17
+ * const secure = new SecureStorageAdapter(base, {
18
+ * allowlist: ['docs/', 'uploads/'],
19
+ * denylist: ['uploads/private/'],
20
+ * });
21
+ *
22
+ * await secure.write('docs/readme.md', Buffer.from('# Hello')); // ✅ Allowed
23
+ * await secure.write('config/secrets.json', Buffer.from('{}')); // ❌ Denied
24
+ * ```
25
+ */
26
+
27
+ import type {
28
+ IStorage,
29
+ StorageMetadata,
30
+ } from "@kb-labs/core-platform/adapters";
31
+
32
+ /**
33
+ * Permission configuration for storage access.
34
+ */
35
+ export interface StoragePermissions {
36
+ /**
37
+ * Allowed path prefixes (e.g., ['docs/', 'uploads/']).
38
+ * If empty or undefined, all paths are allowed (unless denied).
39
+ */
40
+ allowlist?: string[];
41
+
42
+ /**
43
+ * Denied path prefixes (e.g., ['config/', 'secrets/']).
44
+ * Takes precedence over allowlist.
45
+ */
46
+ denylist?: string[];
47
+
48
+ /**
49
+ * Allow read operations (default: true)
50
+ */
51
+ read?: boolean;
52
+
53
+ /**
54
+ * Allow write operations (default: true)
55
+ */
56
+ write?: boolean;
57
+
58
+ /**
59
+ * Allow delete operations (default: false - safer default)
60
+ */
61
+ delete?: boolean;
62
+ }
63
+
64
+ /**
65
+ * Error thrown when storage access is denied.
66
+ */
67
+ export class StoragePermissionError extends Error {
68
+ constructor(
69
+ public readonly operation: string,
70
+ public readonly path: string,
71
+ public readonly reason: string,
72
+ ) {
73
+ super(`Storage access denied: ${operation} ${path} - ${reason}`);
74
+ this.name = "StoragePermissionError";
75
+ }
76
+ }
77
+
78
+ /**
79
+ * SecureStorageAdapter - validates permissions before delegating to base storage.
80
+ *
81
+ * Design:
82
+ * - Validation-only (no path rewriting)
83
+ * - Fails fast with clear errors
84
+ * - Transparent pass-through when permitted
85
+ * - Supports both coarse (read/write/delete) and fine (path-based) permissions
86
+ */
87
+ export class SecureStorageAdapter implements IStorage {
88
+ constructor(
89
+ private readonly baseStorage: IStorage,
90
+ private readonly permissions: StoragePermissions,
91
+ ) {}
92
+
93
+ /**
94
+ * Check if a path is allowed by permissions.
95
+ */
96
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- Security-critical permission check with denylist/allowlist logic
97
+ private checkPath(
98
+ path: string,
99
+ operation: "read" | "write" | "delete",
100
+ ): void {
101
+ // Check operation-level permissions
102
+ const operationAllowed = this.permissions[operation] !== false;
103
+ if (!operationAllowed) {
104
+ throw new StoragePermissionError(
105
+ operation,
106
+ path,
107
+ `${operation} operations are disabled`,
108
+ );
109
+ }
110
+
111
+ // Check denylist first (takes precedence)
112
+ if (this.permissions.denylist) {
113
+ for (const denied of this.permissions.denylist) {
114
+ if (path.startsWith(denied)) {
115
+ throw new StoragePermissionError(
116
+ operation,
117
+ path,
118
+ `path matches denylist: ${denied}`,
119
+ );
120
+ }
121
+ }
122
+ }
123
+
124
+ // Check allowlist (if defined)
125
+ if (this.permissions.allowlist && this.permissions.allowlist.length > 0) {
126
+ let allowed = false;
127
+ for (const prefix of this.permissions.allowlist) {
128
+ if (path.startsWith(prefix)) {
129
+ allowed = true;
130
+ break;
131
+ }
132
+ }
133
+
134
+ if (!allowed) {
135
+ throw new StoragePermissionError(
136
+ operation,
137
+ path,
138
+ `path not in allowlist: [${this.permissions.allowlist.join(", ")}]`,
139
+ );
140
+ }
141
+ }
142
+ }
143
+
144
+ // ═══════════════════════════════════════════════════════════════════════
145
+ // Core IStorage methods (required)
146
+ // ═══════════════════════════════════════════════════════════════════════
147
+
148
+ async read(path: string): Promise<Buffer | null> {
149
+ this.checkPath(path, "read");
150
+ return this.baseStorage.read(path);
151
+ }
152
+
153
+ async write(path: string, data: Buffer): Promise<void> {
154
+ this.checkPath(path, "write");
155
+ await this.baseStorage.write(path, data);
156
+ }
157
+
158
+ async delete(path: string): Promise<void> {
159
+ this.checkPath(path, "delete");
160
+ await this.baseStorage.delete(path);
161
+ }
162
+
163
+ async list(prefix: string): Promise<string[]> {
164
+ this.checkPath(prefix, "read");
165
+ const files = await this.baseStorage.list(prefix);
166
+
167
+ // Filter results by permissions (double-check each file)
168
+ return files.filter((file) => {
169
+ try {
170
+ this.checkPath(file, "read");
171
+ return true;
172
+ } catch {
173
+ return false; // Silently exclude files that don't pass permission check
174
+ }
175
+ });
176
+ }
177
+
178
+ async exists(path: string): Promise<boolean> {
179
+ try {
180
+ this.checkPath(path, "read");
181
+ return await this.baseStorage.exists(path);
182
+ } catch (error) {
183
+ if (error instanceof StoragePermissionError) {
184
+ return false; // Treat permission denied as "not exists" for safety
185
+ }
186
+ throw error;
187
+ }
188
+ }
189
+
190
+ // ═══════════════════════════════════════════════════════════════════════
191
+ // Extended IStorage methods (optional)
192
+ // ═══════════════════════════════════════════════════════════════════════
193
+
194
+ async stat?(path: string): Promise<StorageMetadata | null> {
195
+ if (!this.baseStorage.stat) {
196
+ return null;
197
+ }
198
+
199
+ this.checkPath(path, "read");
200
+ return this.baseStorage.stat(path);
201
+ }
202
+
203
+ async copy?(sourcePath: string, destPath: string): Promise<void> {
204
+ if (!this.baseStorage.copy) {
205
+ throw new Error("copy() not supported by base storage adapter");
206
+ }
207
+
208
+ this.checkPath(sourcePath, "read");
209
+ this.checkPath(destPath, "write");
210
+ await this.baseStorage.copy(sourcePath, destPath);
211
+ }
212
+
213
+ async move?(sourcePath: string, destPath: string): Promise<void> {
214
+ if (!this.baseStorage.move) {
215
+ throw new Error("move() not supported by base storage adapter");
216
+ }
217
+
218
+ this.checkPath(sourcePath, "read");
219
+ this.checkPath(sourcePath, "delete"); // Move = read + delete source
220
+ this.checkPath(destPath, "write");
221
+ await this.baseStorage.move(sourcePath, destPath);
222
+ }
223
+
224
+ async listWithMetadata?(prefix: string): Promise<StorageMetadata[]> {
225
+ if (!this.baseStorage.listWithMetadata) {
226
+ return [];
227
+ }
228
+
229
+ this.checkPath(prefix, "read");
230
+ const files = await this.baseStorage.listWithMetadata(prefix);
231
+
232
+ // Filter results by permissions (double-check each file)
233
+ return files.filter((file) => {
234
+ try {
235
+ this.checkPath(file.path, "read");
236
+ return true;
237
+ } catch {
238
+ return false; // Silently exclude files that don't pass permission check
239
+ }
240
+ });
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Create secure storage adapter with permission validation.
246
+ *
247
+ * @param baseStorage - Base storage adapter (filesystem, S3, etc.)
248
+ * @param permissions - Permission configuration
249
+ * @returns Wrapped storage adapter with permission checks
250
+ *
251
+ * @example
252
+ * ```typescript
253
+ * const secure = createSecureStorage(base, {
254
+ * allowlist: ['docs/', 'uploads/'],
255
+ * denylist: ['uploads/private/'],
256
+ * delete: false, // Prevent deletions
257
+ * });
258
+ * ```
259
+ */
260
+ export function createSecureStorage(
261
+ baseStorage: IStorage,
262
+ permissions: StoragePermissions,
263
+ ): SecureStorageAdapter {
264
+ return new SecureStorageAdapter(baseStorage, permissions);
265
+ }
266
+
267
+ // Default export for direct import
268
+ export default createSecureStorage;
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ text
@@ -0,0 +1 @@
1
+ data
@@ -0,0 +1 @@
1
+ content1
@@ -0,0 +1 @@
1
+ content2
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "baseUrl": ".",
6
+ "paths": {}
7
+ },
8
+ "include": [
9
+ "src/**/*"
10
+ ],
11
+ "exclude": [
12
+ "dist",
13
+ "node_modules"
14
+ ]
15
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "@kb-labs/devkit/tsconfig/node.json",
4
+ "compilerOptions": {
5
+ "rootDir": "src",
6
+ "outDir": "dist"
7
+ },
8
+ "include": ["src"]
9
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'tsup';
2
+ import nodePreset from '@kb-labs/devkit/tsup/node';
3
+
4
+ export default defineConfig({
5
+ ...nodePreset,
6
+ tsconfig: 'tsconfig.build.json',
7
+ dts: true,
8
+ });