@codyswann/lisa 2.316.2 → 2.317.1

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 (212) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +139 -9
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/skills/lisa-atlassian-access/SKILL.md +17 -0
  8. package/plugins/lisa/.codex-plugin/skills/lisa-automation-status/SKILL.md +23 -0
  9. package/plugins/lisa/.codex-plugin/skills/lisa-doctor/SKILL.md +33 -0
  10. package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +17 -0
  11. package/plugins/lisa/.codex-plugin/skills/lisa-notion-access/SKILL.md +17 -0
  12. package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/SKILL.md +106 -0
  13. package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/agents/openai.yaml +4 -0
  14. package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  15. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +137 -51
  16. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  17. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  18. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  19. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  20. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  21. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  22. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  23. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  24. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  25. package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/SKILL.md +69 -0
  26. package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  27. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +150 -0
  28. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/agents/openai.yaml +4 -0
  29. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  30. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  31. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  32. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  33. package/plugins/lisa/commands/implement.md +5 -3
  34. package/plugins/lisa/commands/setup/remote-env.md +7 -0
  35. package/plugins/lisa/skills/lisa-atlassian-access/SKILL.md +17 -0
  36. package/plugins/lisa/skills/lisa-automation-status/SKILL.md +23 -0
  37. package/plugins/lisa/skills/lisa-doctor/SKILL.md +33 -0
  38. package/plugins/lisa/skills/lisa-implement/SKILL.md +17 -0
  39. package/plugins/lisa/skills/lisa-notion-access/SKILL.md +17 -0
  40. package/plugins/lisa/skills/lisa-remote-dispatch/SKILL.md +106 -0
  41. package/plugins/lisa/skills/lisa-remote-dispatch/agents/openai.yaml +4 -0
  42. package/plugins/lisa/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  43. package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +138 -52
  44. package/plugins/lisa/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  45. package/plugins/lisa/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  46. package/plugins/lisa/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  47. package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  48. package/plugins/lisa/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  49. package/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  50. package/plugins/lisa/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  51. package/plugins/lisa/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  52. package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  53. package/plugins/lisa/skills/lisa-setup-automations/SKILL.md +69 -0
  54. package/plugins/lisa/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  55. package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +150 -0
  56. package/plugins/lisa/skills/lisa-setup-remote-env/agents/openai.yaml +4 -0
  57. package/plugins/lisa/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  58. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  59. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  60. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  61. package/plugins/lisa-agy/commands/lisa/implement.md +5 -3
  62. package/plugins/lisa-agy/commands/lisa/setup/remote-env.md +7 -0
  63. package/plugins/lisa-agy/plugin.json +1 -1
  64. package/plugins/lisa-agy/skills/lisa-atlassian-access/SKILL.md +17 -0
  65. package/plugins/lisa-agy/skills/lisa-automation-status/SKILL.md +23 -0
  66. package/plugins/lisa-agy/skills/lisa-doctor/SKILL.md +33 -0
  67. package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +17 -0
  68. package/plugins/lisa-agy/skills/lisa-notion-access/SKILL.md +17 -0
  69. package/plugins/lisa-agy/skills/lisa-remote-dispatch/SKILL.md +106 -0
  70. package/plugins/lisa-agy/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  71. package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +138 -52
  72. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  73. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  74. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  75. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  76. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  77. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  78. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  79. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  80. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  81. package/plugins/lisa-agy/skills/lisa-setup-automations/SKILL.md +69 -0
  82. package/plugins/lisa-agy/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  83. package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +150 -0
  84. package/plugins/lisa-agy/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  85. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  86. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  87. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  88. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  90. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  91. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-copilot/commands/lisa/implement.md +5 -3
  95. package/plugins/lisa-copilot/commands/lisa/setup/remote-env.md +7 -0
  96. package/plugins/lisa-copilot/skills/lisa-atlassian-access/SKILL.md +17 -0
  97. package/plugins/lisa-copilot/skills/lisa-automation-status/SKILL.md +23 -0
  98. package/plugins/lisa-copilot/skills/lisa-doctor/SKILL.md +33 -0
  99. package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +17 -0
  100. package/plugins/lisa-copilot/skills/lisa-notion-access/SKILL.md +17 -0
  101. package/plugins/lisa-copilot/skills/lisa-remote-dispatch/SKILL.md +106 -0
  102. package/plugins/lisa-copilot/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  103. package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +138 -52
  104. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  105. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  106. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  107. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  108. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  109. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  110. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  111. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  112. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  113. package/plugins/lisa-copilot/skills/lisa-setup-automations/SKILL.md +69 -0
  114. package/plugins/lisa-copilot/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  115. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +150 -0
  116. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  117. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  118. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  119. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  120. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  121. package/plugins/lisa-cursor/commands/lisa/implement.md +5 -3
  122. package/plugins/lisa-cursor/commands/lisa/setup/remote-env.md +7 -0
  123. package/plugins/lisa-cursor/skills/lisa-atlassian-access/SKILL.md +17 -0
  124. package/plugins/lisa-cursor/skills/lisa-automation-status/SKILL.md +23 -0
  125. package/plugins/lisa-cursor/skills/lisa-doctor/SKILL.md +33 -0
  126. package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +17 -0
  127. package/plugins/lisa-cursor/skills/lisa-notion-access/SKILL.md +17 -0
  128. package/plugins/lisa-cursor/skills/lisa-remote-dispatch/SKILL.md +106 -0
  129. package/plugins/lisa-cursor/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  130. package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +138 -52
  131. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  132. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  133. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  134. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  135. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  136. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  137. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  138. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  139. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  140. package/plugins/lisa-cursor/skills/lisa-setup-automations/SKILL.md +69 -0
  141. package/plugins/lisa-cursor/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  142. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +150 -0
  143. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  144. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  145. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  146. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  147. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  148. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  149. package/plugins/lisa-expo-agy/plugin.json +1 -1
  150. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  151. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  152. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  153. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  154. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  155. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  156. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  157. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  158. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  159. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  160. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  161. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  162. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  163. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  164. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  165. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  166. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  167. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  168. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  169. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  170. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  171. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  172. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  173. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  174. package/plugins/lisa-rails-agy/plugin.json +1 -1
  175. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  176. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  177. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  178. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  179. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  180. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  181. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  182. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  183. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  184. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  185. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  186. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  187. package/plugins/src/base/commands/implement.md +5 -3
  188. package/plugins/src/base/commands/setup/remote-env.md +7 -0
  189. package/plugins/src/base/skills/lisa-atlassian-access/SKILL.md +17 -0
  190. package/plugins/src/base/skills/lisa-automation-status/SKILL.md +23 -0
  191. package/plugins/src/base/skills/lisa-doctor/SKILL.md +33 -0
  192. package/plugins/src/base/skills/lisa-implement/SKILL.md +17 -0
  193. package/plugins/src/base/skills/lisa-notion-access/SKILL.md +17 -0
  194. package/plugins/src/base/skills/lisa-remote-dispatch/SKILL.md +106 -0
  195. package/plugins/src/base/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  196. package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +138 -52
  197. package/plugins/src/base/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  198. package/plugins/src/base/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  199. package/plugins/src/base/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  200. package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  201. package/plugins/src/base/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  202. package/plugins/src/base/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  203. package/plugins/src/base/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  204. package/plugins/src/base/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  205. package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  206. package/plugins/src/base/skills/lisa-setup-automations/SKILL.md +69 -0
  207. package/plugins/src/base/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  208. package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +150 -0
  209. package/plugins/src/base/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  210. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  211. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  212. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
@@ -1,15 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Resolve a secret through the configured provider.
3
+ * Resolve a secret through the configured provider and surface.
4
4
  *
5
5
  * The executable half of `lisa-secrets-access`. It exists as a CLI rather than
6
6
  * a library because the callers are polyglot — Python scripts, shell, and
7
7
  * TypeScript all need the same answer, and a subprocess is the only interface
8
8
  * all three share without duplicating the logic three times.
9
9
  *
10
- * Resolution order is environment, then provider. The environment comes first
11
- * so a CI run — where secrets are injected by the pipeline — never reaches for
12
- * a provider or a local store at all.
10
+ * The resolution ladder is one rule with one order:
11
+ *
12
+ * environment -> materialized file (surfaces that have one) -> provider
13
+ *
14
+ * Environment comes first so a CI run, where the pipeline injects secrets,
15
+ * never reaches for a provider or a local store at all. The middle rung exists
16
+ * only on surfaces whose bootstrap runs before the consuming process does.
13
17
  *
14
18
  * Values are written to stdout with no trailing newline and nothing else, so
15
19
  * `$(resolve-secret.mjs get NAME)` is safe. Diagnostics go to stderr.
@@ -19,160 +23,65 @@
19
23
  * resolve-secret.mjs list
20
24
  * resolve-secret.mjs describe NAME
21
25
  * resolve-secret.mjs verify
26
+ * resolve-secret.mjs surface
27
+ * @module resolve-secret
22
28
  */
23
29
 
24
- import { execFileSync } from "node:child_process";
25
30
  import { existsSync, readFileSync } from "node:fs";
26
- import { join } from "node:path";
27
31
 
28
- /** Config defaults when `.lisa.config.json` carries no `secrets` block. */
29
- const DEFAULTS = {
30
- provider: "env",
31
- bootstrap: { sources: ["env"], key: null },
32
- require: null,
33
- };
32
+ import { parseEnv } from "./envfile.mjs";
33
+ import { ENV_KEY, fetchAll } from "./providers.mjs";
34
+ import { materializedPaths, readConfig } from "./surfaces.mjs";
34
35
 
35
- /**
36
- * Read the `secrets` block from `.lisa.config.json`.
37
- * @param {string} cwd Directory to look in.
38
- * @returns {{provider: string, bootstrap: {sources: string[], key: string|null}, require: string[]|null}}
39
- */
40
- export function readConfig(cwd = process.cwd()) {
41
- const path = join(cwd, ".lisa.config.json");
42
- if (!existsSync(path)) return DEFAULTS;
43
- try {
44
- const cfg = JSON.parse(readFileSync(path, "utf8")).secrets;
45
- if (!cfg) return DEFAULTS;
46
- return {
47
- provider: cfg.provider ?? DEFAULTS.provider,
48
- bootstrap: { ...DEFAULTS.bootstrap, ...(cfg.bootstrap ?? {}) },
49
- require: cfg.require ?? null,
50
- };
51
- } catch (err) {
52
- throw new Error(`.lisa.config.json is not readable: ${err.message}`);
53
- }
54
- }
36
+ export { readConfig };
55
37
 
56
38
  /**
57
- * Obtain the one credential that unlocks the provider.
39
+ * Read the materialized values file, if this surface has one.
58
40
  *
59
- * Walks `sources` in order. This is the only credential permitted in an OS
60
- * keychain it is a bootstrap, not a cached copy of anything.
61
- * @param {{sources: string[], key: string|null}} bootstrap Bootstrap config.
62
- * @returns {string|null} The token, or null when the provider needs none.
41
+ * A surface without the capability returns empty rather than throwing: the
42
+ * absence of a middle rung is normal, not an error.
43
+ * @param {object} cfg Resolved configuration.
44
+ * @returns {Map<string, string>} Values by exact name.
63
45
  */
64
- function bootstrapToken(bootstrap) {
65
- if (!bootstrap.key) return null;
66
- for (const source of bootstrap.sources) {
67
- if (source === "env") {
68
- const v = (process.env[bootstrap.key] ?? "").trim();
69
- if (v) return v;
70
- }
71
- if (source === "keychain" && process.platform === "darwin") {
72
- try {
73
- const v = execFileSync(
74
- "security",
75
- [
76
- "find-generic-password",
77
- "-s",
78
- bootstrap.key,
79
- "-a",
80
- process.env.USER ?? "",
81
- "-w",
82
- ],
83
- { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
84
- ).trim();
85
- if (v) return v;
86
- } catch {
87
- /* not present in the keychain; try the next source */
88
- }
89
- }
90
- }
91
- throw new Error(
92
- `${bootstrap.key} not found in: ${bootstrap.sources.join(", ")}.\n` +
93
- `It is the bootstrap credential — without it no other secret can be read.`
94
- );
46
+ export function readMaterialized(cfg) {
47
+ if (!cfg.capabilities.materialized) return new Map();
48
+ const { valuesFile } = materializedPaths(cfg.namespace);
49
+ if (!existsSync(valuesFile)) return new Map();
50
+ return parseEnv(readFileSync(valuesFile, "utf8"));
95
51
  }
96
52
 
97
53
  /**
98
- * Fetch every secret the provider grants, keyed by name.
54
+ * Assert a name is declared when the project narrows itself with `require`.
99
55
  *
100
- * Deliberately fetches all rather than one: the provider already scopes what
101
- * this caller may see, so the set it returns *is* the permitted set, and
102
- * re-stating that as a list in config would duplicate a boundary the provider
103
- * already enforces.
104
- * @param {ReturnType<typeof readConfig>} cfg Resolved config.
105
- * @returns {Map<string, {value: string, note: string}>} Secrets by name.
56
+ * A `require` list is an assertion, not merely a filter: naming a secret
57
+ * declares the project needs it, so asking for one outside the list is a
58
+ * configuration error rather than a lookup miss.
59
+ * @param {string} name Requested name.
60
+ * @param {object} cfg Resolved configuration.
106
61
  */
107
- function fetchAll(cfg) {
108
- const out = new Map();
109
- const token = bootstrapToken(cfg.bootstrap);
110
- const env = { ...process.env };
111
- if (cfg.bootstrap.key && token) env[cfg.bootstrap.key] = token;
112
-
113
- switch (cfg.provider) {
114
- case "env":
115
- for (const [k, v] of Object.entries(process.env)) {
116
- if (/^[A-Z][A-Z0-9_]*$/.test(k))
117
- out.set(k, { value: v ?? "", note: "" });
118
- }
119
- return out;
120
-
121
- case "bitwarden": {
122
- const raw = execFileSync("bws", ["secret", "list", "--output", "json"], {
123
- encoding: "utf8",
124
- env,
125
- stdio: ["ignore", "pipe", "pipe"],
126
- });
127
- for (const s of JSON.parse(raw || "[]")) {
128
- out.set(s.key, { value: s.value, note: s.note ?? "" });
129
- }
130
- return out;
131
- }
132
-
133
- case "doppler": {
134
- const raw = execFileSync(
135
- "doppler",
136
- ["secrets", "download", "--no-file", "--format", "json"],
137
- {
138
- encoding: "utf8",
139
- env,
140
- stdio: ["ignore", "pipe", "pipe"],
141
- }
142
- );
143
- for (const [k, v] of Object.entries(JSON.parse(raw || "{}"))) {
144
- out.set(k, { value: String(v), note: "" });
145
- }
146
- return out;
147
- }
148
-
149
- default:
150
- throw new Error(
151
- `provider "${cfg.provider}" has no bulk read implemented yet.\n` +
152
- `Add one in resolve-secret.mjs — see the dispatch table in SKILL.md.`
153
- );
62
+ function assertDeclared(name, cfg) {
63
+ if (cfg.require && !cfg.require.includes(name)) {
64
+ throw new Error(
65
+ `${name} is not declared in secrets.require.\n` +
66
+ `Declared: ${cfg.require.join(", ") || "(none)"}`
67
+ );
154
68
  }
155
69
  }
156
70
 
157
71
  /**
158
- * Resolve one secret: environment first, then the provider.
72
+ * Resolve one secret down the ladder.
159
73
  * @param {string} name Environment-variable-style key.
160
- * @param {ReturnType<typeof readConfig>} [cfg] Resolved config.
74
+ * @param {object} [cfg] Resolved configuration.
161
75
  * @returns {string} The secret value.
162
76
  */
163
77
  export function get(name, cfg = readConfig()) {
164
78
  const fromEnv = (process.env[name] ?? "").trim();
165
79
  if (fromEnv) return fromEnv;
166
80
 
167
- // A `require` list is an assertion, not just a filter: naming a secret
168
- // declares that this project needs it, so asking for one outside the list is
169
- // a configuration error rather than a lookup miss.
170
- if (cfg.require && !cfg.require.includes(name)) {
171
- throw new Error(
172
- `${name} is not declared in secrets.require.\n` +
173
- `Declared: ${cfg.require.join(", ") || "(none)"}`
174
- );
175
- }
81
+ assertDeclared(name, cfg);
82
+
83
+ const fromFile = readMaterialized(cfg).get(name);
84
+ if (fromFile) return fromFile;
176
85
 
177
86
  const all = fetchAll(cfg);
178
87
  const hit = all.get(name);
@@ -186,10 +95,108 @@ export function get(name, cfg = readConfig()) {
186
95
  return hit.value;
187
96
  }
188
97
 
98
+ /**
99
+ * Report which rung of the ladder answers for a name, without revealing values.
100
+ * @param {string} name Requested name.
101
+ * @param {Map<string, object>} provider Provider view.
102
+ * @param {Map<string, string>} file Materialized view.
103
+ * @returns {string} A fixed-width source label.
104
+ */
105
+ function sourceOf(name, provider, file) {
106
+ if ((process.env[name] ?? "").trim()) return "env ";
107
+ if (file.get(name)) return "file ";
108
+ if (provider.get(name)?.value) return "provider ";
109
+ return " ";
110
+ }
111
+
112
+ /**
113
+ * Verify every declared secret, mirroring the ladder's own order.
114
+ *
115
+ * Checking only the provider would report MISSING for every secret in CI, where
116
+ * the pipeline injects them as environment variables — a false negative exactly
117
+ * where verification matters most.
118
+ *
119
+ * This command is read-only. Proving that a rotating credential can actually be
120
+ * written back requires a write, so that check lives in the rotation path
121
+ * rather than here.
122
+ * @param {object} cfg Resolved configuration.
123
+ * @returns {number} Count of secrets that failed.
124
+ */
125
+ function verify(cfg) {
126
+ const provider = fetchAll(cfg);
127
+ const file = readMaterialized(cfg);
128
+ const names = cfg.require ?? [
129
+ ...new Set([...provider.keys(), ...file.keys()]),
130
+ ];
131
+ const rotating = new Set(cfg.rotating ?? []);
132
+ let bad = 0;
133
+
134
+ console.log(`surface: ${cfg.surface} provider: ${cfg.provider}`);
135
+ for (const name of names) {
136
+ const resolves = Boolean(
137
+ (process.env[name] ?? "").trim() ||
138
+ file.get(name) ||
139
+ provider.get(name)?.value
140
+ );
141
+ const named = ENV_KEY.test(name);
142
+ const noted = Boolean(provider.get(name)?.note);
143
+ if (!resolves || !named || !noted) bad += 1;
144
+ console.log(
145
+ ` ${name.padEnd(30)} ${resolves ? "resolves" : "MISSING "} ` +
146
+ `${sourceOf(name, provider, file)} ` +
147
+ `${named ? "name ok" : "NAME NOT UPPER_SNAKE"} ` +
148
+ `${noted ? "noted" : "NO NOTE"}` +
149
+ `${rotating.has(name) ? " rotating" : ""}`
150
+ );
151
+ }
152
+
153
+ for (const name of rotating) {
154
+ if (!cfg.bootstrap.key) {
155
+ console.log(
156
+ ` ${name.padEnd(30)} ROTATING BUT NO BOOTSTRAP — the replacement could ` +
157
+ `not be written back, which strands the credential on first use`
158
+ );
159
+ bad += 1;
160
+ }
161
+ }
162
+ return bad;
163
+ }
164
+
165
+ /**
166
+ * Describe a secret's usage note without ever printing its value.
167
+ * @param {string} name Requested name.
168
+ * @param {object} cfg Resolved configuration.
169
+ */
170
+ function describe(name, cfg) {
171
+ const hit = fetchAll(cfg).get(name);
172
+ if (hit) {
173
+ // Infer *and* warn, never instead of. A silent fallback that works well
174
+ // enough is what guarantees the notes stay empty forever.
175
+ console.log(
176
+ hit.note ||
177
+ `(no note — purpose inferred from the name: ${name}. ` +
178
+ `An inferred mapping must never authorise a write.)`
179
+ );
180
+ return;
181
+ }
182
+ // Environment-only is a legitimate state, not an error: CI injects secrets
183
+ // that never appear in a provider listing. Say so rather than claiming the
184
+ // secret does not exist, and never print the value.
185
+ if ((process.env[name] ?? "").trim()) {
186
+ console.log("(set in the environment; no provider entry, so no note)");
187
+ return;
188
+ }
189
+ throw new Error(`${name} not found in the environment or the provider`);
190
+ }
191
+
189
192
  function main() {
190
193
  const [op, name] = process.argv.slice(2);
191
194
  const cfg = readConfig();
192
195
 
196
+ if (op === "surface") {
197
+ console.log(cfg.surface);
198
+ return;
199
+ }
193
200
  if (op === "get") {
194
201
  if (!name) throw new Error("usage: resolve-secret.mjs get NAME");
195
202
  process.stdout.write(get(name, cfg));
@@ -197,58 +204,26 @@ function main() {
197
204
  }
198
205
  if (op === "list") {
199
206
  // Names only. This command must never be able to leak a value.
200
- console.log([...fetchAll(cfg).keys()].sort().join("\n"));
207
+ const names = new Set([
208
+ ...fetchAll(cfg).keys(),
209
+ ...readMaterialized(cfg).keys(),
210
+ ]);
211
+ console.log([...names].sort().join("\n"));
201
212
  return;
202
213
  }
203
214
  if (op === "describe") {
204
215
  if (!name) throw new Error("usage: resolve-secret.mjs describe NAME");
205
- const hit = fetchAll(cfg).get(name);
206
- if (hit) {
207
- console.log(
208
- hit.note || `(no note — purpose inferred from the name: ${name})`
209
- );
210
- return;
211
- }
212
- // Environment-only is a legitimate state, not an error: CI injects secrets
213
- // that never appear in the provider listing. Say so rather than claiming
214
- // the secret does not exist, and never print the value.
215
- if ((process.env[name] ?? "").trim()) {
216
- console.log(
217
- `(set in the environment; no provider entry, so no note is available)`
218
- );
219
- return;
220
- }
221
- throw new Error(`${name} not found in the environment or the provider`);
216
+ describe(name, cfg);
217
+ return;
222
218
  }
223
219
  if (op === "verify") {
224
- const all = fetchAll(cfg);
225
- const names = cfg.require ?? [...all.keys()];
226
- let bad = 0;
227
- for (const n of names) {
228
- // Mirror get()'s environment-first order. Checking only the provider
229
- // reports MISSING for every secret in CI, where the pipeline injects
230
- // them as environment variables — a false negative precisely where
231
- // verification matters most.
232
- const fromEnv = Boolean((process.env[n] ?? "").trim());
233
- const entry = all.get(n);
234
- const ok = fromEnv || Boolean(entry?.value);
235
- const named = /^[A-Z][A-Z0-9_]*$/.test(n);
236
- const noted = Boolean(entry?.note);
237
- if (!ok || !named) bad += 1;
238
- const source = fromEnv
239
- ? "env "
240
- : entry?.value
241
- ? "provider"
242
- : " ";
243
- console.log(
244
- ` ${n.padEnd(30)} ${ok ? "resolves" : "MISSING "} ${source} ` +
245
- `${named ? "name ok" : "NAME NOT UPPER_SNAKE"} ${noted ? "noted" : "no note"}`
246
- );
247
- }
220
+ const bad = verify(cfg);
248
221
  if (bad) throw new Error(`${bad} secret(s) failed verification`);
249
222
  return;
250
223
  }
251
- throw new Error("usage: resolve-secret.mjs get|list|describe|verify [NAME]");
224
+ throw new Error(
225
+ "usage: resolve-secret.mjs get|list|describe|verify|surface [NAME]"
226
+ );
252
227
  }
253
228
 
254
229
  if (import.meta.url === `file://${process.argv[1]}`) {
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The single-writer path for consumable credentials.
4
+ *
5
+ * A *consumable* credential is one where using it can invalidate the stored
6
+ * copy: an OAuth refresh token the issuer replaces on every exchange, a
7
+ * short-lived session, a single-use enrollment token. The property is not
8
+ * "OAuth" — it is that a successful use makes the value on record wrong.
9
+ *
10
+ * This is a deliberate sibling of the resolver rather than a mode of it. The
11
+ * resolver never writes, and that is what makes it safe to hand to anything. A
12
+ * credential that must be written back needs an authority the read path should
13
+ * not hold, so it gets its own program and its own contract.
14
+ *
15
+ * The failure this exists to prevent is not rotation. It is **rotation with no
16
+ * proven write path**: a job exchanges the token, the issuer invalidates the
17
+ * old one, the replacement cannot be saved, and every downstream consumer is
18
+ * broken until a human notices. So the write is proven *before* the
19
+ * irreversible use, never after.
20
+ *
21
+ * Usage:
22
+ * rotate-secret.mjs preflight NAME # prove the write path, change nothing
23
+ * rotate-secret.mjs checkout NAME # preflight, take the lease, emit value
24
+ * rotate-secret.mjs commit NAME # read replacement on stdin, release
25
+ * rotate-secret.mjs release NAME # release without writing
26
+ * rotate-secret.mjs leases # show current holders
27
+ * @module rotate-secret
28
+ */
29
+
30
+ import { readFileSync } from "node:fs";
31
+ import { hostname } from "node:os";
32
+
33
+ import { LEASE_KEY, fetchAll, rawByKey, writeSecret } from "./providers.mjs";
34
+ import { readConfig } from "./surfaces.mjs";
35
+
36
+ /** How long a lease is honoured before it is treated as abandoned. */
37
+ const LEASE_TTL_MS = 30 * 60 * 1000;
38
+
39
+ /**
40
+ * Assert a name is declared rotating before any write path opens for it.
41
+ *
42
+ * Declaration is config, never inference from a note: the note lives
43
+ * provider-side, is editable outside review, and a read-only account cannot
44
+ * correct a wrong one. Config is the surface where this decision is reviewable.
45
+ * @param {string} name Requested name.
46
+ * @param {object} cfg Resolved configuration.
47
+ */
48
+ function assertRotating(name, cfg) {
49
+ if (!(cfg.rotating ?? []).includes(name)) {
50
+ throw new Error(
51
+ `${name} is not declared in secrets.rotating.\n` +
52
+ `Only a declared consumable credential may use the write path. If this ` +
53
+ `credential really is replaced on use, declare it — silently rotating an ` +
54
+ `undeclared one is how a shared token gets stranded.`
55
+ );
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Locate a secret and its provider-side identifier.
61
+ * @param {string} name Requested name.
62
+ * @param {object} cfg Resolved configuration.
63
+ * @returns {{value: string, id: string}} The current entry.
64
+ */
65
+ function entryFor(name, cfg) {
66
+ const hit = fetchAll(cfg).get(name);
67
+ if (!hit) throw new Error(`${name} is not available to this account`);
68
+ if (!hit.id) {
69
+ throw new Error(
70
+ `${name} has no provider identifier, so it cannot be written back.\n` +
71
+ `Provider "${cfg.provider}" may not support rotation.`
72
+ );
73
+ }
74
+ return { value: hit.value, id: hit.id };
75
+ }
76
+
77
+ /**
78
+ * Prove the replacement could be saved, by writing the value already stored.
79
+ *
80
+ * A no-op re-write is the only honest proof: it exercises the exact permission
81
+ * the rotation will need, against the exact record, and changes nothing. A
82
+ * check that merely confirms the CLI exists proves a different thing than the
83
+ * one that fails — which is precisely how the original incident happened.
84
+ * @param {string} name Requested name.
85
+ * @param {object} cfg Resolved configuration.
86
+ * @returns {{value: string, id: string}} The verified entry.
87
+ */
88
+ export function preflight(name, cfg) {
89
+ assertRotating(name, cfg);
90
+ const entry = entryFor(name, cfg);
91
+ try {
92
+ writeSecret(cfg, entry.id, entry.value);
93
+ } catch (err) {
94
+ throw new Error(
95
+ `no write access to ${name}: ${String(err.message).split("\n")[0]}\n` +
96
+ `Refusing to continue. Rotating a credential whose replacement cannot ` +
97
+ `be persisted breaks every consumer of it.`
98
+ );
99
+ }
100
+ return entry;
101
+ }
102
+
103
+ /**
104
+ * Read the lease record, tolerating its absence.
105
+ * @param {object} cfg Resolved configuration.
106
+ * @returns {{row: object|null, leases: Record<string, object>}} Current state.
107
+ */
108
+ function readLeases(cfg) {
109
+ const row = rawByKey(cfg, LEASE_KEY);
110
+ if (!row) return { row: null, leases: {} };
111
+ try {
112
+ const parsed = JSON.parse(row.value || "{}");
113
+ return { row, leases: typeof parsed === "object" ? parsed : {} };
114
+ } catch {
115
+ return { row, leases: {} };
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Persist the lease record.
121
+ *
122
+ * When the record does not exist the lease is skipped rather than invented.
123
+ * Creating provider entries is not this program's authority, and an advisory
124
+ * lock whose absence blocks a legitimate rotation would be worse than no lock.
125
+ * @param {object} cfg Resolved configuration.
126
+ * @param {object|null} row Existing lease row.
127
+ * @param {Record<string, object>} leases Updated lease map.
128
+ */
129
+ function writeLeases(cfg, row, leases) {
130
+ if (!row?.id) return;
131
+ writeSecret(cfg, row.id, JSON.stringify(leases));
132
+ }
133
+
134
+ /**
135
+ * Identify this holder well enough for a human to find it.
136
+ * @param {object} cfg Resolved configuration.
137
+ * @returns {string} A holder label.
138
+ */
139
+ function holderId(cfg) {
140
+ const run = process.env.GITHUB_RUN_ID;
141
+ return run ? `${cfg.surface}:run-${run}` : `${cfg.surface}:${hostname()}`;
142
+ }
143
+
144
+ /**
145
+ * Take the advisory lease, refusing while another holder's lease is live.
146
+ *
147
+ * Advisory is the honest word. True cross-surface exclusion is not achievable
148
+ * with per-surface primitives, and claiming otherwise would invite exactly the
149
+ * concurrent rotation this guards against. What this does give is a record every
150
+ * surface can see, and an expiry so a crashed holder heals itself.
151
+ * @param {string} name Requested name.
152
+ * @param {object} cfg Resolved configuration.
153
+ * @param {number} now Current epoch milliseconds.
154
+ */
155
+ function acquire(name, cfg, now) {
156
+ const { row, leases } = readLeases(cfg);
157
+ const held = leases[name];
158
+ const holder = holderId(cfg);
159
+ if (held && held.holder !== holder && Number(held.expiresAt) > now) {
160
+ throw new Error(
161
+ `${name} is leased by ${held.holder} until ` +
162
+ `${new Date(Number(held.expiresAt)).toISOString()}.\n` +
163
+ `Two refresh loops race: each receives a new value and invalidates the ` +
164
+ `other, and whichever wrote last wins while the other copy is dead.`
165
+ );
166
+ }
167
+ leases[name] = {
168
+ holder,
169
+ surface: cfg.surface,
170
+ acquiredAt: now,
171
+ expiresAt: now + LEASE_TTL_MS,
172
+ };
173
+ writeLeases(cfg, row, leases);
174
+ }
175
+
176
+ /**
177
+ * Release the lease if this holder owns it.
178
+ * @param {string} name Requested name.
179
+ * @param {object} cfg Resolved configuration.
180
+ */
181
+ function release(name, cfg) {
182
+ const { row, leases } = readLeases(cfg);
183
+ if (leases[name]?.holder === holderId(cfg)) {
184
+ delete leases[name];
185
+ writeLeases(cfg, row, leases);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Read the replacement value from stdin.
191
+ *
192
+ * Stdin rather than an argument, because process arguments are visible to
193
+ * anything that can list processes on the host.
194
+ * @returns {string} The replacement value.
195
+ */
196
+ function readStdin() {
197
+ const value = readFileSync(0, "utf8");
198
+ if (!value.trim()) {
199
+ throw new Error(
200
+ "no replacement value on stdin; refusing to store an empty secret"
201
+ );
202
+ }
203
+ return value;
204
+ }
205
+
206
+ function main() {
207
+ const [op, name] = process.argv.slice(2);
208
+ const cfg = readConfig();
209
+ const now = Date.now();
210
+
211
+ if (op === "leases") {
212
+ const { leases } = readLeases(cfg);
213
+ const rows = Object.entries(leases);
214
+ if (!rows.length) {
215
+ console.log("no rotation leases held");
216
+ return;
217
+ }
218
+ for (const [key, lease] of rows) {
219
+ const state = Number(lease.expiresAt) > now ? "held" : "expired";
220
+ console.log(
221
+ ` ${key.padEnd(30)} ${state.padEnd(8)} ${lease.holder} ` +
222
+ `until ${new Date(Number(lease.expiresAt)).toISOString()}`
223
+ );
224
+ }
225
+ return;
226
+ }
227
+
228
+ if (!name) throw new Error(`usage: rotate-secret.mjs ${op ?? "OP"} NAME`);
229
+
230
+ if (op === "preflight") {
231
+ preflight(name, cfg);
232
+ console.log(`${name}: write path proven; nothing was changed`);
233
+ return;
234
+ }
235
+ if (op === "checkout") {
236
+ const entry = preflight(name, cfg);
237
+ acquire(name, cfg, now);
238
+ process.stdout.write(entry.value);
239
+ return;
240
+ }
241
+ if (op === "commit") {
242
+ assertRotating(name, cfg);
243
+ const { id } = entryFor(name, cfg);
244
+ writeSecret(cfg, id, readStdin());
245
+ release(name, cfg);
246
+ console.log(`${name}: replacement stored and lease released`);
247
+ return;
248
+ }
249
+ if (op === "release") {
250
+ release(name, cfg);
251
+ console.log(`${name}: lease released`);
252
+ return;
253
+ }
254
+ throw new Error(
255
+ "usage: rotate-secret.mjs preflight|checkout|commit|release|leases [NAME]"
256
+ );
257
+ }
258
+
259
+ if (import.meta.url === `file://${process.argv[1]}`) {
260
+ try {
261
+ main();
262
+ } catch (err) {
263
+ console.error(err.message);
264
+ process.exit(1);
265
+ }
266
+ }