loadout-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/MASTER_PLAN.md +1711 -0
  3. package/README.md +453 -0
  4. package/SECURITY.md +18 -0
  5. package/SIMPLE_PLAN.md +44 -0
  6. package/catalog/discovered.json +10469 -0
  7. package/catalog/packages.json +1150 -0
  8. package/dashboard/app.js +607 -0
  9. package/dashboard/index.html +249 -0
  10. package/dashboard/styles.css +384 -0
  11. package/dist/src/cli.js +2735 -0
  12. package/dist/src/core/active-policy.js +233 -0
  13. package/dist/src/core/active-set.js +382 -0
  14. package/dist/src/core/adapters.js +254 -0
  15. package/dist/src/core/adopt.js +92 -0
  16. package/dist/src/core/agent-health-score.js +475 -0
  17. package/dist/src/core/agent-inspection.js +152 -0
  18. package/dist/src/core/agent-versions.js +131 -0
  19. package/dist/src/core/api.js +90 -0
  20. package/dist/src/core/atomic-file.js +22 -0
  21. package/dist/src/core/audit.js +174 -0
  22. package/dist/src/core/benchmark-campaign.js +496 -0
  23. package/dist/src/core/benchmark-evidence.js +458 -0
  24. package/dist/src/core/benchmark-fixtures.js +1157 -0
  25. package/dist/src/core/benchmark-runner.js +301 -0
  26. package/dist/src/core/benchmark-trust.js +795 -0
  27. package/dist/src/core/canary.js +77 -0
  28. package/dist/src/core/candidate-intelligence.js +661 -0
  29. package/dist/src/core/catalog-coverage.js +85 -0
  30. package/dist/src/core/catalog-install.js +186 -0
  31. package/dist/src/core/catalog-release.js +286 -0
  32. package/dist/src/core/catalog.js +327 -0
  33. package/dist/src/core/codex-mcp.js +122 -0
  34. package/dist/src/core/community.js +86 -0
  35. package/dist/src/core/compatibility-intelligence.js +465 -0
  36. package/dist/src/core/completion.js +160 -0
  37. package/dist/src/core/components.js +138 -0
  38. package/dist/src/core/conversion.js +56 -0
  39. package/dist/src/core/credentials.js +377 -0
  40. package/dist/src/core/demo.js +136 -0
  41. package/dist/src/core/diff.js +83 -0
  42. package/dist/src/core/discovery-connector.js +165 -0
  43. package/dist/src/core/doctor.js +95 -0
  44. package/dist/src/core/ecosystem-import.js +1072 -0
  45. package/dist/src/core/evaluate.js +72 -0
  46. package/dist/src/core/file-lock.js +140 -0
  47. package/dist/src/core/freshness-alerts.js +230 -0
  48. package/dist/src/core/github-discovery.js +97 -0
  49. package/dist/src/core/github.js +148 -0
  50. package/dist/src/core/head-to-head.js +437 -0
  51. package/dist/src/core/health-score-evidence.js +169 -0
  52. package/dist/src/core/health.js +129 -0
  53. package/dist/src/core/improve.js +223 -0
  54. package/dist/src/core/install.js +233 -0
  55. package/dist/src/core/intelligence-feed-build.js +59 -0
  56. package/dist/src/core/intelligence-feed.js +353 -0
  57. package/dist/src/core/loadout-badge.js +55 -0
  58. package/dist/src/core/loadout-card.js +87 -0
  59. package/dist/src/core/manifest.js +307 -0
  60. package/dist/src/core/mcp-recipes.js +419 -0
  61. package/dist/src/core/mcp-registry-discovery.js +455 -0
  62. package/dist/src/core/mcp.js +287 -0
  63. package/dist/src/core/model-config.js +102 -0
  64. package/dist/src/core/observations.js +87 -0
  65. package/dist/src/core/outcomes.js +164 -0
  66. package/dist/src/core/package.js +225 -0
  67. package/dist/src/core/paths.js +214 -0
  68. package/dist/src/core/portable.js +130 -0
  69. package/dist/src/core/private-discovery.js +45 -0
  70. package/dist/src/core/profiles.js +255 -0
  71. package/dist/src/core/provenance.js +278 -0
  72. package/dist/src/core/ranking.js +104 -0
  73. package/dist/src/core/recommend.js +181 -0
  74. package/dist/src/core/registry-api.js +97 -0
  75. package/dist/src/core/registry.js +316 -0
  76. package/dist/src/core/release-claims.js +175 -0
  77. package/dist/src/core/remove.js +122 -0
  78. package/dist/src/core/review-queue.js +200 -0
  79. package/dist/src/core/runtime-tool-recipe.js +315 -0
  80. package/dist/src/core/runtime-tools.js +453 -0
  81. package/dist/src/core/safety.js +234 -0
  82. package/dist/src/core/sandbox.js +78 -0
  83. package/dist/src/core/scheduler.js +264 -0
  84. package/dist/src/core/share-report.js +85 -0
  85. package/dist/src/core/signing.js +115 -0
  86. package/dist/src/core/skill-compare.js +239 -0
  87. package/dist/src/core/skill-inventory.js +193 -0
  88. package/dist/src/core/skill-security.js +340 -0
  89. package/dist/src/core/skills-sh-discovery.js +336 -0
  90. package/dist/src/core/skills.js +208 -0
  91. package/dist/src/core/snapshot.js +210 -0
  92. package/dist/src/core/source.js +305 -0
  93. package/dist/src/core/state.js +300 -0
  94. package/dist/src/core/sync.js +348 -0
  95. package/dist/src/core/terminal.js +53 -0
  96. package/dist/src/core/transaction.js +211 -0
  97. package/dist/src/core/update-watch.js +39 -0
  98. package/dist/src/core/update.js +216 -0
  99. package/dist/src/core/upgrade.js +207 -0
  100. package/dist/src/dashboard.js +418 -0
  101. package/dist/src/shared/schemas.js +336 -0
  102. package/dist/src/shared/types.js +1 -0
  103. package/docs/ACTIVE_SET.md +53 -0
  104. package/docs/ACTIVE_SET_POLICY.md +45 -0
  105. package/docs/CANDIDATE_INTELLIGENCE.md +139 -0
  106. package/docs/CATALOG.md +78 -0
  107. package/docs/CATALOG_POLICY.md +41 -0
  108. package/docs/COMMUNITY_DISCOVERY.md +14 -0
  109. package/docs/COMPATIBILITY_POLICY.md +22 -0
  110. package/docs/CONVERSION_AND_SANDBOX.md +27 -0
  111. package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +31 -0
  112. package/docs/DISCOVERED.md +280 -0
  113. package/docs/EVALUATION_PROTOCOL.md +40 -0
  114. package/docs/EVALUATION_PROTOCOL_V1.md +300 -0
  115. package/docs/FEATURE_TEST_MATRIX.md +802 -0
  116. package/docs/GITHUB_AUTHORIZATION.md +38 -0
  117. package/docs/HEAD_TO_HEAD_EVALUATION.md +79 -0
  118. package/docs/PROVENANCE_AND_COMPARISON.md +72 -0
  119. package/docs/PROVIDER_CONFIGURATION.md +45 -0
  120. package/docs/RELEASE_REVIEW.md +117 -0
  121. package/docs/SAFE_UPDATE_DEMO.md +25 -0
  122. package/docs/SCHEMA_DECISIONS.md +25 -0
  123. package/docs/TEAM_POLICY.md +18 -0
  124. package/docs/TESTING.md +190 -0
  125. package/package.json +82 -0
@@ -0,0 +1,138 @@
1
+ import { lstat, readdir } from "node:fs/promises";
2
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { buildSkillPlan } from "./install.js";
4
+ import { adapterCapabilities, agentComponentDirectory } from "./adapters.js";
5
+ const RESOURCE_DIRECTORIES = new Map([
6
+ ["rules", "rule"],
7
+ ["commands", "command"],
8
+ ["agents", "agent"],
9
+ ]);
10
+ const RESOURCE_FILE = /\.(?:md|json|ya?ml)$/i;
11
+ /** Discover conventional resource directories without following symlinks. */
12
+ export async function discoverResources(root) {
13
+ const base = resolve(root);
14
+ const resources = [];
15
+ async function visit(directory, depth, insideSkill) {
16
+ if (depth > 5)
17
+ return;
18
+ const entries = await readdir(directory, { withFileTypes: true });
19
+ const skill = insideSkill ||
20
+ entries.some((entry) => entry.name === "SKILL.md" && entry.isFile());
21
+ for (const entry of entries) {
22
+ if (entry.name === ".git" || entry.name === "node_modules")
23
+ continue;
24
+ const absolute = join(directory, entry.name);
25
+ if (entry.isSymbolicLink()) {
26
+ if (RESOURCE_DIRECTORIES.has(entry.name))
27
+ throw new Error(`Refusing symlinked resource directory: ${absolute}`);
28
+ continue;
29
+ }
30
+ if (!entry.isDirectory())
31
+ continue;
32
+ const type = !skill ? RESOURCE_DIRECTORIES.get(entry.name) : undefined;
33
+ if (type) {
34
+ for (const child of await readdir(absolute, { withFileTypes: true })) {
35
+ if (child.isSymbolicLink())
36
+ throw new Error(`Refusing symlinked package resource: ${join(absolute, child.name)}`);
37
+ // Conventional agent resources are files. Treating arbitrary child
38
+ // directories as agents creates dangerous false positives such as
39
+ // `skills/agents/references`, which is supporting material for a
40
+ // runtime tool rather than an installable agent declaration.
41
+ if (!child.isFile() || !RESOURCE_FILE.test(child.name))
42
+ continue;
43
+ resources.push({
44
+ type,
45
+ name: child.name.replace(/\.(md|json|ya?ml)$/i, ""),
46
+ path: relative(base, join(absolute, child.name))
47
+ .split(sep)
48
+ .join("/"),
49
+ });
50
+ }
51
+ continue;
52
+ }
53
+ await visit(absolute, depth + 1, skill);
54
+ }
55
+ }
56
+ await visit(base, 0, false);
57
+ return resources.sort((a, b) => a.type.localeCompare(b.type) || a.path.localeCompare(b.path));
58
+ }
59
+ function safeSource(root, path) {
60
+ const base = resolve(root);
61
+ const source = resolve(base, path);
62
+ if (source !== base && !source.startsWith(`${base}${sep}`))
63
+ throw new Error(`Resource escapes package root: ${path}`);
64
+ return source;
65
+ }
66
+ export async function buildUniversalPackagePlan(root, packageId, agents) {
67
+ const files = [];
68
+ const warnings = [];
69
+ try {
70
+ const skills = await buildSkillPlan(root, packageId, agents);
71
+ files.push(...skills.files.map((file) => ({
72
+ ...file,
73
+ componentType: "skill",
74
+ compatibility: "native",
75
+ })));
76
+ warnings.push(...skills.warnings);
77
+ }
78
+ catch (error) {
79
+ if (!(error instanceof Error) ||
80
+ !error.message.startsWith("No SKILL.md found"))
81
+ throw error;
82
+ }
83
+ const resources = await discoverResources(root);
84
+ for (const resource of resources) {
85
+ for (const agent of agents) {
86
+ const directory = agentComponentDirectory(agent, resource.type);
87
+ const compatibility = adapterCapabilities(agent.id).components[resource.type];
88
+ if (!directory) {
89
+ warnings.push(`${agent.displayName}: ${resource.type} '${resource.name}' is unsupported and will not be installed.`);
90
+ continue;
91
+ }
92
+ files.push({
93
+ source: safeSource(root, resource.path),
94
+ target: join(directory, packageId, resource.path.split("/").at(-1)),
95
+ targetAgent: agent.id,
96
+ componentType: resource.type,
97
+ compatibility,
98
+ });
99
+ if (compatibility === "adapted")
100
+ warnings.push(`${agent.displayName}: ${resource.type} '${resource.name}' will be installed using an adapted layout.`);
101
+ }
102
+ }
103
+ if (!files.length)
104
+ throw new Error(`No supported skills, rules, commands, or agents found under ${root}`);
105
+ return {
106
+ packageId,
107
+ files,
108
+ targetAgents: agents.map((agent) => agent.id),
109
+ warnings: [...new Set(warnings)],
110
+ };
111
+ }
112
+ export async function addRootFileExports(plan, packageRoot, targetRoot, exports) {
113
+ const sourceRoot = resolve(packageRoot);
114
+ const destinationRoot = resolve(targetRoot);
115
+ for (const item of exports) {
116
+ if (!item.source ||
117
+ !item.target ||
118
+ isAbsolute(item.source) ||
119
+ isAbsolute(item.target))
120
+ throw new Error(`Root-file exports must use non-empty relative paths: ${item.source} -> ${item.target}`);
121
+ const source = resolve(sourceRoot, item.source);
122
+ const target = resolve(destinationRoot, item.target);
123
+ if (source !== sourceRoot && !source.startsWith(`${sourceRoot}${sep}`))
124
+ throw new Error(`Root-file source escapes package: ${item.source}`);
125
+ if (target !== destinationRoot &&
126
+ !target.startsWith(`${destinationRoot}${sep}`))
127
+ throw new Error(`Root-file target escapes allowed scope: ${item.target}`);
128
+ const info = await lstat(source);
129
+ if (info.isSymbolicLink() || (!info.isFile() && !info.isDirectory()))
130
+ throw new Error(`Root-file source must be a real file or directory: ${item.source}`);
131
+ plan.files.push({
132
+ source,
133
+ target,
134
+ componentType: "root",
135
+ compatibility: "native",
136
+ });
137
+ }
138
+ }
@@ -0,0 +1,56 @@
1
+ import { basename } from "node:path";
2
+ function safeName(name) {
3
+ const value = basename(name).replace(/[^A-Za-z0-9._-]+/g, "-");
4
+ if (!value)
5
+ throw new Error("Conversion name must contain a safe identifier");
6
+ return value.toLowerCase();
7
+ }
8
+ /**
9
+ * Compile only static instruction semantics. Hooks never become executable
10
+ * hooks; their trigger and command are preserved in a review artifact.
11
+ */
12
+ export function compileConversion(input, target) {
13
+ const name = safeName(input.name);
14
+ if (!input.body.trim())
15
+ throw new Error("Conversion source body is empty");
16
+ if (input.kind === "hook" && target !== "static-review")
17
+ throw new Error("Hooks may only convert to static-review; executable behavior is never synthesized");
18
+ if (input.kind === "subagent" && target === "static-review")
19
+ throw new Error("Subagents require a skill target for static conversion");
20
+ if (target === "static-review") {
21
+ return {
22
+ source: input.kind,
23
+ target,
24
+ relativePath: `conversion-reports/${name}.md`,
25
+ content: `# Hook conversion review: ${name}\n\nThe original hook was not executed or converted into an executable hook.\n\n## Source instructions\n\n${input.body.trim()}\n`,
26
+ preserved: ["instruction text", "source name"],
27
+ dropped: [
28
+ {
29
+ field: "trigger and command execution",
30
+ reason: "hooks are not portable and must be reviewed manually",
31
+ },
32
+ ],
33
+ requiresApproval: true,
34
+ };
35
+ }
36
+ const dropped = [
37
+ {
38
+ field: "agent runtime metadata",
39
+ reason: "target agent may not support the source model/tools contract",
40
+ },
41
+ ];
42
+ for (const field of Object.keys(input.metadata ?? {}))
43
+ dropped.push({
44
+ field,
45
+ reason: "metadata is not part of the portable instruction body",
46
+ });
47
+ return {
48
+ source: input.kind,
49
+ target,
50
+ relativePath: `skills/${name}/SKILL.md`,
51
+ content: `---\nname: ${name}\ndescription: Converted static instructions; review before use\n---\n\n${input.body.trim()}\n`,
52
+ preserved: ["instruction text", "source name"],
53
+ dropped,
54
+ requiresApproval: true,
55
+ };
56
+ }
@@ -0,0 +1,377 @@
1
+ import { spawn } from "node:child_process";
2
+ const DEFAULT_TIMEOUT_MS = 5_000;
3
+ const DEFAULT_MAX_OUTPUT_BYTES = 64 * 1024;
4
+ const DEFAULT_ACCOUNT = "default";
5
+ const WINDOWS_MAX_SECRET_BYTES = 2_560;
6
+ /**
7
+ * Uses the Win32 Credential Manager API rather than cmdkey because cmdkey
8
+ * cannot retrieve generic credentials and would require a password argument.
9
+ * The secret is read exclusively from stdin by the set operation.
10
+ */
11
+ const WINDOWS_CREDENTIAL_SCRIPT = String.raw `
12
+ $source = @'
13
+ using System;
14
+ using System.ComponentModel;
15
+ using System.Runtime.InteropServices;
16
+
17
+ public static class LoadoutCredentialManager {
18
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
19
+ private struct Credential {
20
+ public UInt32 Flags;
21
+ public UInt32 Type;
22
+ public string TargetName;
23
+ public string Comment;
24
+ public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
25
+ public UInt32 CredentialBlobSize;
26
+ public IntPtr CredentialBlob;
27
+ public UInt32 Persist;
28
+ public UInt32 AttributeCount;
29
+ public IntPtr Attributes;
30
+ public string TargetAlias;
31
+ public string UserName;
32
+ }
33
+
34
+ [DllImport("advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
35
+ private static extern bool CredWrite(ref Credential credential, UInt32 flags);
36
+
37
+ [DllImport("advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
38
+ private static extern bool CredRead(string target, UInt32 type, UInt32 flags, out IntPtr credential);
39
+
40
+ [DllImport("advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)]
41
+ private static extern bool CredDelete(string target, UInt32 type, UInt32 flags);
42
+
43
+ [DllImport("advapi32.dll")]
44
+ private static extern void CredFree(IntPtr credential);
45
+
46
+ public static void Set(string target, string account, string secret) {
47
+ IntPtr blob = Marshal.StringToCoTaskMemUni(secret);
48
+ try {
49
+ Credential credential = new Credential {
50
+ Type = 1,
51
+ TargetName = target,
52
+ CredentialBlobSize = (UInt32)(secret.Length * 2),
53
+ CredentialBlob = blob,
54
+ Persist = 2,
55
+ UserName = account
56
+ };
57
+ if (!CredWrite(ref credential, 0)) throw new Win32Exception(Marshal.GetLastWin32Error());
58
+ } finally {
59
+ Marshal.ZeroFreeCoTaskMemUnicode(blob);
60
+ }
61
+ }
62
+
63
+ public static string Get(string target) {
64
+ IntPtr pointer;
65
+ if (!CredRead(target, 1, 0, out pointer)) {
66
+ int error = Marshal.GetLastWin32Error();
67
+ if (error == 1168) return null;
68
+ throw new Win32Exception(error);
69
+ }
70
+ try {
71
+ Credential credential = (Credential)Marshal.PtrToStructure(pointer, typeof(Credential));
72
+ return Marshal.PtrToStringUni(credential.CredentialBlob, (int)credential.CredentialBlobSize / 2);
73
+ } finally {
74
+ CredFree(pointer);
75
+ }
76
+ }
77
+
78
+ public static bool Delete(string target) {
79
+ if (CredDelete(target, 1, 0)) return true;
80
+ int error = Marshal.GetLastWin32Error();
81
+ if (error == 1168) return false;
82
+ throw new Win32Exception(error);
83
+ }
84
+ }
85
+ '@
86
+ Add-Type -TypeDefinition $source
87
+ $operation = $args[0]
88
+ $target = $args[1]
89
+ $account = $args[2]
90
+ if ($operation -eq 'get') {
91
+ $value = [LoadoutCredentialManager]::Get($target)
92
+ if ($null -eq $value) { exit 3 }
93
+ [Console]::Out.Write($value)
94
+ } elseif ($operation -eq 'set') {
95
+ $value = [Console]::In.ReadToEnd()
96
+ [LoadoutCredentialManager]::Set($target, $account, $value)
97
+ } elseif ($operation -eq 'delete') {
98
+ if (-not [LoadoutCredentialManager]::Delete($target)) { exit 3 }
99
+ } else {
100
+ throw 'Unsupported credential operation'
101
+ }
102
+ `;
103
+ function validateTimeout(timeoutMs) {
104
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 30_000)
105
+ throw new Error("Credential timeout must be between 100ms and 30000ms");
106
+ return timeoutMs;
107
+ }
108
+ function validateOutputLimit(maxOutputBytes) {
109
+ if (!Number.isInteger(maxOutputBytes) ||
110
+ maxOutputBytes < 1_024 ||
111
+ maxOutputBytes > 1024 * 1024)
112
+ throw new Error("Credential output limit must be between 1024 and 1048576 bytes");
113
+ return maxOutputBytes;
114
+ }
115
+ function validateIdentifier(value, field) {
116
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:@/+ -]{0,127}$/.test(value) ||
117
+ value !== value.trim())
118
+ throw new Error(`Credential ${field} must be 1-128 safe identifier characters`);
119
+ return value;
120
+ }
121
+ function normalizeReference(reference) {
122
+ if (reference.kind !== "os-keychain")
123
+ throw new Error("A keychain credential reference is required");
124
+ return {
125
+ service: validateIdentifier(reference.service, "service"),
126
+ account: validateIdentifier(reference.account ?? DEFAULT_ACCOUNT, "account"),
127
+ };
128
+ }
129
+ function validateEnvironmentName(name) {
130
+ if (!/^[A-Z_][A-Z0-9_]{0,127}$/.test(name))
131
+ throw new Error("Credential environment name is invalid");
132
+ return name;
133
+ }
134
+ function validateSecret(secret, platform) {
135
+ const maximum = platform === "win32" ? WINDOWS_MAX_SECRET_BYTES : DEFAULT_MAX_OUTPUT_BYTES;
136
+ const bytes = Buffer.byteLength(secret, platform === "win32" ? "utf16le" : "utf8");
137
+ if (!secret || secret.includes("\0") || bytes > maximum)
138
+ throw new Error("Credential value is empty or exceeds the backend limit");
139
+ }
140
+ function windowsTarget(reference) {
141
+ return `loadout:${reference.service}:${reference.account}`;
142
+ }
143
+ function backendFor(platform) {
144
+ if (platform === "darwin")
145
+ return {
146
+ statusName: "macos-keychain",
147
+ status: { command: "security", args: ["help"] },
148
+ get: ({ service, account }) => ({
149
+ command: "security",
150
+ args: ["find-generic-password", "-s", service, "-a", account, "-w"],
151
+ }),
152
+ set: ({ service, account }, secret) => ({
153
+ command: "security",
154
+ args: [
155
+ "add-generic-password",
156
+ "-U",
157
+ "-s",
158
+ service,
159
+ "-a",
160
+ account,
161
+ "-w",
162
+ ],
163
+ stdin: secret,
164
+ }),
165
+ delete: ({ service, account }) => ({
166
+ command: "security",
167
+ args: ["delete-generic-password", "-s", service, "-a", account],
168
+ }),
169
+ notFoundExitCodes: new Set([44]),
170
+ };
171
+ if (platform === "linux")
172
+ return {
173
+ statusName: "linux-secret-service",
174
+ status: { command: "secret-tool", args: ["--help"] },
175
+ get: ({ service, account }) => ({
176
+ command: "secret-tool",
177
+ args: ["lookup", "service", service, "account", account],
178
+ }),
179
+ set: ({ service, account }, secret) => ({
180
+ command: "secret-tool",
181
+ args: [
182
+ "store",
183
+ "--label=Loadout credential",
184
+ "service",
185
+ service,
186
+ "account",
187
+ account,
188
+ ],
189
+ stdin: secret,
190
+ }),
191
+ delete: ({ service, account }) => ({
192
+ command: "secret-tool",
193
+ args: ["clear", "service", service, "account", account],
194
+ }),
195
+ notFoundExitCodes: new Set([1]),
196
+ };
197
+ const powershellArgs = (operation, reference) => [
198
+ "-NoLogo",
199
+ "-NoProfile",
200
+ "-NonInteractive",
201
+ "-ExecutionPolicy",
202
+ "Bypass",
203
+ "-Command",
204
+ `& { ${WINDOWS_CREDENTIAL_SCRIPT} } '${operation}' '${windowsTarget(reference)}' '${reference.account}'`,
205
+ ];
206
+ return {
207
+ statusName: "windows-credential-manager",
208
+ status: {
209
+ command: "powershell.exe",
210
+ args: [
211
+ "-NoLogo",
212
+ "-NoProfile",
213
+ "-NonInteractive",
214
+ "-Command",
215
+ "if (Get-Command powershell.exe -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }",
216
+ ],
217
+ },
218
+ get: (reference) => ({
219
+ command: "powershell.exe",
220
+ args: powershellArgs("get", reference),
221
+ }),
222
+ set: (reference, secret) => ({
223
+ command: "powershell.exe",
224
+ args: powershellArgs("set", reference),
225
+ stdin: secret,
226
+ }),
227
+ delete: (reference) => ({
228
+ command: "powershell.exe",
229
+ args: powershellArgs("delete", reference),
230
+ }),
231
+ notFoundExitCodes: new Set([3]),
232
+ };
233
+ }
234
+ export const defaultCredentialCommandRunner = async (request) => new Promise((resolve, reject) => {
235
+ const child = spawn(request.command, request.args, {
236
+ shell: false,
237
+ windowsHide: true,
238
+ stdio: ["pipe", "pipe", "pipe"],
239
+ });
240
+ const stdout = [];
241
+ const stderr = [];
242
+ let stdoutBytes = 0;
243
+ let stderrBytes = 0;
244
+ let failure;
245
+ let settled = false;
246
+ const finishWithError = () => {
247
+ if (settled)
248
+ return;
249
+ settled = true;
250
+ clearTimeout(timer);
251
+ reject(new Error(`Credential command failed (${failure ?? "process"})`));
252
+ };
253
+ const collect = (target, chunk, isStdout) => {
254
+ if (failure)
255
+ return;
256
+ if (isStdout)
257
+ stdoutBytes += chunk.length;
258
+ else
259
+ stderrBytes += chunk.length;
260
+ if (stdoutBytes + stderrBytes > request.maxOutputBytes) {
261
+ failure = "output";
262
+ child.kill();
263
+ return;
264
+ }
265
+ target.push(chunk);
266
+ };
267
+ child.stdout.on("data", (chunk) => collect(stdout, chunk, true));
268
+ child.stderr.on("data", (chunk) => collect(stderr, chunk, false));
269
+ child.on("error", () => {
270
+ failure = "spawn";
271
+ finishWithError();
272
+ });
273
+ child.on("close", (code) => {
274
+ if (failure) {
275
+ finishWithError();
276
+ return;
277
+ }
278
+ if (settled)
279
+ return;
280
+ settled = true;
281
+ clearTimeout(timer);
282
+ resolve({
283
+ stdout: Buffer.concat(stdout).toString("utf8"),
284
+ stderr: Buffer.concat(stderr).toString("utf8"),
285
+ exitCode: code ?? 1,
286
+ });
287
+ });
288
+ child.stdin.on("error", () => {
289
+ // A command that exits early may close stdin. Its exit code is enough.
290
+ });
291
+ child.stdin.end(request.stdin ?? "");
292
+ const timer = setTimeout(() => {
293
+ failure = "timeout";
294
+ child.kill();
295
+ }, request.timeoutMs);
296
+ timer.unref();
297
+ });
298
+ function supportedPlatform(platform) {
299
+ if (platform === "darwin" || platform === "linux" || platform === "win32")
300
+ return platform;
301
+ throw new Error(`OS credential storage is unsupported on '${platform}'`);
302
+ }
303
+ function withoutCommandNewline(value) {
304
+ return value.replace(/\r?\n$/, "");
305
+ }
306
+ export function createOsCredentialStore(options = {}) {
307
+ const platform = supportedPlatform(options.platform ?? process.platform);
308
+ const backend = backendFor(platform);
309
+ const runner = options.runner ?? defaultCredentialCommandRunner;
310
+ const timeoutMs = validateTimeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
311
+ const maxOutputBytes = validateOutputLimit(options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES);
312
+ const run = async (operation, command) => {
313
+ try {
314
+ return await runner({
315
+ ...command,
316
+ timeoutMs,
317
+ maxOutputBytes,
318
+ });
319
+ }
320
+ catch {
321
+ // A custom runner could include its input in an exception. Never forward it.
322
+ throw new Error(`Credential ${operation} could not execute safely`);
323
+ }
324
+ };
325
+ return {
326
+ async get(reference) {
327
+ const result = await run("get", backend.get(normalizeReference(reference)));
328
+ if (backend.notFoundExitCodes.has(result.exitCode))
329
+ return undefined;
330
+ if (result.exitCode !== 0)
331
+ throw new Error("Credential get failed in the operating-system store");
332
+ const secret = withoutCommandNewline(result.stdout);
333
+ return secret || undefined;
334
+ },
335
+ async set(reference, secret) {
336
+ validateSecret(secret, platform);
337
+ const result = await run("set", backend.set(normalizeReference(reference), secret));
338
+ if (result.exitCode !== 0)
339
+ throw new Error("Credential set failed in the operating-system store");
340
+ },
341
+ async delete(reference) {
342
+ const result = await run("delete", backend.delete(normalizeReference(reference)));
343
+ if (backend.notFoundExitCodes.has(result.exitCode))
344
+ return false;
345
+ if (result.exitCode !== 0)
346
+ throw new Error("Credential delete failed in the operating-system store");
347
+ return true;
348
+ },
349
+ async status() {
350
+ try {
351
+ const result = await run("status", backend.status);
352
+ return {
353
+ platform,
354
+ backend: backend.statusName,
355
+ available: result.exitCode === 0,
356
+ };
357
+ }
358
+ catch {
359
+ return { platform, backend: backend.statusName, available: false };
360
+ }
361
+ },
362
+ };
363
+ }
364
+ /** Resolve secret-free configuration references only at the point of use. */
365
+ export function createCredentialResolver(options = {}) {
366
+ const environment = options.environment ?? process.env;
367
+ let store = options.store;
368
+ return async (reference) => {
369
+ if (reference.kind === "environment")
370
+ return environment[validateEnvironmentName(reference.name)] || undefined;
371
+ store ??= createOsCredentialStore(options);
372
+ return store.get(reference);
373
+ };
374
+ }
375
+ export async function resolveCredentialReference(reference, options = {}) {
376
+ return createCredentialResolver(options)(reference);
377
+ }