sliftutils 1.0.2 → 1.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.
@@ -0,0 +1,18 @@
1
+ /** NOTE: We also generate the domain *.domain */
2
+ export declare const getHTTPSCert: {
3
+ (key: string): Promise<{
4
+ key: string;
5
+ cert: string;
6
+ }>;
7
+ clear(key: string): void;
8
+ clearAll(): void;
9
+ forceSet(key: string, value: Promise<{
10
+ key: string;
11
+ cert: string;
12
+ }>): void;
13
+ getAllKeys(): string[];
14
+ get(key: string): Promise<{
15
+ key: string;
16
+ cert: string;
17
+ }> | undefined;
18
+ };
@@ -0,0 +1,184 @@
1
+
2
+ import { addRecord, deleteRecord, getRecords, setRecord } from "./dns";
3
+ import { cache, lazy } from "socket-function/src/caching";
4
+ import * as forge from "node-forge";
5
+ import acme from "acme-client";
6
+ import { magenta, red } from "socket-function/src/formatting/logColors";
7
+ import { formatDateTime, formatTime } from "socket-function/src/formatting/format";
8
+ import { timeInMinute } from "socket-function/src/misc";
9
+ import { delay } from "socket-function/src/batching";
10
+ import fs from "fs";
11
+ import { getKeyStore } from "./persistentLocalStorage";
12
+
13
+
14
+ // Expire EXPIRATION_THRESHOLD% of the way through the certificate's lifetime
15
+ const EXPIRATION_THRESHOLD = 0.4;
16
+
17
+ /** NOTE: We also generate the domain *.domain */
18
+ export const getHTTPSCert = cache(async (domain: string): Promise<{ key: string; cert: string }> => {
19
+ if (!domain.endsWith(".")) {
20
+ domain = domain + ".";
21
+ }
22
+ let keyCert: { key: string; cert: string } | undefined;
23
+ let path = domain + ".cert";
24
+
25
+ try {
26
+ keyCert = JSON.parse(fs.readFileSync(path, "utf8")) as { key: string; cert: string };
27
+ } catch { }
28
+ if (keyCert) {
29
+ // If 40% of the lifetime has passed, renew it (has to be < the threshold
30
+ // in EdgeCertController).
31
+ let certObj = parseCert(keyCert.cert);
32
+ let expirationTime = +new Date(certObj.validity.notAfter);
33
+ let createTime = +new Date(certObj.validity.notBefore);
34
+ let renewDate = createTime + (expirationTime - createTime) * EXPIRATION_THRESHOLD;
35
+ if (renewDate < Date.now()) {
36
+ console.log(magenta(`Renewing domain ${domain} (renew target is ${formatDateTime(renewDate)}).`));
37
+ keyCert = undefined;
38
+ }
39
+ } else {
40
+ console.log(magenta(`No cert found for domain ${domain}, generating shortly.`));
41
+ }
42
+ if (keyCert) {
43
+ return keyCert;
44
+ }
45
+
46
+ const accountKey = await getAccountKey(domain);
47
+ let altDomains: string[] = [];
48
+
49
+ // altDomains.push("noproxy." + domain);
50
+ // // NOTE: Allowing local access is just an optimization, not to avoid having to forward ports
51
+ // // (unless you type 127-0-0-1.domain into the browser... then I guess you don't have to forward ports?)
52
+ // altDomains.push("127-0-0-1." + domain);
53
+
54
+ // NOTE: I forget why we were not allowing wildcard domains. I think it was to prevent
55
+ // any HTTPS domains from impersonating servers. But... servers have two levels, so that isn't
56
+ // an issue. And even if they didn't they store their public key in their domain, so you
57
+ // can't really impersonate them anyways...
58
+ // - AND, we need this for IP type A records, which... we need to pick the server we want
59
+ // to connect to.
60
+ altDomains.push("*." + domain);
61
+
62
+ try {
63
+ keyCert = await generateCert({ accountKey, domain, altDomains });
64
+ } catch (e) {
65
+ if (String(e).includes("authorization must be pending")) {
66
+ console.log(`Authorization appears to be pending, waiting 2 minutes for other process to create certificate`);
67
+ await delay(timeInMinute * 2);
68
+ return await getHTTPSCert(domain);
69
+ }
70
+ throw e;
71
+ }
72
+ await fs.promises.writeFile(path, JSON.stringify(keyCert));
73
+ return keyCert;
74
+ });
75
+
76
+
77
+ const getAccountKey = async function getAccountKey(domain: string) {
78
+ let accountKey = getKeyStore<string>(domain, "letsEncryptAccountKey");
79
+ let secret = await accountKey.get();
80
+ if (!secret) {
81
+ // Should only HAPPEN ONCE, EVER!
82
+ console.error(red(`Generating new letsencrypt account key`));
83
+ const keyPair = forge.pki.rsa.generateKeyPair();
84
+ secret = forge.pki.privateKeyToPem(keyPair.privateKey);
85
+ await accountKey.set(secret);
86
+ }
87
+ return secret;
88
+ };
89
+
90
+
91
+ function parseCert(PEMorDER: string | Buffer) {
92
+ return forge.pki.certificateFromPem(normalizeCertToPEM(PEMorDER));
93
+ }
94
+
95
+ function normalizeCertToPEM(PEMorDER: string | Buffer): string {
96
+ if (PEMorDER.toString().startsWith("-----BEGIN CERTIFICATE-----")) {
97
+ return PEMorDER.toString();
98
+ }
99
+ PEMorDER = PEMorDER.toString("base64");
100
+ return "-----BEGIN CERTIFICATE-----\n" + PEMorDER + "\n-----END CERTIFICATE-----";
101
+ }
102
+
103
+
104
+ async function generateCert(config: {
105
+ accountKey: string;
106
+ domain: string;
107
+ altDomains?: string[];
108
+ }): Promise<{
109
+ domains: string[];
110
+ key: string;
111
+ cert: string;
112
+ }> {
113
+ let { accountKey, domain } = config;
114
+
115
+ console.log(magenta(`Generating new cert for ${domain}`));
116
+
117
+ let domainList = [domain, ...config.altDomains || []];
118
+ // Strip trailing "."
119
+ domainList = domainList.map(x => x.endsWith(".") ? x.slice(0, -1) : x);
120
+
121
+ const [certificateKey, certificateCsr] = await acme.forge.createCsr({
122
+ commonName: domainList[0],
123
+ altNames: domainList.slice(1),
124
+ });
125
+
126
+ // So... acme-client is fine. Just re-implement the "auto" mode ourselves, to have more control over it.
127
+ const client = new acme.Client({
128
+ directoryUrl: acme.directory.letsencrypt.production,
129
+ accountKey: accountKey,
130
+ });
131
+
132
+ const accountPayload = {
133
+ termsOfServiceAgreed: true,
134
+ contact: [`mailto:devops@perspectanalytics.com`],
135
+ };
136
+
137
+ try {
138
+ await client.getAccountUrl();
139
+ } catch {
140
+ await client.createAccount(accountPayload);
141
+ }
142
+
143
+ const orderPayload = {
144
+ identifiers: domainList.map(domain => ({ type: "dns", value: domain })),
145
+ };
146
+ const order = await client.createOrder(orderPayload);
147
+ const authorizations = await client.getAuthorizations(order);
148
+ console.log(`Starting authorizations: ${JSON.stringify(authorizations)}`);
149
+
150
+ for (let auth of authorizations) {
151
+ if (auth.status === "valid") {
152
+ console.log(`Authorization already valid for ${auth.identifier.value}`);
153
+ continue;
154
+ }
155
+ console.log(`Starting authorization for ${JSON.stringify(auth)}`);
156
+
157
+ // Only use DNS authorization
158
+ let challenge = auth.challenges.find(x => x.type === "dns-01");
159
+ if (!challenge) {
160
+ throw new Error("No DNS challenge found");
161
+ }
162
+ const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
163
+
164
+ let hostname = auth.identifier.value;
165
+ let challengeRecordName = "_acme-challenge." + hostname + ".";
166
+ await setRecord("TXT", challengeRecordName, keyAuthorization);
167
+
168
+ await client.completeChallenge(challenge);
169
+ console.log(`Challenge completed`);
170
+
171
+ await client.waitForValidStatus(challenge);
172
+ console.log(`Status of order is valid`);
173
+ }
174
+
175
+ const finalized = await client.finalizeOrder(order, certificateCsr);
176
+ console.log(`Order finalized`);
177
+
178
+ let cert = await client.getCertificate(finalized);
179
+ return {
180
+ domains: domainList,
181
+ key: certificateKey.toString(),
182
+ cert: cert,
183
+ };
184
+ }
@@ -0,0 +1,17 @@
1
+
2
+
3
+ declare module "node-forge" {
4
+ declare type Ed25519PublicKey = {
5
+ publicKeyBytes: Buffer;
6
+ } & Buffer;
7
+ declare type Ed25519PrivateKey = {
8
+ privateKeyBytes: Buffer;
9
+ } & Buffer;
10
+ class ed25519 {
11
+ static generateKeyPair(): { publicKey: Ed25519PublicKey, privateKey: Ed25519PrivateKey };
12
+ static privateKeyToPem(key: Ed25519PrivateKey): string;
13
+ static privateKeyFromPem(pem: string): Ed25519PrivateKey;
14
+ static publicKeyToPem(key: Ed25519PublicKey): string;
15
+ static publicKeyFromPem(pem: string): Ed25519PublicKey;
16
+ }
17
+ }
@@ -0,0 +1,5 @@
1
+ import { MaybePromise } from "socket-function/src/types";
2
+ export declare function getKeyStore<T>(appName: string, key: string): {
3
+ get(): MaybePromise<T | undefined>;
4
+ set(value: T | null): MaybePromise<void>;
5
+ };
@@ -0,0 +1,36 @@
1
+ import { isNode } from "socket-function/src/misc";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import { MaybePromise } from "socket-function/src/types";
5
+ import { cache } from "socket-function/src/caching";
6
+
7
+ export function getKeyStore<T>(appName: string, key: string): {
8
+ get(): MaybePromise<T | undefined>;
9
+ set(value: T | null): MaybePromise<void>;
10
+ } {
11
+ if (isNode()) {
12
+ let path = os.homedir() + `/keystore_${appName}_` + key + ".json";
13
+ return {
14
+ get() {
15
+ let contents: string | undefined = undefined;
16
+ try { contents = fs.readFileSync(path, "utf8"); } catch { }
17
+ if (!contents) return undefined;
18
+ return JSON.parse(contents) as T;
19
+ },
20
+ set(value: T | null) {
21
+ fs.writeFileSync(path, JSON.stringify(value));
22
+ }
23
+ };
24
+ } else {
25
+ return {
26
+ get() {
27
+ let json = localStorage.getItem(key);
28
+ if (!json) return undefined;
29
+ return JSON.parse(json) as T;
30
+ },
31
+ set(value: T | null) {
32
+ localStorage.setItem(key, JSON.stringify(value));
33
+ }
34
+ };
35
+ }
36
+ }
@@ -29,7 +29,7 @@ export declare function yamlOpenRouterCall<T>(config: {
29
29
  validate?: (response: T) => void;
30
30
  }): Promise<T>;
31
31
  export declare function simpleAICall(model: string, message: string): Promise<string>;
32
- /** The message must request the result to be returned in YAML. */
32
+ /** The message must request the result to be returned in YAML (we automatically parse this and return an object). */
33
33
  export declare function simpleAICallTyped<T>(model: string, message: string): Promise<T>;
34
34
  export declare function openRouterCall(config: {
35
35
  model: string;
@@ -58,7 +58,7 @@ export async function simpleAICall(model: string, message: string): Promise<stri
58
58
  messages: [{ role: "user", content: message }],
59
59
  });
60
60
  }
61
- /** The message must request the result to be returned in YAML. */
61
+ /** The message must request the result to be returned in YAML (we automatically parse this and return an object). */
62
62
  export async function simpleAICallTyped<T>(model: string, message: string): Promise<T> {
63
63
  return await yamlOpenRouterCall({
64
64
  model,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -43,7 +43,9 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@types/chrome": "^0.0.237",
46
+ "@types/node-forge": "^1.3.11",
46
47
  "@types/shell-quote": "^1.7.5",
48
+ "acme-client": "^5.0.0",
47
49
  "js-sha256": "^0.11.1",
48
50
  "mobx": "^6.13.3",
49
51
  "preact-old-types": "^10.28.1",
@@ -25,7 +25,8 @@ export class Anchor extends preact.Component<{
25
25
  <a
26
26
  {...remaining}
27
27
  className={
28
- css.textDecoration("none")
28
+ "Anchor "
29
+ + css.textDecoration("none")
29
30
  .opacity(0.8, "hover")
30
31
  + (selected && css.color("hsl(110, 75%, 65%)", "soft"))
31
32
  + (!selected && css.color("hsl(210, 75%, 65%)", "soft"))
package/web/Page.tsx CHANGED
@@ -48,7 +48,7 @@ export class Page extends preact.Component {
48
48
 
49
49
  return (
50
50
  <div className={css.size("100vw", "100vh").vbox(0)}>
51
- <div className={css.hbox(12).pad2(20, 0)}>
51
+ <div className={css.hbox(12).pad2(20, 10).paddingBottom(5, "important")}>
52
52
  {pages.map(p => (
53
53
  <Anchor key={p.key} params={[[pageURL, p.key]]}>
54
54
  {p.key}
package/web/browser.tsx CHANGED
@@ -6,7 +6,7 @@ import { list } from "socket-function/src/misc";
6
6
  import { enableHotReloading } from "../builders/hotReload";
7
7
  import { URLParam } from "../render-utils/URLParam";
8
8
  import { Page } from "./Page";
9
- import { configureMobxNextFrameScheduler } from "sliftutils/render-utils/mobxTyped";
9
+ import { configureMobxNextFrameScheduler } from "../render-utils/mobxTyped";
10
10
 
11
11
 
12
12
  async function main() {
package/web/index.html CHANGED
@@ -23,6 +23,9 @@
23
23
  margin: 0px;
24
24
  padding: 0px;
25
25
  }
26
+ a:not(.Anchor), a:visited:not(.Anchor) {
27
+ color: hsl(210, 75%, 65%);
28
+ }
26
29
  </style>
27
30
  </head>
28
31
  <body>