keyv-github 1.3.0 → 1.4.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 CHANGED
@@ -99,6 +99,8 @@ const store = new KeyvGithub("owner/repo", {
99
99
 
100
100
  ## Key rules
101
101
 
102
+ ⚠️ **Keys are validated but NOT sanitized.** You must sanitize keys yourself before passing them to this adapter. Invalid keys will throw an error.
103
+
102
104
  Keys must be valid relative file paths:
103
105
 
104
106
  - Non-empty
@@ -109,6 +111,18 @@ Keys must be valid relative file paths:
109
111
 
110
112
  Invalid keys throw synchronously before any API request.
111
113
 
114
+ ```ts
115
+ // ✗ These will throw errors
116
+ await store.set("/absolute/path", "value"); // leading slash
117
+ await store.set("path/", "value"); // trailing slash
118
+ await store.set("path/../escape", "value"); // directory traversal
119
+ await store.set("path//double", "value"); // double slashes
120
+
121
+ // ✓ Valid keys
122
+ await store.set("data/file.txt", "value");
123
+ await store.set("nested/path/key.json", "value");
124
+ ```
125
+
112
126
  ## See Also
113
127
 
114
128
  Other Keyv storage adapters by the same author:
package/dist/index.d.mts CHANGED
@@ -12,8 +12,20 @@ interface KeyvGithubOptions {
12
12
  url: string;
13
13
  branch?: string;
14
14
  client?: Octokit | Octokit["rest"];
15
- /** Customize the commit message. value is null for deletes. */
15
+ /**
16
+ * Customize the commit message for single-key operations. value is null for deletes.
17
+ * @warning Consider adding `[skip ci]` to your commit messages to prevent
18
+ * triggering CI workflows on each key-value update.
19
+ */
16
20
  msg?: (key: string, value: string | null) => string;
21
+ /**
22
+ * Customize the commit message for batch operations (setMany, deleteMany, clear).
23
+ * @param operation - 'set' | 'delete' | 'clear'
24
+ * @param paths - array of file paths being modified
25
+ * @warning Consider adding `[skip ci]` to your commit messages to prevent
26
+ * triggering CI workflows on each key-value update.
27
+ */
28
+ batchMsg?: (operation: "set" | "delete" | "clear", paths: string[]) => string;
17
29
  /** clear() deletes every file in the repo and is disabled by default. Set to true to allow it. */
18
30
  enableClear?: boolean;
19
31
  /** Path prefix prepended to every key (e.g. 'data/'). Defaults to ''. */
@@ -28,6 +40,10 @@ interface KeyvGithubOptions {
28
40
  *
29
41
  * Each key is a file path in the repo; the file content is the value.
30
42
  * Example: new KeyvGithub("https://github.com/owner/repo/tree/main", { client })
43
+ *
44
+ * @warning **Keys are validated but NOT sanitized.** You must ensure keys are valid
45
+ * GitHub file paths before calling any method. Invalid keys throw an error.
46
+ * Requirements: non-empty, no leading/trailing `/`, no `//`, no `.`/`..` segments, no null bytes.
31
47
  */
32
48
  declare class KeyvGithub extends EventEmitter implements KeyvStoreAdapter {
33
49
  opts: KeyvGithubOptions;
@@ -39,6 +55,7 @@ declare class KeyvGithub extends EventEmitter implements KeyvStoreAdapter {
39
55
  get branch(): string;
40
56
  rest: Octokit["rest"];
41
57
  private msg;
58
+ private batchMsg;
42
59
  readonly enableClear: boolean;
43
60
  readonly prefix: string;
44
61
  readonly suffix: string;
package/dist/index.mjs CHANGED
@@ -7,6 +7,10 @@ import { Octokit } from "octokit";
7
7
  *
8
8
  * Each key is a file path in the repo; the file content is the value.
9
9
  * Example: new KeyvGithub("https://github.com/owner/repo/tree/main", { client })
10
+ *
11
+ * @warning **Keys are validated but NOT sanitized.** You must ensure keys are valid
12
+ * GitHub file paths before calling any method. Invalid keys throw an error.
13
+ * Requirements: non-empty, no leading/trailing `/`, no `//`, no `.`/`..` segments, no null bytes.
10
14
  */
11
15
  var KeyvGithub = class KeyvGithub extends EventEmitter {
12
16
  opts;
@@ -20,6 +24,7 @@ var KeyvGithub = class KeyvGithub extends EventEmitter {
20
24
  }
21
25
  rest;
22
26
  msg;
27
+ batchMsg;
23
28
  enableClear;
24
29
  prefix;
25
30
  suffix;
@@ -37,7 +42,12 @@ var KeyvGithub = class KeyvGithub extends EventEmitter {
37
42
  ...options
38
43
  };
39
44
  this.rest = options.client instanceof Octokit ? options.client.rest : options.client ?? new Octokit().rest;
40
- this.msg = options.msg ?? ((key, value) => value === null ? `delete ${key}` : `update ${key}`);
45
+ this.msg = options.msg ?? ((key, value) => value === null ? `delete ${key} [skip ci]` : `update ${key} [skip ci]`);
46
+ this.batchMsg = options.batchMsg ?? ((op, paths) => {
47
+ const n = paths.length;
48
+ if (op === "clear") return `clear: remove ${n} files [skip ci]`;
49
+ return `batch ${op} ${n} files [skip ci]`;
50
+ });
41
51
  this.enableClear = options.enableClear ?? false;
42
52
  this.prefix = options.prefix ?? "";
43
53
  this.suffix = options.suffix ?? "";
@@ -223,7 +233,8 @@ var KeyvGithub = class KeyvGithub extends EventEmitter {
223
233
  this.validatePath(this.toPath(key));
224
234
  }
225
235
  const entries = values.map(({ key, value }) => [this.toPath(key), String(value)]);
226
- const message = entries.length === 1 ? this.msg(entries[0][0], entries[0][1]) : `batch update ${entries.length} files`;
236
+ const paths = entries.map(([p]) => p);
237
+ const message = entries.length === 1 ? this.msg(entries[0][0], entries[0][1]) : this.batchMsg("set", paths);
227
238
  await this._batchCommit({
228
239
  set: entries,
229
240
  message
@@ -250,7 +261,7 @@ var KeyvGithub = class KeyvGithub extends EventEmitter {
250
261
  const existingPaths = new Set(treeData.tree.filter((i) => i.type === "blob" && i.path).map((i) => i.path));
251
262
  const toDelete = keys.map((k) => this.toPath(k)).filter((p) => existingPaths.has(p));
252
263
  if (toDelete.length === 0) return false;
253
- const message = toDelete.length === 1 ? this.msg(toDelete[0], null) : `batch delete ${toDelete.length} files`;
264
+ const message = toDelete.length === 1 ? this.msg(toDelete[0], null) : this.batchMsg("delete", toDelete);
254
265
  await this._batchCommit({
255
266
  delete: toDelete,
256
267
  message
@@ -273,7 +284,7 @@ var KeyvGithub = class KeyvGithub extends EventEmitter {
273
284
  const allPaths = treeData.tree.filter((i) => i.type === "blob" && i.path && i.path.startsWith(this.prefix) && i.path.endsWith(this.suffix)).map((i) => i.path);
274
285
  if (allPaths.length > 0) await this._batchCommit({
275
286
  delete: allPaths,
276
- message: `clear: remove ${allPaths.length} files`
287
+ message: this.batchMsg("clear", allPaths)
277
288
  });
278
289
  }
279
290
  async *iterator(prefix) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keyv-github",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "module": "src/index.ts",
5
5
  "main": "dist/index.mjs",
6
6
  "types": "dist/index.d.mts",
package/src/index.test.ts CHANGED
@@ -386,7 +386,7 @@ describe("setMany", () => {
386
386
  expect(mockFiles.get("b/2.txt")?.content).toBe("world");
387
387
  expect(mockFiles.get("c/3.txt")?.content).toBe("!");
388
388
  expect(messages).toHaveLength(1); // single commit
389
- expect(messages[0]).toBe("batch update 3 files");
389
+ expect(messages[0]).toBe("batch set 3 files [skip ci]");
390
390
  });
391
391
 
392
392
  test("uses msg hook for single-entry batch", async () => {
@@ -440,7 +440,7 @@ describe("deleteMany", () => {
440
440
  expect(mockFiles.has("b")).toBe(false);
441
441
  expect(mockFiles.has("c")).toBe(true);
442
442
  expect(messages).toHaveLength(1); // single commit
443
- expect(messages[0]).toBe("batch delete 2 files");
443
+ expect(messages[0]).toBe("batch delete 2 files [skip ci]");
444
444
  });
445
445
 
446
446
  test("returns false when no keys exist", async () => {
@@ -527,15 +527,15 @@ describe("msg hook", () => {
527
527
  expect(messages[messages.length - 1]).toBe("chore: rm to/remove");
528
528
  });
529
529
 
530
- test("default msg falls back to 'update <key>' / 'delete <key>'", async () => {
530
+ test("default msg falls back to 'update <key> [skip ci]' / 'delete <key> [skip ci]'", async () => {
531
531
  const { store, messages } = makeStore();
532
532
  await store.set("k", "v");
533
- expect(messages[messages.length - 1]).toBe("update k");
533
+ expect(messages[messages.length - 1]).toBe("update k [skip ci]");
534
534
 
535
535
  const files2 = new Map([["k2", { content: "v", sha: "s1" }]]);
536
536
  const { store: store2, messages: messages2 } = makeStore(files2);
537
537
  await store2.delete("k2");
538
- expect(messages2[messages2.length - 1]).toBe("delete k2");
538
+ expect(messages2[messages2.length - 1]).toBe("delete k2 [skip ci]");
539
539
  });
540
540
  });
541
541
 
package/src/index.ts CHANGED
@@ -12,8 +12,20 @@ export interface KeyvGithubOptions {
12
12
  url: string;
13
13
  branch?: string;
14
14
  client?: Octokit | Octokit["rest"];
15
- /** Customize the commit message. value is null for deletes. */
15
+ /**
16
+ * Customize the commit message for single-key operations. value is null for deletes.
17
+ * @warning Consider adding `[skip ci]` to your commit messages to prevent
18
+ * triggering CI workflows on each key-value update.
19
+ */
16
20
  msg?: (key: string, value: string | null) => string;
21
+ /**
22
+ * Customize the commit message for batch operations (setMany, deleteMany, clear).
23
+ * @param operation - 'set' | 'delete' | 'clear'
24
+ * @param paths - array of file paths being modified
25
+ * @warning Consider adding `[skip ci]` to your commit messages to prevent
26
+ * triggering CI workflows on each key-value update.
27
+ */
28
+ batchMsg?: (operation: "set" | "delete" | "clear", paths: string[]) => string;
17
29
  /** clear() deletes every file in the repo and is disabled by default. Set to true to allow it. */
18
30
  enableClear?: boolean;
19
31
  /** Path prefix prepended to every key (e.g. 'data/'). Defaults to ''. */
@@ -29,6 +41,10 @@ export interface KeyvGithubOptions {
29
41
  *
30
42
  * Each key is a file path in the repo; the file content is the value.
31
43
  * Example: new KeyvGithub("https://github.com/owner/repo/tree/main", { client })
44
+ *
45
+ * @warning **Keys are validated but NOT sanitized.** You must ensure keys are valid
46
+ * GitHub file paths before calling any method. Invalid keys throw an error.
47
+ * Requirements: non-empty, no leading/trailing `/`, no `//`, no `.`/`..` segments, no null bytes.
32
48
  */
33
49
  export default class KeyvGithub
34
50
  extends EventEmitter
@@ -46,6 +62,10 @@ export default class KeyvGithub
46
62
  }
47
63
  rest: Octokit["rest"];
48
64
  private msg: (key: string, value: string | null) => string;
65
+ private batchMsg: (
66
+ operation: "set" | "delete" | "clear",
67
+ paths: string[],
68
+ ) => string;
49
69
  readonly enableClear: boolean;
50
70
  readonly prefix: string;
51
71
  readonly suffix: string;
@@ -70,7 +90,15 @@ export default class KeyvGithub
70
90
  : (options.client ?? new Octokit().rest);
71
91
  this.msg =
72
92
  options.msg ??
73
- ((key, value) => (value === null ? `delete ${key}` : `update ${key}`));
93
+ ((key, value) =>
94
+ value === null ? `delete ${key} [skip ci]` : `update ${key} [skip ci]`);
95
+ this.batchMsg =
96
+ options.batchMsg ??
97
+ ((op, paths) => {
98
+ const n = paths.length;
99
+ if (op === "clear") return `clear: remove ${n} files [skip ci]`;
100
+ return `batch ${op} ${n} files [skip ci]`;
101
+ });
74
102
  this.enableClear = options.enableClear ?? false;
75
103
  this.prefix = options.prefix ?? "";
76
104
  this.suffix = options.suffix ?? "";
@@ -319,10 +347,11 @@ export default class KeyvGithub
319
347
  this.toPath(key),
320
348
  String(value),
321
349
  ]);
350
+ const paths = entries.map(([p]) => p);
322
351
  const message =
323
352
  entries.length === 1
324
353
  ? this.msg(entries[0]![0], entries[0]![1])
325
- : `batch update ${entries.length} files`;
354
+ : this.batchMsg("set", paths);
326
355
  await this._batchCommit({ set: entries, message });
327
356
  }
328
357
 
@@ -361,7 +390,7 @@ export default class KeyvGithub
361
390
  const message =
362
391
  toDelete.length === 1
363
392
  ? this.msg(toDelete[0]!, null)
364
- : `batch delete ${toDelete.length} files`;
393
+ : this.batchMsg("delete", toDelete);
365
394
  await this._batchCommit({ delete: toDelete, message });
366
395
  return true;
367
396
  }
@@ -397,7 +426,7 @@ export default class KeyvGithub
397
426
  if (allPaths.length > 0) {
398
427
  await this._batchCommit({
399
428
  delete: allPaths,
400
- message: `clear: remove ${allPaths.length} files`,
429
+ message: this.batchMsg("clear", allPaths),
401
430
  });
402
431
  }
403
432
  }