@cleocode/core 2026.10.13 → 2026.10.14-rc.ci.683

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 (204) hide show
  1. package/dist/build-provenance.json +2 -2
  2. package/dist/cloud/nexus-vault.d.ts +6 -2
  3. package/dist/cloud/nexus-vault.d.ts.map +1 -1
  4. package/dist/cloud/nexus-vault.js +53 -3
  5. package/dist/cloud/nexus-vault.js.map +1 -1
  6. package/dist/cloud/secret-seal.js +2 -2
  7. package/dist/cloud/secret-seal.js.map +1 -1
  8. package/dist/config/registry.d.ts +0 -21
  9. package/dist/config/registry.d.ts.map +1 -1
  10. package/dist/config/registry.js +0 -41
  11. package/dist/config/registry.js.map +1 -1
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +1 -0
  14. package/dist/config.js.map +1 -1
  15. package/dist/crypto/credentials.d.ts +13 -0
  16. package/dist/crypto/credentials.d.ts.map +1 -1
  17. package/dist/crypto/credentials.js +35 -6
  18. package/dist/crypto/credentials.js.map +1 -1
  19. package/dist/docs/docs-ops.d.ts +11 -1
  20. package/dist/docs/docs-ops.d.ts.map +1 -1
  21. package/dist/docs/docs-ops.js +38 -3
  22. package/dist/docs/docs-ops.js.map +1 -1
  23. package/dist/docs/docs-update.d.ts.map +1 -1
  24. package/dist/docs/docs-update.js +5 -1
  25. package/dist/docs/docs-update.js.map +1 -1
  26. package/dist/docs/supersede.d.ts.map +1 -1
  27. package/dist/docs/supersede.js +5 -1
  28. package/dist/docs/supersede.js.map +1 -1
  29. package/dist/docs/wikilinks.d.ts +3 -1
  30. package/dist/docs/wikilinks.d.ts.map +1 -1
  31. package/dist/docs/wikilinks.js +23 -12
  32. package/dist/docs/wikilinks.js.map +1 -1
  33. package/dist/gateway-client/generated/index.d.ts +2 -2
  34. package/dist/gateway-client/generated/index.d.ts.map +1 -1
  35. package/dist/gateway-client/generated/index.js +1 -1
  36. package/dist/gateway-client/generated/index.js.map +1 -1
  37. package/dist/gateway-client/generated/namespaces.gen.d.ts +33 -0
  38. package/dist/gateway-client/generated/namespaces.gen.d.ts.map +1 -1
  39. package/dist/gateway-client/generated/namespaces.gen.js +34 -1
  40. package/dist/gateway-client/generated/namespaces.gen.js.map +1 -1
  41. package/dist/gateway-client/generated/sdk.gen.d.ts +137 -5
  42. package/dist/gateway-client/generated/sdk.gen.d.ts.map +1 -1
  43. package/dist/gateway-client/generated/sdk.gen.js +355 -6
  44. package/dist/gateway-client/generated/sdk.gen.js.map +1 -1
  45. package/dist/gateway-client/generated/types.gen.d.ts +1811 -187
  46. package/dist/gateway-client/generated/types.gen.d.ts.map +1 -1
  47. package/dist/lib/error-message.d.ts +30 -0
  48. package/dist/lib/error-message.d.ts.map +1 -0
  49. package/dist/lib/error-message.js +65 -0
  50. package/dist/lib/error-message.js.map +1 -0
  51. package/dist/lib/index.d.ts +1 -0
  52. package/dist/lib/index.d.ts.map +1 -1
  53. package/dist/lib/index.js +1 -0
  54. package/dist/lib/index.js.map +1 -1
  55. package/dist/llm/credentials-store.d.ts +29 -16
  56. package/dist/llm/credentials-store.d.ts.map +1 -1
  57. package/dist/llm/credentials-store.js +158 -139
  58. package/dist/llm/credentials-store.js.map +1 -1
  59. package/dist/memory/brain-embedding.d.ts +4 -2
  60. package/dist/memory/brain-embedding.d.ts.map +1 -1
  61. package/dist/memory/brain-embedding.js +8 -2
  62. package/dist/memory/brain-embedding.js.map +1 -1
  63. package/dist/memory/embedding-local.d.ts +28 -5
  64. package/dist/memory/embedding-local.d.ts.map +1 -1
  65. package/dist/memory/embedding-local.js +47 -9
  66. package/dist/memory/embedding-local.js.map +1 -1
  67. package/dist/memory/engine-compat.d.ts +4 -0
  68. package/dist/memory/engine-compat.d.ts.map +1 -1
  69. package/dist/memory/engine-compat.js +76 -0
  70. package/dist/memory/engine-compat.js.map +1 -1
  71. package/dist/memory/global-brain.d.ts +93 -0
  72. package/dist/memory/global-brain.d.ts.map +1 -0
  73. package/dist/memory/global-brain.js +183 -0
  74. package/dist/memory/global-brain.js.map +1 -0
  75. package/dist/memory/patterns.d.ts +2 -2
  76. package/dist/memory/quality-feedback.d.ts.map +1 -1
  77. package/dist/memory/quality-feedback.js +0 -26
  78. package/dist/memory/quality-feedback.js.map +1 -1
  79. package/dist/memory/retrieval/observe.js +4 -4
  80. package/dist/memory/retrieval/observe.js.map +1 -1
  81. package/dist/memory/transcript-extractor.js +5 -2
  82. package/dist/memory/transcript-extractor.js.map +1 -1
  83. package/dist/orchestration/lean-change.d.ts.map +1 -1
  84. package/dist/orchestration/lean-change.js +2 -3
  85. package/dist/orchestration/lean-change.js.map +1 -1
  86. package/dist/orchestration/lean-debt.d.ts +48 -0
  87. package/dist/orchestration/lean-debt.d.ts.map +1 -0
  88. package/dist/orchestration/lean-debt.js +167 -0
  89. package/dist/orchestration/lean-debt.js.map +1 -0
  90. package/dist/sessions/briefing.d.ts.map +1 -1
  91. package/dist/sessions/briefing.js +10 -0
  92. package/dist/sessions/briefing.js.map +1 -1
  93. package/dist/store/attachment-store.d.ts.map +1 -1
  94. package/dist/store/attachment-store.js +5 -1
  95. package/dist/store/attachment-store.js.map +1 -1
  96. package/dist/store/backup-sidecar.d.ts +5 -2
  97. package/dist/store/backup-sidecar.d.ts.map +1 -1
  98. package/dist/store/backup-sidecar.js +31 -18
  99. package/dist/store/backup-sidecar.js.map +1 -1
  100. package/dist/store/credential-transfer.d.ts +13 -14
  101. package/dist/store/credential-transfer.d.ts.map +1 -1
  102. package/dist/store/credential-transfer.js +75 -23
  103. package/dist/store/credential-transfer.js.map +1 -1
  104. package/dist/store/docs-bytes-backup.d.ts +133 -0
  105. package/dist/store/docs-bytes-backup.d.ts.map +1 -0
  106. package/dist/store/docs-bytes-backup.js +450 -0
  107. package/dist/store/docs-bytes-backup.js.map +1 -0
  108. package/dist/store/llm-accounts-accessor.d.ts +107 -0
  109. package/dist/store/llm-accounts-accessor.d.ts.map +1 -0
  110. package/dist/store/llm-accounts-accessor.js +224 -0
  111. package/dist/store/llm-accounts-accessor.js.map +1 -0
  112. package/dist/store/memory-accessor.d.ts +44 -0
  113. package/dist/store/memory-accessor.d.ts.map +1 -1
  114. package/dist/store/memory-accessor.js +60 -0
  115. package/dist/store/memory-accessor.js.map +1 -1
  116. package/dist/store/memory-sqlite.d.ts +18 -0
  117. package/dist/store/memory-sqlite.d.ts.map +1 -1
  118. package/dist/store/memory-sqlite.js +40 -23
  119. package/dist/store/memory-sqlite.js.map +1 -1
  120. package/dist/store/migration-manager.d.ts.map +1 -1
  121. package/dist/store/migration-manager.js +7 -1
  122. package/dist/store/migration-manager.js.map +1 -1
  123. package/dist/store/restore-store.d.ts +12 -1
  124. package/dist/store/restore-store.d.ts.map +1 -1
  125. package/dist/store/restore-store.js +150 -11
  126. package/dist/store/restore-store.js.map +1 -1
  127. package/dist/store/row-identity-registry.d.ts +4 -4
  128. package/dist/store/row-identity-registry.d.ts.map +1 -1
  129. package/dist/store/row-identity-registry.js +35 -8
  130. package/dist/store/row-identity-registry.js.map +1 -1
  131. package/dist/store/schema/cleo-global/accounts.d.ts +6 -5
  132. package/dist/store/schema/cleo-global/accounts.d.ts.map +1 -1
  133. package/dist/store/schema/cleo-global/accounts.js +7 -6
  134. package/dist/store/schema/cleo-global/accounts.js.map +1 -1
  135. package/dist/store/schema/cleo-project/docs.d.ts +32 -0
  136. package/dist/store/schema/cleo-project/docs.d.ts.map +1 -1
  137. package/dist/store/schema/cleo-project/docs.js +9 -0
  138. package/dist/store/schema/cleo-project/docs.js.map +1 -1
  139. package/dist/store/sync/apply/applier.d.ts +3 -1
  140. package/dist/store/sync/apply/applier.d.ts.map +1 -1
  141. package/dist/store/sync/apply/applier.js +80 -19
  142. package/dist/store/sync/apply/applier.js.map +1 -1
  143. package/dist/store/sync/apply/parent-delete.d.ts.map +1 -1
  144. package/dist/store/sync/apply/parent-delete.js +4 -0
  145. package/dist/store/sync/apply/parent-delete.js.map +1 -1
  146. package/dist/store/sync/apply/write-api.d.ts +5 -1
  147. package/dist/store/sync/apply/write-api.d.ts.map +1 -1
  148. package/dist/store/sync/apply/write-api.js +22 -0
  149. package/dist/store/sync/apply/write-api.js.map +1 -1
  150. package/dist/store/sync/capture.d.ts +27 -0
  151. package/dist/store/sync/capture.d.ts.map +1 -1
  152. package/dist/store/sync/capture.js +32 -1
  153. package/dist/store/sync/capture.js.map +1 -1
  154. package/dist/store/sync/genesis.d.ts.map +1 -1
  155. package/dist/store/sync/genesis.js +3 -1
  156. package/dist/store/sync/genesis.js.map +1 -1
  157. package/dist/store/sync/inbox.d.ts +18 -2
  158. package/dist/store/sync/inbox.d.ts.map +1 -1
  159. package/dist/store/sync/inbox.js +23 -2
  160. package/dist/store/sync/inbox.js.map +1 -1
  161. package/dist/store/sync/pull.d.ts +1 -1
  162. package/dist/store/sync/pull.d.ts.map +1 -1
  163. package/dist/store/sync/pull.js +4 -1
  164. package/dist/store/sync/pull.js.map +1 -1
  165. package/dist/store/sync/timestamp-canon.d.ts.map +1 -1
  166. package/dist/store/sync/timestamp-canon.js +3 -0
  167. package/dist/store/sync/timestamp-canon.js.map +1 -1
  168. package/dist/store/table-classification.d.ts.map +1 -1
  169. package/dist/store/table-classification.js +29 -3
  170. package/dist/store/table-classification.js.map +1 -1
  171. package/dist/system/backup.d.ts +5 -2
  172. package/dist/system/backup.d.ts.map +1 -1
  173. package/dist/system/backup.js +39 -3
  174. package/dist/system/backup.js.map +1 -1
  175. package/dist/tasks/affected-scope.d.ts +44 -5
  176. package/dist/tasks/affected-scope.d.ts.map +1 -1
  177. package/dist/tasks/affected-scope.js +159 -20
  178. package/dist/tasks/affected-scope.js.map +1 -1
  179. package/dist/tasks/complete-ci.d.ts.map +1 -1
  180. package/dist/tasks/complete-ci.js +11 -9
  181. package/dist/tasks/complete-ci.js.map +1 -1
  182. package/dist/tasks/done-plan.d.ts.map +1 -1
  183. package/dist/tasks/done-plan.js +4 -6
  184. package/dist/tasks/done-plan.js.map +1 -1
  185. package/dist/tasks/gate-preview.d.ts.map +1 -1
  186. package/dist/tasks/gate-preview.js +13 -5
  187. package/dist/tasks/gate-preview.js.map +1 -1
  188. package/dist/tasks/gate-result-cache.d.ts +30 -1
  189. package/dist/tasks/gate-result-cache.d.ts.map +1 -1
  190. package/dist/tasks/gate-result-cache.js +81 -2
  191. package/dist/tasks/gate-result-cache.js.map +1 -1
  192. package/dist/tasks/gate-runner.d.ts +27 -2
  193. package/dist/tasks/gate-runner.d.ts.map +1 -1
  194. package/dist/tasks/gate-runner.js +67 -14
  195. package/dist/tasks/gate-runner.js.map +1 -1
  196. package/dist/tasks/qa-run-binding.d.ts +15 -0
  197. package/dist/tasks/qa-run-binding.d.ts.map +1 -1
  198. package/dist/tasks/qa-run-binding.js +24 -3
  199. package/dist/tasks/qa-run-binding.js.map +1 -1
  200. package/migrations/drizzle-brain/20261014180000_t13719-prune-candidate-column/migration.sql +16 -0
  201. package/migrations/drizzle-cleo-project/20261014100000_t12919-docs-row-uids/migration.sql +27 -0
  202. package/package.json +13 -13
  203. package/templates/CLEO-REFERENCE.md +1 -1
  204. package/templates/global-config.template.json +2 -1
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Bounded, single-line formatting for thrown values in warnings and logs.
3
+ *
4
+ * Drizzle (and other query builders) interpolate the full SQL text and every
5
+ * bound parameter into `error.message` — on a large failed query that is
6
+ * hundreds of kilobytes on one line of stderr (observed: ~900 KB per docs
7
+ * write on axiom-app, T13711). Warnings should carry the driver cause (the
8
+ * small, actionable "why"), never the parameter dump.
9
+ *
10
+ * Dependency-free; safe to import from any layer (lib barrel contract).
11
+ *
12
+ * @task T13711
13
+ */
14
+ /**
15
+ * Format an unknown thrown value as a single-line, length-capped message.
16
+ *
17
+ * Prefers the deepest wrapped driver cause (the end of the `error.cause`
18
+ * chain) — the underlying SQLite/driver error, which does not contain the
19
+ * parameter dump — and falls back to the outer message. Everything from a
20
+ * drizzle `params:` section onward is cut, then whitespace runs collapse to
21
+ * single spaces so multi-line dumps cannot flood the log even before the cap.
22
+ *
23
+ * @param err - The thrown value (anything `throw`-able).
24
+ * @param maxLength - Character cap for the message body. Default 500.
25
+ * @returns A single-line message: at most `maxLength` characters of the
26
+ * message, followed (only when cut) by `… [truncated N chars]`, where N is
27
+ * the number of characters removed.
28
+ */
29
+ export declare function boundedErrorMessage(err: unknown, maxLength?: number): string;
30
+ //# sourceMappingURL=error-message.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-message.d.ts","sourceRoot":"","sources":["../../src/lib/error-message.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAWH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,GAAE,MAA2B,GAAG,MAAM,CAwBhG"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Bounded, single-line formatting for thrown values in warnings and logs.
3
+ *
4
+ * Drizzle (and other query builders) interpolate the full SQL text and every
5
+ * bound parameter into `error.message` — on a large failed query that is
6
+ * hundreds of kilobytes on one line of stderr (observed: ~900 KB per docs
7
+ * write on axiom-app, T13711). Warnings should carry the driver cause (the
8
+ * small, actionable "why"), never the parameter dump.
9
+ *
10
+ * Dependency-free; safe to import from any layer (lib barrel contract).
11
+ *
12
+ * @task T13711
13
+ */
14
+ /**
15
+ * Default cap for a formatted error message, in characters.
16
+ *
17
+ * Chosen to keep a full warning line well under typical log-pipeline line
18
+ * limits while preserving the actionable prefix (error class + SQLSTATE /
19
+ * SQLite extended code + short context).
20
+ */
21
+ const DEFAULT_MAX_LENGTH = 500;
22
+ /**
23
+ * Format an unknown thrown value as a single-line, length-capped message.
24
+ *
25
+ * Prefers the deepest wrapped driver cause (the end of the `error.cause`
26
+ * chain) — the underlying SQLite/driver error, which does not contain the
27
+ * parameter dump — and falls back to the outer message. Everything from a
28
+ * drizzle `params:` section onward is cut, then whitespace runs collapse to
29
+ * single spaces so multi-line dumps cannot flood the log even before the cap.
30
+ *
31
+ * @param err - The thrown value (anything `throw`-able).
32
+ * @param maxLength - Character cap for the message body. Default 500.
33
+ * @returns A single-line message: at most `maxLength` characters of the
34
+ * message, followed (only when cut) by `… [truncated N chars]`, where N is
35
+ * the number of characters removed.
36
+ */
37
+ export function boundedErrorMessage(err, maxLength = DEFAULT_MAX_LENGTH) {
38
+ let deepest = err;
39
+ const seen = new Set([err]);
40
+ while (deepest instanceof Error && deepest.cause instanceof Error && !seen.has(deepest.cause)) {
41
+ deepest = deepest.cause;
42
+ seen.add(deepest);
43
+ }
44
+ let raw;
45
+ if (deepest instanceof Error) {
46
+ raw = deepest === err ? deepest.message : `${deepest.name}: ${deepest.message}`;
47
+ }
48
+ else {
49
+ try {
50
+ raw = String(deepest);
51
+ }
52
+ catch {
53
+ // String() throws for values with no toString, e.g. Object.create(null).
54
+ raw = '[unprintable thrown value]';
55
+ }
56
+ }
57
+ const singleLine = raw
58
+ .replace(/(?:\n| )params:[\s\S]*$/, '')
59
+ .replace(/\s+/g, ' ')
60
+ .trim();
61
+ if (singleLine.length <= maxLength)
62
+ return singleLine;
63
+ return `${singleLine.slice(0, maxLength)}… [truncated ${singleLine.length - maxLength} chars]`;
64
+ }
65
+ //# sourceMappingURL=error-message.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-message.js","sourceRoot":"","sources":["../../src/lib/error-message.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;;GAMG;AACH,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAE/B;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAY,EAAE,YAAoB,kBAAkB;IACtF,IAAI,OAAO,GAAG,GAAG,CAAC;IAClB,MAAM,IAAI,GAAG,IAAI,GAAG,CAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IACrC,OAAO,OAAO,YAAY,KAAK,IAAI,OAAO,CAAC,KAAK,YAAY,KAAK,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9F,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC;QACxB,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACpB,CAAC;IACD,IAAI,GAAW,CAAC;IAChB,IAAI,OAAO,YAAY,KAAK,EAAE,CAAC;QAC7B,GAAG,GAAG,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC;IAClF,CAAC;SAAM,CAAC;QACN,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC;QAAC,MAAM,CAAC;YACP,yEAAyE;YACzE,GAAG,GAAG,4BAA4B,CAAC;QACrC,CAAC;IACH,CAAC;IACD,MAAM,UAAU,GAAG,GAAG;SACnB,OAAO,CAAC,yBAAyB,EAAE,EAAE,CAAC;SACtC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;IACV,IAAI,UAAU,CAAC,MAAM,IAAI,SAAS;QAAE,OAAO,UAAU,CAAC;IACtD,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,gBAAgB,UAAU,CAAC,MAAM,GAAG,SAAS,SAAS,CAAC;AACjG,CAAC"}
@@ -7,6 +7,7 @@
7
7
  * @module lib
8
8
  */
9
9
  export { runWithConcurrency } from './concurrency.js';
10
+ export { boundedErrorMessage } from './error-message.js';
10
11
  export { computeDelay, type RetryablePredicate, type RetryContext, type RetryOptions, withRetry, } from './retry.js';
11
12
  export { detectLanguage, grammarPackage, SUPPORTED_EXTENSIONS, SUPPORTED_LANGUAGES, type TreeSitterLanguage, } from './tree-sitter-languages.js';
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EACL,YAAY,EACZ,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,SAAS,GACV,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,KAAK,kBAAkB,GACxB,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EACL,YAAY,EACZ,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,SAAS,GACV,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,KAAK,kBAAkB,GACxB,MAAM,4BAA4B,CAAC"}
package/dist/lib/index.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * @module lib
8
8
  */
9
9
  export { runWithConcurrency } from './concurrency.js';
10
+ export { boundedErrorMessage } from './error-message.js';
10
11
  export { computeDelay, withRetry, } from './retry.js';
11
12
  export { detectLanguage, grammarPackage, SUPPORTED_EXTENSIONS, SUPPORTED_LANGUAGES, } from './tree-sitter-languages.js';
12
13
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EACL,YAAY,EAIZ,SAAS,GACV,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,mBAAmB,GAEpB,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EACL,YAAY,EAIZ,SAAS,GACV,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,mBAAmB,GAEpB,MAAM,4BAA4B,CAAC"}
@@ -1,18 +1,21 @@
1
1
  /**
2
2
  * Multi-credential pool storage for the CLEO LLM layer (T-LLM-CRED Phase 2).
3
3
  *
4
- * Persists a versioned list of provider credentials at
5
- * `~/.cleo/llm-credentials.json` (XDG-aware, resolved via `getCleoHome()` so
6
- * the test-only `XDG_DATA_HOME` override applies identically to every CLEO
7
- * global file).
4
+ * The pool lives in the global `accounts` table (T13715), written through
5
+ * the encrypting accessor `store/llm-accounts-accessor.ts`: secrets are
6
+ * stored under the machine key and the pool syncs on the home stream.
7
+ *
8
+ * `~/.cleo/llm-credentials.json` (resolved via `getCleoHome()`) is a LEGACY
9
+ * source only: {@link migrateLegacyCredentialsFile} moves its entries into
10
+ * `accounts` once, idempotently, and removes the plaintext file after reading
11
+ * every entry back. Until then the sync reader also serves the file's entries
12
+ * that `accounts` lacks, so resolution stays correct mid-migration.
8
13
  *
9
14
  * ## Goals
10
15
  *
11
16
  * - Multiple credentials per provider (label → entry), each with priority,
12
17
  * disabled-flag, expiry, and a free-form `metadata` bag.
13
- * - Strict 0600 perms on every successful write.
14
- * - Cross-process locking via `proper-lockfile` reused through `withLock`.
15
- * - Atomic writes via `writeJsonFileAtomic` (temp → rename + numbered backup).
18
+ * - Secrets encrypted at rest; never written in plaintext (T13715).
16
19
  * - Synchronous read path so the resolver in `credentials.ts` can call it
17
20
  * from its existing sync function.
18
21
  *
@@ -139,10 +142,8 @@ export interface StoredCredential {
139
142
  * that a re-add of the same `(provider, label)` pair does not silently
140
143
  * drop an existing refresh token.
141
144
  *
142
- * Security: stored in plaintext alongside the access token. The same
143
- * 0600 file permission that protects `accessToken` applies here. See the
144
- * security-review note in the S-07 block above for the Phase 2 rationale
145
- * for deferral and Phase 3 reintroduction.
145
+ * Security: encrypted at rest in `accounts.refresh_enc` under the same
146
+ * machine-key id as `accessToken` (T13715).
146
147
  *
147
148
  * @task T9266
148
149
  */
@@ -173,7 +174,8 @@ export interface CredentialsStoreData {
173
174
  credentials: StoredCredential[];
174
175
  }
175
176
  /**
176
- * Absolute path of the credential store file.
177
+ * Absolute path of the LEGACY credential store file (migrated into the
178
+ * `accounts` table by {@link migrateLegacyCredentialsFile}, T13715).
177
179
  *
178
180
  * Resolved through `getCleoHome()` so XDG overrides apply uniformly with
179
181
  * `credentials.ts`'s global-config tier.
@@ -183,6 +185,16 @@ export interface CredentialsStoreData {
183
185
  export declare function credentialsStorePath(): string;
184
186
  /** Internal test hook: reset the once-warned latch. */
185
187
  export declare function _resetPermsWarningForTests(): void;
188
+ /**
189
+ * Move the LEGACY `llm-credentials.json` entries into the `accounts` table,
190
+ * once. Idempotent: an entry `accounts` already holds is kept as it is, and
191
+ * a missing file is a no-op. The plaintext file is removed only after every
192
+ * entry reads back from `accounts`; a malformed file is left in place.
193
+ *
194
+ * @returns How many entries were moved.
195
+ * @task T13715
196
+ */
197
+ export declare function migrateLegacyCredentialsFile(): Promise<number>;
186
198
  /**
187
199
  * Type-narrow check used during read to drop malformed entries silently.
188
200
  *
@@ -230,12 +242,13 @@ export declare function getCredentialByLabel(provider: ModelTransport, label: st
230
242
  *
231
243
  * - When `input.priority` is omitted, the new entry receives
232
244
  * `max(existing priorities) + 10` so it ranks lowest by default.
233
- * - Acquires the file lock via `withLock`; safe under concurrent writers.
234
- * - chmod 0600 is enforced on the written file.
245
+ * - Stored in the `accounts` table with its secrets encrypted under the
246
+ * machine key (T13715).
235
247
  *
236
- * Returns the inserted (or replaced) entry as it now lives in the file.
248
+ * Returns the inserted (or replaced) entry as it now lives in the pool.
237
249
  *
238
250
  * @task T9257
251
+ * @task T13715
239
252
  */
240
253
  export declare function addCredential(input: Omit<StoredCredential, 'priority'> & {
241
254
  priority?: number;
@@ -244,7 +257,7 @@ export declare function addCredential(input: Omit<StoredCredential, 'priority'>
244
257
  * Remove a credential by `(provider, label)`.
245
258
  *
246
259
  * Returns `true` when a matching entry was found and removed, `false`
247
- * otherwise. Does NOT create the file when it does not yet exist.
260
+ * otherwise. A legacy-file entry is migrated first, so it cannot reappear.
248
261
  *
249
262
  * @task T9257
250
263
  */
@@ -1 +1 @@
1
- {"version":3,"file":"credentials-store.d.ts","sourceRoot":"","sources":["../../src/llm/credentials-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"credentials-store.d.ts","sourceRoot":"","sources":["../../src/llm/credentials-store.ts"],"names":[],"mappings":"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"}
@@ -1,18 +1,21 @@
1
1
  /**
2
2
  * Multi-credential pool storage for the CLEO LLM layer (T-LLM-CRED Phase 2).
3
3
  *
4
- * Persists a versioned list of provider credentials at
5
- * `~/.cleo/llm-credentials.json` (XDG-aware, resolved via `getCleoHome()` so
6
- * the test-only `XDG_DATA_HOME` override applies identically to every CLEO
7
- * global file).
4
+ * The pool lives in the global `accounts` table (T13715), written through
5
+ * the encrypting accessor `store/llm-accounts-accessor.ts`: secrets are
6
+ * stored under the machine key and the pool syncs on the home stream.
7
+ *
8
+ * `~/.cleo/llm-credentials.json` (resolved via `getCleoHome()`) is a LEGACY
9
+ * source only: {@link migrateLegacyCredentialsFile} moves its entries into
10
+ * `accounts` once, idempotently, and removes the plaintext file after reading
11
+ * every entry back. Until then the sync reader also serves the file's entries
12
+ * that `accounts` lacks, so resolution stays correct mid-migration.
8
13
  *
9
14
  * ## Goals
10
15
  *
11
16
  * - Multiple credentials per provider (label → entry), each with priority,
12
17
  * disabled-flag, expiry, and a free-form `metadata` bag.
13
- * - Strict 0600 perms on every successful write.
14
- * - Cross-process locking via `proper-lockfile` reused through `withLock`.
15
- * - Atomic writes via `writeJsonFileAtomic` (temp → rename + numbered backup).
18
+ * - Secrets encrypted at rest; never written in plaintext (T13715).
16
19
  * - Synchronous read path so the resolver in `credentials.ts` can call it
17
20
  * from its existing sync function.
18
21
  *
@@ -34,24 +37,23 @@
34
37
  * @task T9257
35
38
  * @epic T-LLM-CRED-CENTRALIZATION
36
39
  */
37
- import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
38
- import { dirname, join } from 'node:path';
40
+ import { existsSync, readFileSync, statSync, unlinkSync } from 'node:fs';
41
+ import { join } from 'node:path';
39
42
  // Note: `credentials.ts` already depends on `credentials-store.ts`
40
43
  // (`pickCredentialForProviderSync`). The reverse arrow here closes a
41
44
  // cycle, but every imported symbol is a function reference read at
42
45
  // call-time — ESM's late binding makes the cycle safe.
43
46
  import { getCleoHome } from '@cleocode/paths';
44
47
  import { getLogger } from '../logger.js';
45
- // `withLock` already calls `writeJsonFileAtomic` under the hood — we
46
- // piggy-back on it for atomic writes + numbered backups.
47
- import { withLock } from '../store/file-utils.js';
48
+ import { deleteLlmAccount, listLlmAccountRowsSync, llmAccountToCredentialSync, upsertLlmAccount, } from '../store/llm-accounts-accessor.js';
48
49
  import { clearAnthropicKeyCache } from './credentials.js';
49
50
  const logger = getLogger('llm-credentials-store');
50
51
  // ---------------------------------------------------------------------------
51
52
  // Paths + chmod
52
53
  // ---------------------------------------------------------------------------
53
54
  /**
54
- * Absolute path of the credential store file.
55
+ * Absolute path of the LEGACY credential store file (migrated into the
56
+ * `accounts` table by {@link migrateLegacyCredentialsFile}, T13715).
55
57
  *
56
58
  * Resolved through `getCleoHome()` so XDG overrides apply uniformly with
57
59
  * `credentials.ts`'s global-config tier.
@@ -61,53 +63,6 @@ const logger = getLogger('llm-credentials-store');
61
63
  export function credentialsStorePath() {
62
64
  return join(getCleoHome(), 'llm-credentials.json');
63
65
  }
64
- /**
65
- * Ensure the store file exists with a parseable empty-store body.
66
- *
67
- * `withLock` (file-utils) writes a zero-byte file when none exists so
68
- * `proper-lockfile` can hold the lock. The subsequent `readJsonFile` then
69
- * throws SyntaxError on the empty content. To avoid that, we pre-seed the
70
- * file with `{"version":1,"defaultStrategy":"priorityWithFallback",
71
- * "credentials":[]}` BEFORE acquiring the lock. The pre-seed is itself
72
- * race-tolerant: `writeFileSync({flag:'wx'})` makes creation idempotent
73
- * across concurrent callers — only the first one writes; the rest get
74
- * EEXIST and continue.
75
- *
76
- * @task T9257
77
- */
78
- function ensureFileInitialized(path) {
79
- if (existsSync(path))
80
- return;
81
- const dir = dirname(path);
82
- if (!existsSync(dir)) {
83
- // 0o700 so other UIDs cannot enumerate the .backups/ subdir contents
84
- // even though every individual file in it is 0o600.
85
- mkdirSync(dir, { recursive: true, mode: 0o700 });
86
- }
87
- else {
88
- // Existing dir created at a looser mode (e.g. 0o755 from an earlier
89
- // CLEO release): tighten on first write. Best-effort; ignore failures
90
- // (Windows, mount-points, NFS without chmod support).
91
- try {
92
- chmodSync(dir, 0o700);
93
- }
94
- catch {
95
- /* non-fatal */
96
- }
97
- }
98
- try {
99
- writeFileSync(path, `${JSON.stringify(emptyStore(), null, 2)}\n`, {
100
- encoding: 'utf-8',
101
- flag: 'wx',
102
- mode: 0o600,
103
- });
104
- }
105
- catch (err) {
106
- // EEXIST → another caller already initialized it; OK.
107
- if (err.code !== 'EEXIST')
108
- throw err;
109
- }
110
- }
111
66
  /**
112
67
  * S-02 (CWE-367 TOCTOU rename→chmod): the previous `enforce0600(path)`
113
68
  * helper used to chmod the live file AFTER `withLock` released its
@@ -161,21 +116,22 @@ function emptyStore() {
161
116
  return { version: 1, defaultStrategy: 'priorityWithFallback', credentials: [] };
162
117
  }
163
118
  /**
164
- * Synchronous file read + parse. Never throws.
119
+ * Synchronous read + parse of the LEGACY file. Never throws.
165
120
  *
166
- * Returns an empty store when the file is missing, unreadable, malformed,
167
- * or has the wrong `version`.
121
+ * Returns an empty store when the file is missing or blank, and `null` when
122
+ * it is unreadable, malformed, or has the wrong `version` — such a file is
123
+ * never migrated away.
168
124
  *
169
125
  * @task T9257
170
126
  */
171
- function readStoreSync() {
127
+ function readLegacyFileSync() {
172
128
  const path = credentialsStorePath();
173
129
  let raw;
174
130
  try {
175
131
  raw = readFileSync(path, 'utf-8');
176
132
  }
177
- catch {
178
- return emptyStore();
133
+ catch (err) {
134
+ return err.code === 'ENOENT' ? emptyStore() : null;
179
135
  }
180
136
  checkPermsOnRead(path);
181
137
  if (!raw.trim())
@@ -183,7 +139,7 @@ function readStoreSync() {
183
139
  try {
184
140
  const parsed = JSON.parse(raw);
185
141
  if (parsed.version !== 1 || !Array.isArray(parsed.credentials)) {
186
- return emptyStore();
142
+ return null;
187
143
  }
188
144
  return {
189
145
  version: 1,
@@ -192,8 +148,96 @@ function readStoreSync() {
192
148
  };
193
149
  }
194
150
  catch {
195
- return emptyStore();
151
+ return null;
152
+ }
153
+ }
154
+ /** The `(provider, label)` key of a credential. */
155
+ function keyOf(c) {
156
+ return `${c.provider}\u0000${c.label}`;
157
+ }
158
+ /**
159
+ * The pool's credentials in the `accounts` table, decrypted. Never throws: a
160
+ * row this machine's key cannot decrypt (a restored or copied store) is left
161
+ * out and logged by `(provider, label)` only.
162
+ *
163
+ * @task T13715
164
+ */
165
+ function readAccountsSync() {
166
+ let rows;
167
+ try {
168
+ rows = listLlmAccountRowsSync();
169
+ }
170
+ catch (err) {
171
+ logger.warn({ err: err.message }, 'llm-credentials-store: accounts unreadable');
172
+ return [];
173
+ }
174
+ const out = [];
175
+ for (const row of rows) {
176
+ try {
177
+ const credential = llmAccountToCredentialSync(row);
178
+ if (credential !== null)
179
+ out.push(credential);
180
+ }
181
+ catch {
182
+ logger.warn({ provider: row.provider, label: row.label }, "llm-credentials-store: this machine's key cannot decrypt the credential; re-add it");
183
+ }
184
+ }
185
+ return out;
186
+ }
187
+ /**
188
+ * The pool: every `accounts` credential, plus legacy-file entries `accounts`
189
+ * does not hold yet (an unmigrated file). Never throws.
190
+ *
191
+ * @task T13715
192
+ */
193
+ function readStoreSync() {
194
+ const accounts = readAccountsSync();
195
+ const legacy = readLegacyFileSync() ?? emptyStore();
196
+ const have = new Set(accounts.map(keyOf));
197
+ return {
198
+ version: 1,
199
+ defaultStrategy: legacy.defaultStrategy,
200
+ credentials: [...accounts, ...legacy.credentials.filter((c) => !have.has(keyOf(c)))],
201
+ };
202
+ }
203
+ /**
204
+ * Move the LEGACY `llm-credentials.json` entries into the `accounts` table,
205
+ * once. Idempotent: an entry `accounts` already holds is kept as it is, and
206
+ * a missing file is a no-op. The plaintext file is removed only after every
207
+ * entry reads back from `accounts`; a malformed file is left in place.
208
+ *
209
+ * @returns How many entries were moved.
210
+ * @task T13715
211
+ */
212
+ export async function migrateLegacyCredentialsFile() {
213
+ const path = credentialsStorePath();
214
+ if (!existsSync(path))
215
+ return 0;
216
+ const legacy = readLegacyFileSync();
217
+ if (legacy === null) {
218
+ logger.warn({ path }, 'llm-credentials-store: legacy file unreadable; left in place');
219
+ return 0;
220
+ }
221
+ const have = new Set(readAccountsSync().map(keyOf));
222
+ const missing = legacy.credentials.filter((c) => !have.has(keyOf(c)));
223
+ for (const credential of missing)
224
+ await upsertLlmAccount(credential);
225
+ const after = new Set(readAccountsSync().map(keyOf));
226
+ if (!legacy.credentials.every((c) => after.has(keyOf(c)))) {
227
+ logger.warn({ path }, 'llm-credentials-store: migration incomplete; legacy file kept');
228
+ return missing.length;
229
+ }
230
+ try {
231
+ unlinkSync(path);
232
+ }
233
+ catch (err) {
234
+ // Another process finished the same migration first.
235
+ if (err.code !== 'ENOENT')
236
+ throw err;
196
237
  }
238
+ if (missing.length > 0)
239
+ clearAnthropicKeyCache();
240
+ return missing.length;
197
241
  }
198
242
  /**
199
243
  * Type-narrow check used during read to drop malformed entries silently.
@@ -316,6 +360,18 @@ export function pickCredentialForProviderSync(provider, opts = {}) {
316
360
  // ---------------------------------------------------------------------------
317
361
  // Public async API
318
362
  // ---------------------------------------------------------------------------
363
+ /**
364
+ * {@link migrateLegacyCredentialsFile} for read paths: a failed migration is
365
+ * logged, never thrown, because the reader still serves the file's entries.
366
+ */
367
+ async function migrateForRead() {
368
+ try {
369
+ await migrateLegacyCredentialsFile();
370
+ }
371
+ catch (err) {
372
+ logger.warn({ err: err.message }, 'llm-credentials-store: migration deferred');
373
+ }
374
+ }
319
375
  /**
320
376
  * List credentials in the pool, optionally filtered to a single provider.
321
377
  *
@@ -324,6 +380,7 @@ export function pickCredentialForProviderSync(provider, opts = {}) {
324
380
  * @task T9257
325
381
  */
326
382
  export async function listCredentials(provider) {
383
+ await migrateForRead();
327
384
  const data = readStoreSync();
328
385
  if (!provider)
329
386
  return [...data.credentials];
@@ -336,6 +393,7 @@ export async function listCredentials(provider) {
336
393
  * @task T9257
337
394
  */
338
395
  export async function getCredentialByLabel(provider, label) {
396
+ await migrateForRead();
339
397
  const data = readStoreSync();
340
398
  return data.credentials.find((c) => c.provider === provider && c.label === label) ?? null;
341
399
  }
@@ -344,92 +402,52 @@ export async function getCredentialByLabel(provider, label) {
344
402
  *
345
403
  * - When `input.priority` is omitted, the new entry receives
346
404
  * `max(existing priorities) + 10` so it ranks lowest by default.
347
- * - Acquires the file lock via `withLock`; safe under concurrent writers.
348
- * - chmod 0600 is enforced on the written file.
405
+ * - Stored in the `accounts` table with its secrets encrypted under the
406
+ * machine key (T13715).
349
407
  *
350
- * Returns the inserted (or replaced) entry as it now lives in the file.
408
+ * Returns the inserted (or replaced) entry as it now lives in the pool.
351
409
  *
352
410
  * @task T9257
411
+ * @task T13715
353
412
  */
354
413
  export async function addCredential(input) {
355
- const path = credentialsStorePath();
356
- ensureFileInitialized(path);
357
- let inserted = null;
358
- await withLock(path, (current) => {
359
- const data = current ?? emptyStore();
360
- if (data.version !== 1) {
361
- // Fresh / corrupt — rebuild rather than silently propagate bad version.
362
- data.version = 1;
363
- data.credentials = [];
364
- }
365
- data.defaultStrategy ??= 'priorityWithFallback';
366
- const remaining = data.credentials.filter((c) => !(c.provider === input.provider && c.label === input.label));
367
- const maxPriority = remaining.reduce((m, c) => (c.priority > m ? c.priority : m), -10);
368
- const priority = typeof input.priority === 'number' ? input.priority : maxPriority + 10;
369
- const next = {
370
- provider: input.provider,
371
- label: input.label,
372
- authType: input.authType,
373
- accessToken: input.accessToken,
374
- expiresAt: input.expiresAt ?? null,
375
- priority,
376
- source: input.source,
377
- baseUrl: input.baseUrl ?? null,
378
- extraHeaders: input.extraHeaders,
379
- metadata: input.metadata,
380
- lastStatus: input.lastStatus,
381
- lastErrorCode: input.lastErrorCode,
382
- lastErrorResetAt: input.lastErrorResetAt,
383
- requestCount: input.requestCount,
384
- disabled: input.disabled ?? false,
385
- ...(input.refreshToken !== undefined && { refreshToken: input.refreshToken }),
386
- };
387
- data.credentials = [...remaining, next];
388
- inserted = next;
389
- return data;
390
- },
391
- // SECURITY (S-01/S-02): mode is plumbed all the way through to the
392
- // temp file + rotated backup writes so the file never touches disk
393
- // at a looser permission, even momentarily, and historical copies
394
- // under .backups/ are equally locked down.
395
- { mode: 0o600 });
396
- // `inserted` is set inside the transform closure before withLock returns.
397
- if (!inserted) {
398
- throw new Error('credentials-store: invariant violation — insert was not recorded');
399
- }
414
+ await migrateLegacyCredentialsFile();
415
+ const remaining = readStoreSync().credentials.filter((c) => !(c.provider === input.provider && c.label === input.label));
416
+ const maxPriority = remaining.reduce((m, c) => (c.priority > m ? c.priority : m), -10);
417
+ const priority = typeof input.priority === 'number' ? input.priority : maxPriority + 10;
418
+ const next = {
419
+ provider: input.provider,
420
+ label: input.label,
421
+ authType: input.authType,
422
+ accessToken: input.accessToken,
423
+ expiresAt: input.expiresAt ?? null,
424
+ priority,
425
+ source: input.source,
426
+ baseUrl: input.baseUrl ?? null,
427
+ extraHeaders: input.extraHeaders,
428
+ metadata: input.metadata,
429
+ lastStatus: input.lastStatus,
430
+ lastErrorCode: input.lastErrorCode,
431
+ lastErrorResetAt: input.lastErrorResetAt,
432
+ requestCount: input.requestCount,
433
+ disabled: input.disabled ?? false,
434
+ ...(input.refreshToken !== undefined && { refreshToken: input.refreshToken }),
435
+ };
436
+ await upsertLlmAccount(next);
400
437
  clearAnthropicKeyCache();
401
- return inserted;
438
+ return next;
402
439
  }
403
440
  /**
404
441
  * Remove a credential by `(provider, label)`.
405
442
  *
406
443
  * Returns `true` when a matching entry was found and removed, `false`
407
- * otherwise. Does NOT create the file when it does not yet exist.
444
+ * otherwise. A legacy-file entry is migrated first, so it cannot reappear.
408
445
  *
409
446
  * @task T9257
410
447
  */
411
448
  export async function removeCredential(provider, label) {
412
- const path = credentialsStorePath();
413
- // Quick-out: avoid acquiring a lock + creating the file when nothing to do.
414
- const snapshot = readStoreSync();
415
- if (!snapshot.credentials.some((c) => c.provider === provider && c.label === label)) {
416
- return false;
417
- }
418
- ensureFileInitialized(path);
419
- let removed = false;
420
- await withLock(path, (current) => {
421
- const data = current ?? emptyStore();
422
- if (data.version !== 1 || !Array.isArray(data.credentials)) {
423
- return emptyStore();
424
- }
425
- const before = data.credentials.length;
426
- data.credentials = data.credentials.filter((c) => !(c.provider === provider && c.label === label));
427
- removed = data.credentials.length < before;
428
- data.defaultStrategy ??= 'priorityWithFallback';
429
- return data;
430
- },
431
- // SECURITY (S-01/S-02): see addCredential for rationale.
432
- { mode: 0o600 });
449
+ await migrateLegacyCredentialsFile();
450
+ const removed = await deleteLlmAccount(provider, label);
433
451
  // S-10: invalidate the Anthropic-key cache so a removed entry stops
434
452
  // being served by the resolver in the same process.
435
453
  if (removed)
@@ -445,6 +463,7 @@ export async function removeCredential(provider, label) {
445
463
  * @task T9257
446
464
  */
447
465
  export async function pickCredentialForProvider(provider, opts = {}) {
466
+ await migrateForRead();
448
467
  return pickCredentialForProviderSync(provider, opts);
449
468
  }
450
469
  //# sourceMappingURL=credentials-store.js.map