@ozyman42/ozy-cli 0.4.9-windows-x64.0 → 0.4.9

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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -0
  3. package/bin/ozy +209 -0
  4. package/bin/ozy-signing-agent +209 -0
  5. package/bin/ozy-ssh-keygen +209 -0
  6. package/bin/ozy-virtual-security-key +209 -0
  7. package/dist/commands/git/hosts.js +18 -0
  8. package/dist/commands/git/hosts.js.map +1 -0
  9. package/dist/commands/git/index.js +8 -0
  10. package/dist/commands/git/index.js.map +1 -0
  11. package/dist/commands/git/setup.js +145 -0
  12. package/dist/commands/git/setup.js.map +1 -0
  13. package/dist/commands/index.js +13 -0
  14. package/dist/commands/index.js.map +1 -0
  15. package/dist/commands/npm/index.js +7 -0
  16. package/dist/commands/npm/index.js.map +1 -0
  17. package/dist/commands/npm/setup/git-remote.js +66 -0
  18. package/dist/commands/npm/setup/git-remote.js.map +1 -0
  19. package/dist/commands/npm/setup/index.js +168 -0
  20. package/dist/commands/npm/setup/index.js.map +1 -0
  21. package/dist/commands/npm/setup/npm-auth.js +62 -0
  22. package/dist/commands/npm/setup/npm-auth.js.map +1 -0
  23. package/dist/commands/npm/setup/package-json-validate.js +47 -0
  24. package/dist/commands/npm/setup/package-json-validate.js.map +1 -0
  25. package/dist/commands/npm/setup/package-registry.js +20 -0
  26. package/dist/commands/npm/setup/package-registry.js.map +1 -0
  27. package/dist/commands/npm/setup/setup.test.js +95 -0
  28. package/dist/commands/npm/setup/setup.test.js.map +1 -0
  29. package/dist/commands/npm/setup/trusted-publishing.js +84 -0
  30. package/dist/commands/npm/setup/trusted-publishing.js.map +1 -0
  31. package/dist/commands/npm/setup/workflow.js +19 -0
  32. package/dist/commands/npm/setup/workflow.js.map +1 -0
  33. package/dist/commands/ssh/get-sk-credential.js +104 -0
  34. package/dist/commands/ssh/get-sk-credential.js.map +1 -0
  35. package/dist/commands/ssh/index.js +7 -0
  36. package/dist/commands/ssh/index.js.map +1 -0
  37. package/dist/commands/upgrade.js +236 -0
  38. package/dist/commands/upgrade.js.map +1 -0
  39. package/dist/common/command.js +15 -0
  40. package/dist/common/command.js.map +1 -0
  41. package/dist/common/constants.js +25 -0
  42. package/dist/common/constants.js.map +1 -0
  43. package/dist/common/effective-modules-extensions.js +13 -0
  44. package/dist/common/effective-modules-extensions.js.map +1 -0
  45. package/dist/common/log.js +4 -0
  46. package/dist/common/log.js.map +1 -0
  47. package/dist/common/render-caller-tree.js +20 -0
  48. package/dist/common/render-caller-tree.js.map +1 -0
  49. package/dist/entrypoints/ozy-signing-agent.js +48 -0
  50. package/dist/entrypoints/ozy-signing-agent.js.map +1 -0
  51. package/dist/entrypoints/ozy-ssh-keygen.js +9 -0
  52. package/dist/entrypoints/ozy-ssh-keygen.js.map +1 -0
  53. package/dist/entrypoints/ozy-virtual-security-key.js +18 -0
  54. package/dist/entrypoints/ozy-virtual-security-key.js.map +1 -0
  55. package/dist/entrypoints/ozy.js +3 -0
  56. package/dist/entrypoints/ozy.js.map +1 -0
  57. package/dist/index.js +2 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/internal/thing.js +4 -0
  60. package/dist/internal/thing.js.map +1 -0
  61. package/dist/modules/cli/agent-client/impl.js +109 -0
  62. package/dist/modules/cli/agent-client/impl.js.map +1 -0
  63. package/dist/modules/cli/agent-client/interface.js +11 -0
  64. package/dist/modules/cli/agent-client/interface.js.map +1 -0
  65. package/dist/modules/cli/git/impl.js +124 -0
  66. package/dist/modules/cli/git/impl.js.map +1 -0
  67. package/dist/modules/cli/git/interface.js +3 -0
  68. package/dist/modules/cli/git/interface.js.map +1 -0
  69. package/dist/modules/cli/git/submodule/fix.js +257 -0
  70. package/dist/modules/cli/git/submodule/fix.js.map +1 -0
  71. package/dist/modules/cli/git/submodule/git-state.js +154 -0
  72. package/dist/modules/cli/git/submodule/git-state.js.map +1 -0
  73. package/dist/modules/cli/git/submodule/gitmodules.js +113 -0
  74. package/dist/modules/cli/git/submodule/gitmodules.js.map +1 -0
  75. package/dist/modules/cli/git/submodule/printer.js +232 -0
  76. package/dist/modules/cli/git/submodule/printer.js.map +1 -0
  77. package/dist/modules/cli/git/submodule/resolve-git-dir.js +16 -0
  78. package/dist/modules/cli/git/submodule/resolve-git-dir.js.map +1 -0
  79. package/dist/modules/cli/github/impl.js +137 -0
  80. package/dist/modules/cli/github/impl.js.map +1 -0
  81. package/dist/modules/cli/github/interface.js +2 -0
  82. package/dist/modules/cli/github/interface.js.map +1 -0
  83. package/dist/modules/cli/index.js +9 -0
  84. package/dist/modules/cli/index.js.map +1 -0
  85. package/dist/modules/common/crypto/impl.js +133 -0
  86. package/dist/modules/common/crypto/impl.js.map +1 -0
  87. package/dist/modules/common/crypto/interface.js +2 -0
  88. package/dist/modules/common/crypto/interface.js.map +1 -0
  89. package/dist/modules/common/index.js +10 -0
  90. package/dist/modules/common/index.js.map +1 -0
  91. package/dist/modules/common/kep-map-store/impl.js +42 -0
  92. package/dist/modules/common/kep-map-store/impl.js.map +1 -0
  93. package/dist/modules/common/kep-map-store/interface.js +2 -0
  94. package/dist/modules/common/kep-map-store/interface.js.map +1 -0
  95. package/dist/modules/common/os-platform/caller-info.js +132 -0
  96. package/dist/modules/common/os-platform/caller-info.js.map +1 -0
  97. package/dist/modules/common/os-platform/impl.js +95 -0
  98. package/dist/modules/common/os-platform/impl.js.map +1 -0
  99. package/dist/modules/common/os-platform/interface.js +2 -0
  100. package/dist/modules/common/os-platform/interface.js.map +1 -0
  101. package/dist/modules/common/os-platform/virtual-hid/index.js +12 -0
  102. package/dist/modules/common/os-platform/virtual-hid/index.js.map +1 -0
  103. package/dist/modules/common/os-platform/virtual-hid/linux.js +5 -0
  104. package/dist/modules/common/os-platform/virtual-hid/linux.js.map +1 -0
  105. package/dist/modules/common/os-platform/virtual-hid/mac.js +119 -0
  106. package/dist/modules/common/os-platform/virtual-hid/mac.js.map +1 -0
  107. package/dist/modules/common/os-platform/virtual-hid/windows.js +5 -0
  108. package/dist/modules/common/os-platform/virtual-hid/windows.js.map +1 -0
  109. package/dist/modules/common/ssh-config/impl.js +116 -0
  110. package/dist/modules/common/ssh-config/impl.js.map +1 -0
  111. package/dist/modules/common/ssh-config/interface.js +10 -0
  112. package/dist/modules/common/ssh-config/interface.js.map +1 -0
  113. package/dist/modules/ssh-agent/index.js +8 -0
  114. package/dist/modules/ssh-agent/index.js.map +1 -0
  115. package/dist/modules/ssh-agent/session/impl.js +265 -0
  116. package/dist/modules/ssh-agent/session/impl.js.map +1 -0
  117. package/dist/modules/ssh-agent/session/interface-rpc.js +20 -0
  118. package/dist/modules/ssh-agent/session/interface-rpc.js.map +1 -0
  119. package/dist/modules/ssh-agent/session/interface.js +32 -0
  120. package/dist/modules/ssh-agent/session/interface.js.map +1 -0
  121. package/dist/modules/ssh-agent/session/passkey-prf-page.js +204 -0
  122. package/dist/modules/ssh-agent/session/passkey-prf-page.js.map +1 -0
  123. package/dist/modules/ssh-agent/session/prf-flow.js +103 -0
  124. package/dist/modules/ssh-agent/session/prf-flow.js.map +1 -0
  125. package/dist/modules/ssh-agent/ssh-agent/impl.js +117 -0
  126. package/dist/modules/ssh-agent/ssh-agent/impl.js.map +1 -0
  127. package/dist/modules/ssh-agent/ssh-agent/interface.js +2 -0
  128. package/dist/modules/ssh-agent/ssh-agent/interface.js.map +1 -0
  129. package/dist/scripts/build.js +444 -0
  130. package/dist/scripts/build.js.map +1 -0
  131. package/dist/scripts/check-npm-version.js +8 -0
  132. package/dist/scripts/check-npm-version.js.map +1 -0
  133. package/dist/scripts/constants.js +12 -0
  134. package/dist/scripts/constants.js.map +1 -0
  135. package/dist/scripts/kill-existing-agent.js +21 -0
  136. package/dist/scripts/kill-existing-agent.js.map +1 -0
  137. package/dist/scripts/publish.js +83 -0
  138. package/dist/scripts/publish.js.map +1 -0
  139. package/install.js +209 -0
  140. package/package.json +58 -11
  141. package/src/commands/git/hosts.ts +20 -0
  142. package/src/commands/git/index.ts +9 -0
  143. package/src/commands/git/setup.ts +182 -0
  144. package/src/commands/index.ts +15 -0
  145. package/src/commands/npm/index.ts +8 -0
  146. package/src/commands/npm/setup/git-remote.ts +78 -0
  147. package/src/commands/npm/setup/index.ts +189 -0
  148. package/src/commands/npm/setup/npm-auth.ts +58 -0
  149. package/src/commands/npm/setup/package-json-validate.ts +68 -0
  150. package/src/commands/npm/setup/package-registry.ts +24 -0
  151. package/src/commands/npm/setup/publish-workflow.yml +35 -0
  152. package/src/commands/npm/setup/setup.test.ts +117 -0
  153. package/src/commands/npm/setup/trusted-publishing.ts +106 -0
  154. package/src/commands/npm/setup/workflow.ts +20 -0
  155. package/src/commands/ssh/get-sk-credential.ts +110 -0
  156. package/src/commands/ssh/index.ts +8 -0
  157. package/src/commands/upgrade.ts +261 -0
  158. package/src/common/command.ts +16 -0
  159. package/src/common/constants.ts +26 -0
  160. package/src/common/effective-modules-extensions.ts +24 -0
  161. package/src/common/log.ts +3 -0
  162. package/src/common/render-caller-tree.ts +22 -0
  163. package/src/entrypoints/ozy-signing-agent.ts +48 -0
  164. package/src/entrypoints/ozy-ssh-keygen.ts +10 -0
  165. package/src/entrypoints/ozy-virtual-security-key.ts +21 -0
  166. package/src/entrypoints/ozy.ts +2 -0
  167. package/src/index.ts +1 -0
  168. package/src/internal/thing.ts +3 -0
  169. package/src/modules/cli/agent-client/impl.ts +152 -0
  170. package/src/modules/cli/agent-client/interface.ts +17 -0
  171. package/src/modules/cli/git/impl.ts +133 -0
  172. package/src/modules/cli/git/interface.ts +23 -0
  173. package/src/modules/cli/git/submodule/fix.ts +298 -0
  174. package/src/modules/cli/git/submodule/git-state.ts +217 -0
  175. package/src/modules/cli/git/submodule/gitmodules.ts +126 -0
  176. package/src/modules/cli/git/submodule/printer.ts +256 -0
  177. package/src/modules/cli/git/submodule/resolve-git-dir.ts +14 -0
  178. package/src/modules/cli/github/impl.ts +169 -0
  179. package/src/modules/cli/github/interface.ts +29 -0
  180. package/src/modules/cli/index.ts +16 -0
  181. package/src/modules/common/crypto/impl.ts +173 -0
  182. package/src/modules/common/crypto/interface.ts +20 -0
  183. package/src/modules/common/index.ts +19 -0
  184. package/src/modules/common/kep-map-store/impl.ts +47 -0
  185. package/src/modules/common/kep-map-store/interface.ts +9 -0
  186. package/src/modules/common/os-platform/caller-info.ts +152 -0
  187. package/src/modules/common/os-platform/impl.ts +91 -0
  188. package/src/modules/common/os-platform/interface.ts +16 -0
  189. package/src/modules/common/os-platform/virtual-hid/index.ts +12 -0
  190. package/src/modules/common/os-platform/virtual-hid/linux.ts +7 -0
  191. package/src/modules/common/os-platform/virtual-hid/mac.ts +150 -0
  192. package/src/modules/common/os-platform/virtual-hid/windows.ts +7 -0
  193. package/src/modules/common/ssh-config/impl.ts +130 -0
  194. package/src/modules/common/ssh-config/interface.ts +33 -0
  195. package/src/modules/ssh-agent/index.ts +13 -0
  196. package/src/modules/ssh-agent/session/agent-notes.md +210 -0
  197. package/src/modules/ssh-agent/session/design.md +2 -0
  198. package/src/modules/ssh-agent/session/html-refactor.md +23 -0
  199. package/src/modules/ssh-agent/session/impl.ts +328 -0
  200. package/src/modules/ssh-agent/session/interface-rpc.ts +24 -0
  201. package/src/modules/ssh-agent/session/interface.ts +62 -0
  202. package/src/modules/ssh-agent/session/passkey-prf-page.ts +224 -0
  203. package/src/modules/ssh-agent/session/prf-flow.ts +152 -0
  204. package/src/modules/ssh-agent/ssh-agent/impl.ts +136 -0
  205. package/src/modules/ssh-agent/ssh-agent/interface.ts +9 -0
  206. package/src/scripts/build.ts +559 -0
  207. package/src/scripts/check-npm-version.ts +7 -0
  208. package/src/scripts/command-launcher.js.ejs +186 -0
  209. package/src/scripts/constants.ts +11 -0
  210. package/src/scripts/kill-existing-agent.ts +22 -0
  211. package/src/scripts/publish.ts +89 -0
  212. package/src/scripts/windows-bin-shims.md +1104 -0
  213. package/multi-call-binary.exe +0 -0
@@ -0,0 +1,224 @@
1
+ import { Option } from "effect";
2
+ import { FUTURE_TOOL_NAME } from "@/common/constants";
3
+ import type { ECDHKeyPair } from "@/modules/common/crypto/interface";
4
+ import { CredentialId } from "@/modules/common/crypto/impl";
5
+ import { PrfInput } from "./prf-flow";
6
+
7
+ export interface UseOrCreatePasskeyPageInput {
8
+ agentKeyPair: ECDHKeyPair;
9
+ challenge: string;
10
+ context: PrfInput;
11
+ transportKeyContext: string;
12
+ username: string;
13
+ }
14
+
15
+ type Mode = 'credential' | 'selectExisting' | 'create' | 'createOrSelect';
16
+ type ChainNode = { command: string; directory: Option.Option<string> };
17
+ type TreeNode = { label: string; children: TreeNode[] };
18
+
19
+ function escHtml(s: string): string {
20
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
21
+ }
22
+
23
+ function nodeLabel(node: ChainNode): string {
24
+ const dir = Option.getOrElse(() => '(cannot load dir)')(node.directory);
25
+ return `${node.command} ${dir}`;
26
+ }
27
+
28
+ function findCommonPrefixLen(chains: ReadonlyArray<ReadonlyArray<ChainNode>>): number {
29
+ if (chains.length <= 1) return chains[0]?.length ?? 0;
30
+ let len = 0;
31
+ for (let i = 0; ; i++) {
32
+ if (chains.some(c => i >= c.length)) break;
33
+ const first = chains[0]![i]!;
34
+ if (chains.some(c => c[i]!.command !== first.command)) break;
35
+ len = i + 1;
36
+ }
37
+ return len;
38
+ }
39
+
40
+ function buildTreeNodes(chains: ReadonlyArray<ReadonlyArray<ChainNode>>, from: number): TreeNode[] {
41
+ const groups = new Map<string, { chains: ReadonlyArray<ChainNode>[]; node: ChainNode }>();
42
+ for (const chain of chains) {
43
+ if (from >= chain.length) continue;
44
+ const node = chain[from]!;
45
+ const key = `${node.command}|${Option.getOrElse(() => '')(node.directory)}`;
46
+ const existing = groups.get(key);
47
+ if (existing) {
48
+ existing.chains.push(chain);
49
+ } else {
50
+ groups.set(key, { chains: [chain], node });
51
+ }
52
+ }
53
+ return Array.from(groups.values()).map(({ chains: groupChains, node }) => ({
54
+ label: nodeLabel(node),
55
+ children: buildTreeNodes(groupChains, from + 1),
56
+ }));
57
+ }
58
+
59
+ function renderTreeLines(nodes: TreeNode[], prefix: string): string[] {
60
+ const lines: string[] = [];
61
+ for (let i = 0; i < nodes.length; i++) {
62
+ const node = nodes[i]!;
63
+ const isLast = i === nodes.length - 1;
64
+ const connector = isLast ? '└── ' : '├── ';
65
+ const childPrefix = prefix + (isLast ? ' ' : '│ ');
66
+ lines.push(escHtml(prefix + connector + node.label));
67
+ lines.push(...renderTreeLines(node.children, childPrefix));
68
+ }
69
+ return lines;
70
+ }
71
+
72
+ function buildSignRequestTreeHtml(chains: ReadonlyArray<ReadonlyArray<ChainNode>>): string {
73
+ if (chains.length === 0) return '';
74
+ const commonPrefixLen = findCommonPrefixLen(chains);
75
+ const minLen = Math.min(...chains.map(c => c.length));
76
+ // Show at least 3 levels per branch; always include divergence point
77
+ const displayStart = Math.max(0, Math.min(commonPrefixLen, minLen - 3));
78
+
79
+ const rootNodes = buildTreeNodes(chains, displayStart);
80
+ const lines: string[] = [];
81
+
82
+ if (rootNodes.length === 1) {
83
+ lines.push(escHtml(rootNodes[0]!.label));
84
+ lines.push(...renderTreeLines(rootNodes[0]!.children, ''));
85
+ } else {
86
+ lines.push(...renderTreeLines(rootNodes, ''));
87
+ }
88
+
89
+ return `<pre class="tree">${lines.join('\n')}</pre>`;
90
+ }
91
+
92
+ function getContextInfo(context: PrfInput): { contextHtml: string; credentialId: string | null; mode: Mode; friendlyName: string | null } {
93
+ return PrfInput.$match(context, {
94
+ DerivePubkeyOnly: ({ credentialId: credOpt, pubkey }) => {
95
+ const credIdBase58 = Option.getOrNull(credOpt);
96
+ const credIdBase64 = credIdBase58 ? new CredentialId(credIdBase58).base64 : null;
97
+ const mode: Mode = credIdBase64 !== null ? 'credential'
98
+ : Option.isSome(pubkey) ? 'selectExisting'
99
+ : 'createOrSelect';
100
+ const friendlyName = credIdBase58 ? new CredentialId(credIdBase58).humanReadableName : null;
101
+ const contextHtml = friendlyName && credIdBase58
102
+ ? `<p style="color:#666;font-size:.95rem">Setup — confirming ownership of <strong>${escHtml(friendlyName)}</strong> (${escHtml(credIdBase58)})</p>`
103
+ : '<p style="color:#666;font-size:.95rem">Setup — create or select a passkey for SSH signing</p>';
104
+ return { contextHtml, credentialId: credIdBase64, mode, friendlyName };
105
+ },
106
+ DerivePubkeyForRequests: ({ session }) => {
107
+ const chains = session.definition.expectedSignRequests.map(r => r.expectedCallerChain);
108
+ const friendlyName = new CredentialId(session.credentialId).humanReadableName;
109
+ const contextHtml = `<p style="color:#666;font-size:.95rem">Signing with <strong>${escHtml(friendlyName)}</strong> (${escHtml(session.credentialId)})</p>${buildSignRequestTreeHtml(chains)}`;
110
+ return {
111
+ contextHtml,
112
+ credentialId: new CredentialId(session.credentialId).base64,
113
+ mode: 'credential' as Mode,
114
+ friendlyName,
115
+ };
116
+ },
117
+ });
118
+ }
119
+
120
+ function buttonsHtml(mode: Mode, friendlyName: string | null): string {
121
+ if (mode === 'createOrSelect') {
122
+ return `<button class="btn" onclick="btnClick('create')">Create new passkey</button>` +
123
+ `<button class="btn" onclick="btnClick('selectExisting')">Sign with existing passkey</button>`;
124
+ }
125
+ const label = mode === 'credential' && friendlyName ? `Sign with ${friendlyName}`
126
+ : mode === 'selectExisting' ? 'Sign with existing passkey'
127
+ : 'Create new passkey';
128
+ return `<button class="btn" onclick="btnClick()">Authorize — ${label}</button>`;
129
+ }
130
+
131
+ export function useOrCreatePasskeyPage(input: UseOrCreatePasskeyPageInput): string {
132
+ const { agentKeyPair, challenge, context, transportKeyContext, username } = input;
133
+ const capitalizedName = FUTURE_TOOL_NAME.slice(0, 1).toLocaleUpperCase() + FUTURE_TOOL_NAME.slice(1);
134
+ const title = `${capitalizedName} SSH Auth`;
135
+
136
+ const { contextHtml, credentialId, mode, friendlyName } = getContextInfo(context);
137
+
138
+ const cfg = JSON.stringify({
139
+ agentPublicKey: agentKeyPair.base64Pubkey,
140
+ challenge,
141
+ credentialId,
142
+ mode,
143
+ username,
144
+ }).replace(/</g, "\\u003c");
145
+
146
+ return `<!DOCTYPE html>
147
+ <html>
148
+ <head><title>${title}</title><meta charset="utf-8">
149
+ <style>
150
+ body{font-family:system-ui,sans-serif;max-width:560px;margin:80px auto;padding:0 20px;text-align:center}
151
+ h1{font-size:1.5rem}#st{font-size:1.1rem;margin-top:20px}.ok{color:#060}.err{color:#d00}
152
+ .btn{margin:8px 4px;padding:10px 28px;font-size:1rem;cursor:pointer}.btn:disabled{cursor:default;opacity:.5}
153
+ .tree{font-family:monospace;text-align:left;font-size:.85rem;background:#f5f5f5;padding:12px;border-radius:6px;overflow-x:auto;margin:16px 0}
154
+ </style>
155
+ </head>
156
+ <body>
157
+ <h1>${title}</h1>
158
+ ${contextHtml}
159
+ <div id="trust-row" style="margin-bottom:16px;color:#aaa;font-size:.9rem"><label><input type="checkbox" id="trust" onchange="trustChanged()"> trust this device (cache SSH private key) for <input type="number" id="mins" value="15" min="1" disabled style="width:4rem;color:#aaa"> minutes</label></div>
160
+ ${buttonsHtml(mode, friendlyName)}
161
+ <p id="st"></p>
162
+ <script>
163
+ const C=${cfg};
164
+ function bu(b){return btoa(String.fromCharCode(...new Uint8Array(b))).replace(/\\+/g,'-').replace(/\\//g,'_').replace(/=+$/,'')}
165
+ function fu(s){const b=atob(s.replace(/-/g,'+').replace(/_/g,'/'));const u=new Uint8Array(b.length);for(let i=0;i<b.length;i++)u[i]=b.charCodeAt(i);return u}
166
+ function btnClick(m){document.querySelectorAll('.btn').forEach(b=>b.disabled=true);main(m);}
167
+ function trustChanged(){const c=document.getElementById('trust').checked;const m=document.getElementById('mins');document.getElementById('trust-row').style.color=c?'':'#aaa';m.disabled=!c;m.style.color=c?'':'#aaa';}
168
+ async function main(modeOverride){
169
+ const mode=modeOverride??C.mode;
170
+ const st=document.getElementById('st');
171
+ const fid=new URLSearchParams(location.search).get('id');
172
+ try{
173
+ const ch=fu(C.challenge);
174
+ const salt=new Uint8Array(32);new TextEncoder().encodeInto('${FUTURE_TOOL_NAME}-prf-v1',salt);
175
+ let cred;
176
+ if(mode==='credential'){
177
+ cred=await navigator.credentials.get({publicKey:{
178
+ challenge:ch,userVerification:'required',
179
+ allowCredentials:[{type:'public-key',id:fu(C.credentialId)}],
180
+ extensions:{prf:{eval:{first:salt}}}
181
+ }});
182
+ }else if(mode==='selectExisting'){
183
+ cred=await navigator.credentials.get({publicKey:{
184
+ challenge:ch,userVerification:'required',
185
+ extensions:{prf:{eval:{first:salt}}}
186
+ }});
187
+ }else{
188
+ cred=await navigator.credentials.create({publicKey:{
189
+ challenge:ch,rp:{name:'${FUTURE_TOOL_NAME} challenge'},
190
+ user:{id:crypto.getRandomValues(new Uint8Array(16)),name:C.username,displayName:C.username+' — ${capitalizedName} SSH Key'},
191
+ pubKeyCredParams:[{alg:-7,type:'public-key'}],
192
+ authenticatorSelection:{authenticatorAttachment:'platform',userVerification:'required',residentKey:'required'},
193
+ extensions:{prf:{eval:{first:salt}}}
194
+ }});
195
+ }
196
+ const prf=cred.getClientExtensionResults().prf?.results?.first;
197
+ if(!prf)throw Object.assign(new Error('PRF extension not returned — authenticator may not support PRF'),{noRetry:true});
198
+ const ap=await crypto.subtle.importKey('raw',fu(C.agentPublicKey),{name:'ECDH',namedCurve:'P-256'},false,[]);
199
+ const bkp=await crypto.subtle.generateKey({name:'ECDH',namedCurve:'P-256'},true,['deriveBits']);
200
+ const sh=await crypto.subtle.deriveBits({name:'ECDH',public:ap},bkp.privateKey,256);
201
+ const hk=await crypto.subtle.importKey('raw',sh,'HKDF',false,['deriveKey']);
202
+ const ak=await crypto.subtle.deriveKey(
203
+ {name:'HKDF',hash:'SHA-256',salt:new Uint8Array(32),info:new TextEncoder().encode('${transportKeyContext}')},
204
+ hk,{name:'AES-GCM',length:256},false,['encrypt']
205
+ );
206
+ const iv=crypto.getRandomValues(new Uint8Array(12));
207
+ const enc=await crypto.subtle.encrypt({name:'AES-GCM',iv},ak,prf);
208
+ const bp=await crypto.subtle.exportKey('raw',bkp.publicKey);
209
+ const cm=document.getElementById('trust').checked?Math.max(1,parseInt(document.getElementById('mins').value,10)||15):undefined;
210
+ const r=await fetch('/seed?id='+fid,{method:'POST',headers:{'Content-Type':'application/json'},
211
+ body:JSON.stringify({encryptedSeed:bu(enc),iv:bu(iv),browserPublicKey:bu(bp),credentialId:bu(cred.rawId),cacheMinutes:cm})});
212
+ if(!r.ok)throw new Error('Agent returned '+r.status);
213
+ const d=await r.json();
214
+ st.className='ok';st.textContent='Authorized as '+d.friendlyName+'. You can close this tab.';
215
+ }catch(e){
216
+ const cancelled=e&&(e.name==='NotAllowedError'||e.name==='AbortError');
217
+ await fetch('/error?id='+fid,{method:'POST',headers:{'Content-Type':'application/json'},
218
+ body:JSON.stringify({cancelled,message:e?.message??String(e)})}).catch(()=>{});
219
+ st.className='err';st.textContent=(cancelled?'Cancelled or passkey not found.':'Error: '+(e?.message??e))+' You can close this tab.';
220
+ }
221
+ }
222
+ </script>
223
+ </body></html>`;
224
+ }
@@ -0,0 +1,152 @@
1
+ import { Effect, Option, pipe, Deferred, Scope, Data, Result } from "effect";
2
+ import { effunct, type EffectGen } from "effective-modules";
3
+ import { SessionError } from "./interface";
4
+ import { FUTURE_TOOL_NAME } from "@/common/constants";
5
+ import { randomUUID } from "node:crypto";
6
+ import { CredentialId } from "@/modules/common/crypto/impl";
7
+ import { useOrCreatePasskeyPage } from "./passkey-prf-page";
8
+ import { CommonModules, commonModules } from "@/modules/common";
9
+ import { log } from "@/common/log";
10
+ import { SSHKeyPair } from "@/modules/common/crypto/impl";
11
+ import type { ActiveSession } from "./impl";
12
+
13
+ type BrowserResult = {
14
+ seed: Uint8Array;
15
+ credentialId: CredentialId;
16
+ cacheMinutes?: number;
17
+ };
18
+
19
+ export type PrfResult = {
20
+ keyPair: SSHKeyPair;
21
+ credentialId: CredentialId;
22
+ cacheMinutes?: number;
23
+ };
24
+
25
+ export type PrfInput = Data.TaggedEnum<{
26
+ DerivePubkeyOnly: {
27
+ pubkey: Option.Option<string>;
28
+ credentialId: Option.Option<string>;
29
+ username: string;
30
+ },
31
+ DerivePubkeyForRequests: {
32
+ session: ActiveSession
33
+ }
34
+ }>
35
+ export const PrfInput = Data.taggedEnum<PrfInput>();
36
+
37
+ const TRANSPORT_KEY_CONTEXT = `${FUTURE_TOOL_NAME}-transport-v1`;
38
+
39
+ export function* prfFlow(input: PrfInput): EffectGen<PrfResult, SessionError, CommonModules.Crypto | CommonModules.OSPlatform | Scope.Scope> {
40
+ const crypto = yield* commonModules.Crypto;
41
+ const osPlatform = yield* commonModules.OSPlatform;
42
+
43
+ const flowId = PrfInput.$match(input, {
44
+ DerivePubkeyOnly: () => randomUUID(),
45
+ DerivePubkeyForRequests: ({ session }) => session.id,
46
+ });
47
+
48
+ const agentKeyPair = yield* pipe(
49
+ effunct(crypto.createECDHKey)(),
50
+ Effect.catch(err => Effect.fail(SessionError.cases.InternalError.make({ reason: err })))
51
+ );
52
+
53
+ const challenge = yield* crypto.getRandomChallenge();
54
+ const deferred = yield* Deferred.make<BrowserResult, SessionError>();
55
+
56
+ const server = yield* Effect.acquireRelease(
57
+ Effect.sync(() =>
58
+ Bun.serve({
59
+ port: 0,
60
+ async fetch(req) {
61
+ const url = new URL(req.url);
62
+ if (url.searchParams.get("id") !== flowId) {
63
+ return new Response("Forbidden", { status: 403 });
64
+ }
65
+ if (url.pathname === "/" && req.method === "GET") {
66
+ return new Response(
67
+ useOrCreatePasskeyPage({
68
+ agentKeyPair, challenge, context: input,
69
+ transportKeyContext: TRANSPORT_KEY_CONTEXT,
70
+ username: PrfInput.$match(input, {
71
+ DerivePubkeyOnly: ({ username }) => username,
72
+ DerivePubkeyForRequests: () => '',
73
+ }),
74
+ }),
75
+ { headers: { "Content-Type": "text/html; charset=utf-8" } }
76
+ );
77
+ }
78
+ if (url.pathname === "/seed" && req.method === "POST") {
79
+ try {
80
+ const body = await req.json() as {
81
+ encryptedSeed: string; iv: string;
82
+ browserPublicKey: string; credentialId: string;
83
+ cacheMinutes?: number;
84
+ };
85
+ Effect.runFork(Effect.gen(function* () {
86
+ const innerResult = yield* Effect.result(Effect.gen(function* () {
87
+ const decrypted = yield* crypto.decrypt({
88
+ senderPubkey: body.browserPublicKey,
89
+ receiverKey: agentKeyPair.keyPair,
90
+ cipherText: body.encryptedSeed,
91
+ iv: body.iv,
92
+ hkdfInfo: TRANSPORT_KEY_CONTEXT
93
+ });
94
+ const completed = yield* Deferred.succeed(deferred, {
95
+ seed: decrypted,
96
+ credentialId: CredentialId.fromBase64(body.credentialId),
97
+ cacheMinutes: body.cacheMinutes,
98
+ });
99
+ if (!completed) {
100
+ yield* Effect.fail(SessionError.cases.InternalError.make({
101
+ reason: 'Detected multiple POSTs to seed endpoint'
102
+ }));
103
+ }
104
+ }));
105
+ if (Result.isFailure(innerResult)) {
106
+ console.error('[/seed fork error]', innerResult.failure);
107
+ yield* Deferred.fail(deferred, SessionError.cases.InternalError.make({
108
+ reason: `Seed decryption failed: ${String(innerResult.failure)}`
109
+ }));
110
+ }
111
+ }));
112
+ return new Response(JSON.stringify({ ok: true, friendlyName: CredentialId.fromBase64(body.credentialId).humanReadableName }), {
113
+ headers: { "Content-Type": "application/json" },
114
+ });
115
+ } catch (e) {
116
+ console.error("[/seed handler error]", e);
117
+ Effect.runFork(Deferred.fail(deferred, SessionError.cases.InternalError.make({
118
+ reason: (e as Error).toString()
119
+ })));
120
+ return new Response(JSON.stringify({ error: String(e) }), {
121
+ status: 500, headers: { "Content-Type": "application/json" },
122
+ });
123
+ }
124
+ }
125
+ if (url.pathname === "/error" && req.method === "POST") {
126
+ const body = await req.json() as { cancelled: boolean; message: string };
127
+ const error = body.cancelled
128
+ ? SessionError.cases.WebAuthnCancelled.make({})
129
+ : SessionError.cases.InternalError.make({ reason: body.message });
130
+ Effect.runFork(Deferred.fail(deferred, error));
131
+ return new Response(JSON.stringify({ ok: true }), {
132
+ headers: { "Content-Type": "application/json" },
133
+ });
134
+ }
135
+ return new Response("Not found", { status: 404 });
136
+ },
137
+ })
138
+ ),
139
+ (server) => Effect.sync(() => server.stop())
140
+ );
141
+
142
+ log(`Opening browser for WebAuthn PRF on port ${server.port}`);
143
+ yield* osPlatform.openBrowserWindow(`http://localhost:${server.port}/?id=${flowId}`);
144
+
145
+ const { seed, credentialId, cacheMinutes } = yield* Deferred.await(deferred);
146
+
147
+ const keyPair = yield* pipe(
148
+ SSHKeyPair.fromSeed(seed, `${FUTURE_TOOL_NAME}-key-v1`),
149
+ Effect.mapError(reason => SessionError.cases.InternalError.make({ reason }))
150
+ );
151
+ return { keyPair, credentialId, cacheMinutes };
152
+ }
@@ -0,0 +1,136 @@
1
+ import { Effect, pipe, Result, Layer } from "effect";
2
+ import { effunct, implementing, type EffectGen } from "effective-modules";
3
+ import type { ISSHAgent } from "./interface";
4
+ import type { CallerProcess } from "@/modules/common/os-platform/interface";
5
+ import { agentModules } from "@/modules/ssh-agent";
6
+ import { commonModules } from "@/modules/common";
7
+ import { SSHPubkey } from "@/modules/common/crypto/impl";
8
+ import { log } from "@/common/log";
9
+ import { renderCallerTree } from "@/common/render-caller-tree";
10
+ import { AGENT_PID_FILE_PATH, AGENT_SOCK_FILE_PATH } from "@/common/constants";
11
+ import { writeFileSync } from "node:fs";
12
+
13
+ const SSH2_AGENT_FAILURE = 5;
14
+ const SSH2_AGENT_REQUEST_IDENTITIES = 11;
15
+ const SSH2_AGENT_IDENTITIES_ANSWER = 12;
16
+ const SSH2_AGENT_SIGN_REQUEST = 13;
17
+ const SSH2_AGENT_SIGN_RESPONSE = 14;
18
+ const SSH2_AGENT_EXTENSION = 27;
19
+
20
+ function u32(n: number): Buffer {
21
+ const b = Buffer.allocUnsafe(4);
22
+ b.writeUInt32BE(n, 0);
23
+ return b;
24
+ }
25
+
26
+ function sshString(s: Buffer): Buffer {
27
+ return Buffer.concat([u32(s.length), s]);
28
+ }
29
+
30
+ function buildSignResponse(keyType: string, signature: Buffer): Buffer {
31
+ const sigBlob = Buffer.concat([sshString(Buffer.from(keyType)), sshString(signature)]);
32
+ const body = Buffer.concat([Buffer.from([SSH2_AGENT_SIGN_RESPONSE]), sshString(sigBlob)]);
33
+ return Buffer.concat([u32(body.length), body]);
34
+ }
35
+
36
+ function failureResponse(): Buffer {
37
+ const r = Buffer.alloc(5);
38
+ r.writeUInt32BE(1, 0);
39
+ r[4] = SSH2_AGENT_FAILURE;
40
+ return r;
41
+ }
42
+
43
+ const { KeyMapStore, OSPlatform } = commonModules;
44
+ const { SSHAgent, Session } = agentModules;
45
+
46
+ export class SSHAgentImpl extends implementing(SSHAgent).uses(Session, KeyMapStore, OSPlatform) implements ISSHAgent {
47
+
48
+ private log(...items: any[]) {
49
+ log(`[ssh-agent]`, ...items);
50
+ }
51
+
52
+ *start(): EffectGen<void, string> {
53
+ const { OSPlatform: { startSocketServer } } = this.dependencies;
54
+ const { handleExtension, handleRequestIdentities, handleSignRequest, log } = this;
55
+ yield* startSocketServer(AGENT_SOCK_FILE_PATH, function*(data, callerChain): EffectGen<Buffer> {
56
+ const msgType = data[4];
57
+ switch (msgType) {
58
+ case SSH2_AGENT_EXTENSION:
59
+ log(`received extensions request`);
60
+ return yield* handleExtension(data);
61
+ case SSH2_AGENT_REQUEST_IDENTITIES:
62
+ log(`received request identities request`);
63
+ return yield* handleRequestIdentities();
64
+ case SSH2_AGENT_SIGN_REQUEST:
65
+ log(`received signature request`);
66
+ return yield* handleSignRequest(data, callerChain);
67
+ default:
68
+ log(`received unknown message type ${msgType}`);
69
+ return failureResponse();
70
+ }
71
+ });
72
+ writeFileSync(AGENT_PID_FILE_PATH, process.pid.toString());
73
+ log(`listening on ${AGENT_SOCK_FILE_PATH}`);
74
+ }
75
+
76
+ *handleExtension(data: Buffer): EffectGen<Buffer, never> {
77
+ const nameLen = data.readUInt32BE(5);
78
+ const name = data.subarray(9, 9 + nameLen).toString("utf8");
79
+ this.log(`extension request for ${name}`);
80
+ // We don't support any extensions, so we fail this outright.
81
+ return yield* Effect.sync(() => failureResponse());
82
+ }
83
+
84
+ *handleRequestIdentities(): EffectGen<Buffer, never> {
85
+ const keyMapStore = this.dependencies.KeyMapStore;
86
+ const listResult = yield* pipe(
87
+ effunct(keyMapStore.listPubkeys)(),
88
+ Effect.result
89
+ );
90
+ const pubkeys = Result.isSuccess(listResult) ? listResult.success : [];
91
+ if (Result.isFailure(listResult)) {
92
+ this.log(`Failure to retrieve keys ${listResult.failure}`);
93
+ }
94
+ this.log(`Total keys ${pubkeys.length}`);
95
+ if (pubkeys.length === 0) {
96
+ const body = Buffer.concat([Buffer.from([SSH2_AGENT_IDENTITIES_ANSWER]), u32(0)]);
97
+ return Buffer.concat([u32(body.length), body]);
98
+ }
99
+ const keyEntries = pubkeys.map(pubkey => {
100
+ return Buffer.concat([sshString(pubkey.wire), sshString(Buffer.alloc(0))]);
101
+ });
102
+ const body = Buffer.concat([
103
+ Buffer.from([SSH2_AGENT_IDENTITIES_ANSWER]),
104
+ u32(pubkeys.length),
105
+ ...keyEntries,
106
+ ]);
107
+ return Buffer.concat([u32(body.length), body]);
108
+ }
109
+
110
+ *handleSignRequest(data: Buffer, callerChain: CallerProcess[]): EffectGen<Buffer, never> {
111
+ const session = this.dependencies.Session;
112
+ let offset = 5; // skip length(4) + type(1)
113
+ const keyBlobLen = data.readUInt32BE(offset); offset += 4;
114
+ const pubkeyWire = Buffer.from(data.subarray(offset, offset + keyBlobLen)); offset += keyBlobLen;
115
+ const dataToSignLen = data.readUInt32BE(offset); offset += 4;
116
+ const dataToSign = Buffer.from(data.subarray(offset, offset + dataToSignLen));
117
+
118
+ const pubkey = SSHPubkey.fromWire(pubkeyWire);
119
+
120
+ this.log(`Sign request for pubkey ${pubkey.authorizedKey}\n${renderCallerTree(callerChain.slice(-3))}`);
121
+ const signResult = yield* pipe(
122
+ effunct(session.sign)({
123
+ data: dataToSign,
124
+ pubkey,
125
+ callerTree: callerChain
126
+ }),
127
+ Effect.result
128
+ );
129
+ return Result.isSuccess(signResult) ? buildSignResponse(pubkey.keyType, signResult.success) : failureResponse();
130
+ }
131
+
132
+ public static ServiceLayer = Layer.effectDiscard(Effect.gen(function*() {
133
+ const sshAgent = yield* agentModules.SSHAgent;
134
+ yield* sshAgent.start();
135
+ })).pipe(Layer.provideMerge(this.Layer))
136
+ }
@@ -0,0 +1,9 @@
1
+ import type { EffectGen } from "effective-modules";
2
+ import type { CallerProcess } from "@/modules/common/os-platform/interface";
3
+
4
+ export interface ISSHAgent {
5
+ start(): EffectGen<void, string>;
6
+ handleExtension(data: Buffer): EffectGen<Buffer, never>;
7
+ handleRequestIdentities(): EffectGen<Buffer, never>;
8
+ handleSignRequest(data: Buffer, callerChain: CallerProcess[]): EffectGen<Buffer, never>;
9
+ }