clauderipple 0.2.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 (71) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/LICENSE +674 -0
  3. package/README.ko.md +328 -0
  4. package/README.md +372 -0
  5. package/bin/clauderipple.js +12 -0
  6. package/dist/app/assets/trayDownTemplate.png +0 -0
  7. package/dist/app/assets/trayDownTemplate@2x.png +0 -0
  8. package/dist/app/assets/trayTemplate.png +0 -0
  9. package/dist/app/assets/trayTemplate@2x.png +0 -0
  10. package/dist/app/assets/trayWarnTemplate.png +0 -0
  11. package/dist/app/assets/trayWarnTemplate@2x.png +0 -0
  12. package/dist/app/assets/trayWin.png +0 -0
  13. package/dist/app/assets/trayWin@2x.png +0 -0
  14. package/dist/app/assets/trayWinDown.png +0 -0
  15. package/dist/app/assets/trayWinDown@2x.png +0 -0
  16. package/dist/app/assets/trayWinWarn.png +0 -0
  17. package/dist/app/assets/trayWinWarn@2x.png +0 -0
  18. package/dist/app/dist/main.js +518 -0
  19. package/dist/cli/src/browser.js +21 -0
  20. package/dist/cli/src/bundle.js +51 -0
  21. package/dist/cli/src/certs.js +33 -0
  22. package/dist/cli/src/claude-auth.js +112 -0
  23. package/dist/cli/src/codex.js +172 -0
  24. package/dist/cli/src/gen-certs.js +7 -0
  25. package/dist/cli/src/hooks/agent-title.js +160 -0
  26. package/dist/cli/src/index.js +489 -0
  27. package/dist/cli/src/launchd.js +183 -0
  28. package/dist/cli/src/picker.js +166 -0
  29. package/dist/cli/src/probe.js +55 -0
  30. package/dist/cli/src/runtime.js +62 -0
  31. package/dist/cli/src/schtasks.js +134 -0
  32. package/dist/cli/src/settings.js +142 -0
  33. package/dist/cli/src/supervisor.js +100 -0
  34. package/dist/cli/src/tray.js +85 -0
  35. package/dist/router/src/admin.js +945 -0
  36. package/dist/router/src/bootstrap.js +80 -0
  37. package/dist/router/src/certs.js +65 -0
  38. package/dist/router/src/compat.js +172 -0
  39. package/dist/router/src/config.js +179 -0
  40. package/dist/router/src/health.js +45 -0
  41. package/dist/router/src/identity.js +51 -0
  42. package/dist/router/src/index.js +144 -0
  43. package/dist/router/src/ingress/models.js +29 -0
  44. package/dist/router/src/ingress/server.js +400 -0
  45. package/dist/router/src/ingress/translate.js +457 -0
  46. package/dist/router/src/log.js +81 -0
  47. package/dist/router/src/picker.js +74 -0
  48. package/dist/router/src/presets.js +267 -0
  49. package/dist/router/src/providers/anthropic-observed.js +88 -0
  50. package/dist/router/src/providers/anthropic-token-file.js +48 -0
  51. package/dist/router/src/providers/anthropic.js +203 -0
  52. package/dist/router/src/providers/chatgpt/auth.js +226 -0
  53. package/dist/router/src/providers/chatgpt/index.js +274 -0
  54. package/dist/router/src/providers/chatgpt/sse.js +28 -0
  55. package/dist/router/src/providers/chatgpt/translate.js +393 -0
  56. package/dist/router/src/providers/claude-oauth.js +252 -0
  57. package/dist/router/src/providers/openai/index.js +193 -0
  58. package/dist/router/src/providers/openai/translate.js +504 -0
  59. package/dist/router/src/proxy.js +724 -0
  60. package/dist/router/src/redact.js +43 -0
  61. package/dist/router/src/requestlog.js +346 -0
  62. package/dist/router/src/routing.js +113 -0
  63. package/dist/router/src/version.js +8 -0
  64. package/dist/router/src/x509.js +203 -0
  65. package/dist/ui/app.js +1228 -0
  66. package/dist/ui/i18n.js +95 -0
  67. package/dist/ui/index.html +104 -0
  68. package/dist/ui/presets-fallback.js +61 -0
  69. package/dist/ui/style.css +347 -0
  70. package/docs/ARCHITECTURE.md +441 -0
  71. package/package.json +66 -0
@@ -0,0 +1,203 @@
1
+ // Minimal X.509 issuance on node:crypto alone — no openssl binary, no npm dependency.
2
+ //
3
+ // Why hand-rolled: the product needs exactly two shapes, a self-signed CA and a serverAuth leaf,
4
+ // and Windows ships no openssl (macOS LibreSSL also forced -sha256 and RSA on us, see below). A
5
+ // general X.509 library would pull a dependency tree into a project that has none and whose app
6
+ // bundle ships source files, not node_modules. The public key comes out of node:crypto already
7
+ // DER-encoded (SPKI) and node:crypto does the signing, so what is left is a thin DER writer.
8
+ //
9
+ // RSA 2048 + SHA-256 on purpose, matching what the openssl path produced: Node's TLS client
10
+ // rejects EC keys written with explicit curve parameters, and SHA-1 signatures ("ca md too weak").
11
+ //
12
+ // Wrong output cannot pass silently: a malformed certificate fails the TLS handshake outright,
13
+ // and the tests cross-check every field against openssl.
14
+ import crypto from "node:crypto";
15
+ // ---- DER writing ------------------------------------------------------------------------
16
+ const TAG = {
17
+ INTEGER: 0x02,
18
+ BIT_STRING: 0x03,
19
+ OCTET_STRING: 0x04,
20
+ NULL: 0x05,
21
+ OID: 0x06,
22
+ UTF8_STRING: 0x0c,
23
+ SEQUENCE: 0x30,
24
+ SET: 0x31,
25
+ BOOLEAN: 0x01,
26
+ UTC_TIME: 0x17,
27
+ };
28
+ /** DER length: short form below 128, else long form with a leading byte-count byte. */
29
+ function len(n) {
30
+ if (n < 0x80)
31
+ return Buffer.from([n]);
32
+ const bytes = [];
33
+ for (let v = n; v > 0; v = Math.floor(v / 256))
34
+ bytes.unshift(v % 256);
35
+ return Buffer.from([0x80 | bytes.length, ...bytes]);
36
+ }
37
+ function tlv(tag, value) {
38
+ return Buffer.concat([Buffer.from([tag]), len(value.length), value]);
39
+ }
40
+ const seq = (...parts) => tlv(TAG.SEQUENCE, Buffer.concat(parts));
41
+ const set = (...parts) => tlv(TAG.SET, Buffer.concat(parts));
42
+ /** Context-specific constructed [n], used for the version and extensions wrappers. */
43
+ const explicit = (n, value) => tlv(0xa0 | n, value);
44
+ /** DER INTEGER is signed: a leading byte >= 0x80 needs a 0x00 pad so it stays positive. */
45
+ function integer(value) {
46
+ let b = typeof value === "number" ? Buffer.from([value]) : value;
47
+ while (b.length > 1 && b[0] === 0x00 && (b[1] & 0x80) === 0)
48
+ b = b.subarray(1);
49
+ if (b[0] & 0x80)
50
+ b = Buffer.concat([Buffer.from([0x00]), b]);
51
+ return tlv(TAG.INTEGER, b);
52
+ }
53
+ function oid(dotted) {
54
+ const parts = dotted.split(".").map(Number);
55
+ const bytes = [parts[0] * 40 + parts[1]];
56
+ for (const part of parts.slice(2)) {
57
+ const chunk = [part % 128];
58
+ for (let v = Math.floor(part / 128); v > 0; v = Math.floor(v / 128))
59
+ chunk.unshift((v % 128) | 0x80);
60
+ bytes.push(...chunk);
61
+ }
62
+ return tlv(TAG.OID, Buffer.from(bytes));
63
+ }
64
+ /** BIT STRING with the count of unused trailing bits as its first content byte. */
65
+ function bitString(bits, unused = 0) {
66
+ return tlv(TAG.BIT_STRING, Buffer.concat([Buffer.from([unused]), bits]));
67
+ }
68
+ /** UTCTime (YYMMDDHHMMSSZ). Valid until 2049; nothing we issue reaches that. */
69
+ function utcTime(d) {
70
+ const p = (n) => String(n).padStart(2, "0");
71
+ const s = `${p(d.getUTCFullYear() % 100)}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}${p(d.getUTCHours())}${p(d.getUTCMinutes())}${p(d.getUTCSeconds())}Z`;
72
+ return tlv(TAG.UTC_TIME, Buffer.from(s, "ascii"));
73
+ }
74
+ const OID_CN = "2.5.4.3";
75
+ const OID_SHA256_RSA = "1.2.840.113549.1.1.11";
76
+ /** A Name with a single CN, written as UTF8String (what openssl's default string_mask emits). */
77
+ function nameWithCn(cn) {
78
+ return seq(set(seq(oid(OID_CN), tlv(TAG.UTF8_STRING, Buffer.from(cn, "utf8")))));
79
+ }
80
+ const sha256WithRsa = () => seq(oid(OID_SHA256_RSA), tlv(TAG.NULL, Buffer.alloc(0)));
81
+ function extension(id, critical, value) {
82
+ const parts = [oid(id)];
83
+ if (critical)
84
+ parts.push(tlv(TAG.BOOLEAN, Buffer.from([0xff])));
85
+ parts.push(tlv(TAG.OCTET_STRING, value));
86
+ return seq(...parts);
87
+ }
88
+ // ---- DER reading (only enough to lift a field out of an existing certificate) ------------
89
+ /** Range of the TLV starting at `offset`: where its value begins and where the whole TLV ends. */
90
+ function readTlv(buf, offset) {
91
+ const tag = buf[offset];
92
+ const first = buf[offset + 1];
93
+ if (first < 0x80)
94
+ return { tag, valueStart: offset + 2, end: offset + 2 + first };
95
+ const n = first & 0x7f;
96
+ let length = 0;
97
+ for (let i = 0; i < n; i++)
98
+ length = length * 256 + buf[offset + 2 + i];
99
+ return { tag, valueStart: offset + 2 + n, end: offset + 2 + n + length };
100
+ }
101
+ /**
102
+ * The subject Name of `certPem`, as the exact DER bytes it was written with.
103
+ *
104
+ * A leaf's issuer must be byte-identical to its CA's subject. Re-encoding "CN=…" from the string
105
+ * form would risk a different string type (PrintableString vs UTF8String) and break chain
106
+ * building against a CA we did not issue — including the one already trusted in the user's
107
+ * keychain — so the bytes are copied rather than rebuilt.
108
+ */
109
+ export function subjectDer(certPem) {
110
+ const der = new crypto.X509Certificate(certPem).raw;
111
+ const cert = readTlv(der, 0); // Certificate
112
+ const tbs = readTlv(der, cert.valueStart); // TBSCertificate
113
+ let p = tbs.valueStart;
114
+ if (der[p] === 0xa0)
115
+ p = readTlv(der, p).end; // [0] version, optional
116
+ p = readTlv(der, p).end; // serialNumber
117
+ p = readTlv(der, p).end; // signature
118
+ p = readTlv(der, p).end; // issuer
119
+ p = readTlv(der, p).end; // validity
120
+ const subject = readTlv(der, p);
121
+ return der.subarray(p, subject.end);
122
+ }
123
+ // ---- issuance ---------------------------------------------------------------------------
124
+ function pem(label, der) {
125
+ const b64 = der.toString("base64").replace(/(.{64})/g, "$1\n").replace(/\n$/, "");
126
+ return `-----BEGIN ${label}-----\n${b64}\n-----END ${label}-----\n`;
127
+ }
128
+ function newKeyPair() {
129
+ return crypto.generateKeyPairSync("rsa", { modulusLength: 2048 });
130
+ }
131
+ /** Serial numbers must be positive and unpredictable; 16 random bytes with the top bit cleared. */
132
+ function serial() {
133
+ const b = crypto.randomBytes(16);
134
+ b[0] = b[0] & 0x7f;
135
+ return b;
136
+ }
137
+ function signCert(tbs, caKey) {
138
+ const signature = crypto.sign("sha256", tbs, caKey);
139
+ return seq(tbs, sha256WithRsa(), bitString(signature));
140
+ }
141
+ function validity(days) {
142
+ const now = new Date();
143
+ // Backdate an hour so a client whose clock is slightly behind ours still accepts a fresh cert.
144
+ const notBefore = new Date(now.getTime() - 3600_000);
145
+ const notAfter = new Date(now.getTime() + days * 86_400_000);
146
+ return seq(utcTime(notBefore), utcTime(notAfter));
147
+ }
148
+ function tbsCertificate(opts) {
149
+ return seq(explicit(0, integer(2)), // v3
150
+ integer(serial()), sha256WithRsa(), opts.issuer, validity(opts.days), opts.subject, opts.spki, explicit(3, seq(...opts.extensions)));
151
+ }
152
+ const basicConstraintsCa = () => extension("2.5.29.19", true, seq(tlv(TAG.BOOLEAN, Buffer.from([0xff])), integer(0)));
153
+ /** cA defaults to FALSE, so an end-entity constraint is an empty SEQUENCE. */
154
+ const basicConstraintsLeaf = () => extension("2.5.29.19", true, seq());
155
+ /** keyCertSign (bit 5) + cRLSign (bit 6). */
156
+ const keyUsageCa = () => extension("2.5.29.15", true, bitString(Buffer.from([0x06]), 1));
157
+ /** digitalSignature (bit 0) + keyEncipherment (bit 2). */
158
+ const keyUsageLeaf = () => extension("2.5.29.15", true, bitString(Buffer.from([0xa0]), 5));
159
+ const extendedKeyUsageServer = () => extension("2.5.29.37", false, seq(oid("1.3.6.1.5.5.7.3.1")));
160
+ /** SHA-1 of the public key BIT STRING, per RFC 5280's first method. */
161
+ function subjectKeyIdentifier(spki) {
162
+ const outer = readTlv(spki, 0);
163
+ let p = outer.valueStart;
164
+ p = readTlv(spki, p).end; // algorithm
165
+ const keyBits = readTlv(spki, p);
166
+ const hash = crypto.createHash("sha1").update(spki.subarray(keyBits.valueStart + 1, keyBits.end)).digest();
167
+ return extension("2.5.29.14", false, tlv(TAG.OCTET_STRING, hash));
168
+ }
169
+ /** dNSName is [2] IMPLICIT IA5String inside GeneralNames. */
170
+ function subjectAltName(hosts) {
171
+ return extension("2.5.29.17", false, seq(...hosts.map((h) => tlv(0x82, Buffer.from(h, "ascii")))));
172
+ }
173
+ function keyPem(key) {
174
+ return key.export({ format: "pem", type: "pkcs8" }).toString();
175
+ }
176
+ /** Self-signed CA. Not installed anywhere by this module; the caller decides what to trust it in. */
177
+ export function createCa(opts = {}) {
178
+ const { publicKey, privateKey } = newKeyPair();
179
+ const spki = publicKey.export({ format: "der", type: "spki" });
180
+ const name = nameWithCn(opts.cn ?? "ClaudeRipple local CA");
181
+ const tbs = tbsCertificate({
182
+ subject: name,
183
+ issuer: name,
184
+ spki,
185
+ days: opts.days ?? 3650,
186
+ extensions: [basicConstraintsCa(), keyUsageCa(), subjectKeyIdentifier(spki)],
187
+ });
188
+ return { certPem: pem("CERTIFICATE", signCert(tbs, privateKey)), keyPem: keyPem(privateKey) };
189
+ }
190
+ /** serverAuth certificate for `host`, signed by an existing CA (ours or one already trusted). */
191
+ export function createLeaf(opts) {
192
+ const { publicKey, privateKey } = newKeyPair();
193
+ const spki = publicKey.export({ format: "der", type: "spki" });
194
+ const tbs = tbsCertificate({
195
+ subject: nameWithCn(opts.host),
196
+ issuer: subjectDer(opts.caCertPem),
197
+ spki,
198
+ days: opts.days ?? 825,
199
+ extensions: [basicConstraintsLeaf(), keyUsageLeaf(), extendedKeyUsageServer(), subjectAltName([opts.host])],
200
+ });
201
+ const caKey = crypto.createPrivateKey(opts.caKeyPem);
202
+ return { certPem: pem("CERTIFICATE", signCert(tbs, caKey)), keyPem: keyPem(privateKey) };
203
+ }