@hashsome/runtime 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.
- package/LICENSE +674 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/app/entry.client.d.ts +1 -0
- package/dist/app/entry.client.js +7 -0
- package/dist/app/entry.client.js.map +1 -0
- package/dist/app/entry.server.d.ts +2 -0
- package/dist/app/entry.server.js +20 -0
- package/dist/app/entry.server.js.map +1 -0
- package/dist/app/index.d.ts +1 -0
- package/dist/app/index.js +1 -0
- package/dist/app/index.js.map +1 -0
- package/dist/app/root.d.ts +18 -0
- package/dist/app/root.js +36 -0
- package/dist/app/root.js.map +1 -0
- package/dist/bin/hashsome.d.ts +2 -0
- package/dist/bin/hashsome.js +17 -0
- package/dist/bin/hashsome.js.map +1 -0
- package/dist/src/cli/build.d.ts +4 -0
- package/dist/src/cli/build.js +18 -0
- package/dist/src/cli/build.js.map +1 -0
- package/dist/src/cli/dev.d.ts +1 -0
- package/dist/src/cli/dev.js +19 -0
- package/dist/src/cli/dev.js.map +1 -0
- package/dist/src/cli/package.d.ts +13 -0
- package/dist/src/cli/package.js +115 -0
- package/dist/src/cli/package.js.map +1 -0
- package/dist/src/cli/start.d.ts +1 -0
- package/dist/src/cli/start.js +12 -0
- package/dist/src/cli/start.js.map +1 -0
- package/dist/src/config.d.ts +36 -0
- package/dist/src/config.js +54 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +8 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/release/bundle.d.ts +10 -0
- package/dist/src/release/bundle.js +50 -0
- package/dist/src/release/bundle.js.map +1 -0
- package/dist/src/release/package.d.ts +26 -0
- package/dist/src/release/package.js +102 -0
- package/dist/src/release/package.js.map +1 -0
- package/dist/src/release/render.d.ts +32 -0
- package/dist/src/release/render.js +335 -0
- package/dist/src/release/render.js.map +1 -0
- package/dist/src/serve.d.ts +8 -0
- package/dist/src/serve.js +14 -0
- package/dist/src/serve.js.map +1 -0
- package/dist/src/server/assets.d.ts +10 -0
- package/dist/src/server/assets.js +46 -0
- package/dist/src/server/assets.js.map +1 -0
- package/dist/src/server/integrations.d.ts +12 -0
- package/dist/src/server/integrations.js +39 -0
- package/dist/src/server/integrations.js.map +1 -0
- package/dist/src/server/production.d.ts +7 -0
- package/dist/src/server/production.js +45 -0
- package/dist/src/server/production.js.map +1 -0
- package/dist/src/server/proxy.d.ts +34 -0
- package/dist/src/server/proxy.js +165 -0
- package/dist/src/server/proxy.js.map +1 -0
- package/dist/src/server/websocket.d.ts +5 -0
- package/dist/src/server/websocket.js +17 -0
- package/dist/src/server/websocket.js.map +1 -0
- package/dist/src/vite-config.d.ts +12 -0
- package/dist/src/vite-config.js +35 -0
- package/dist/src/vite-config.js.map +1 -0
- package/dist/src/vite.d.ts +5 -0
- package/dist/src/vite.js +42 -0
- package/dist/src/vite.js.map +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/** The files of a release, as text. Pure: `package.ts` writes them out. */
|
|
2
|
+
export const TARGETS = ['plain', 'compose', 'helm', 'image'];
|
|
3
|
+
export const imageRef = ({ name, tag }) => `${name}:${tag}`;
|
|
4
|
+
/** A project name made safe for an image, a service and a chart. */
|
|
5
|
+
export function safeName(raw) {
|
|
6
|
+
const name = raw
|
|
7
|
+
.replace(/^@[^/]+\//, '')
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
10
|
+
.replace(/^-+|-+$/g, '');
|
|
11
|
+
return name || 'hashsome';
|
|
12
|
+
}
|
|
13
|
+
export function dockerfile({ port }) {
|
|
14
|
+
return `# The release's server and client: nothing is installed or compiled here.
|
|
15
|
+
FROM node:24-slim
|
|
16
|
+
ENV NODE_ENV=production PORT=${port}
|
|
17
|
+
WORKDIR /app
|
|
18
|
+
COPY server.mjs ./
|
|
19
|
+
COPY client ./client
|
|
20
|
+
USER node
|
|
21
|
+
EXPOSE ${port}
|
|
22
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/healthz').then((r)=>process.exit(r.ok?0:1),()=>process.exit(1))"
|
|
23
|
+
CMD ["node", "server.mjs"]
|
|
24
|
+
`;
|
|
25
|
+
}
|
|
26
|
+
export function plainReadme(info) {
|
|
27
|
+
return `# ${info.name}: plain
|
|
28
|
+
|
|
29
|
+
The whole app as files: \`server.mjs\` (the server, with your config and integrations inside it) and
|
|
30
|
+
\`client/\` (your dashboards). Run it anywhere Node 24 or newer is installed, no \`npm install\`:
|
|
31
|
+
|
|
32
|
+
\`\`\`sh
|
|
33
|
+
HA_URL=... HA_TOKEN=... node server.mjs
|
|
34
|
+
\`\`\`
|
|
35
|
+
|
|
36
|
+
It listens on \`PORT\` (default ${info.port}) and \`HOST\` (default 0.0.0.0). Everything the config
|
|
37
|
+
reads from the environment (tokens and addresses) is set when it runs; none is in these files.
|
|
38
|
+
\`/healthz\` answers while it is up.
|
|
39
|
+
|
|
40
|
+
The static \`client/\` alone is not enough: the server keeps the tokens, talks to your backends and
|
|
41
|
+
serves their artwork, and the browser only ever talks to it.
|
|
42
|
+
|
|
43
|
+
If your backend uses a private certificate authority, give Node its certificate:
|
|
44
|
+
\`NODE_EXTRA_CA_CERTS=/path/to/ca.crt node server.mjs\`.
|
|
45
|
+
|
|
46
|
+
\`Dockerfile\` is the one \`compose\` and \`helm\` use: \`docker build -t ${info.name} .\` here builds the
|
|
47
|
+
image yourself.
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
export function composeFile(info) {
|
|
51
|
+
return `# docker compose up -d (after: docker load -i ../image.tar, and a .env next to this file)
|
|
52
|
+
services:
|
|
53
|
+
${info.name}:
|
|
54
|
+
image: ${imageRef(info)}
|
|
55
|
+
restart: unless-stopped
|
|
56
|
+
ports:
|
|
57
|
+
- "${info.port}:${info.port}"
|
|
58
|
+
# Your tokens and addresses, read at runtime. Never baked into the image.
|
|
59
|
+
env_file: .env
|
|
60
|
+
# A private certificate authority for your backend:
|
|
61
|
+
# environment:
|
|
62
|
+
# NODE_EXTRA_CA_CERTS: /certs/ca.crt
|
|
63
|
+
# volumes:
|
|
64
|
+
# - ./ca.crt:/certs/ca.crt:ro
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
export function composeReadme(info) {
|
|
68
|
+
return `# ${info.name}: Docker Compose
|
|
69
|
+
|
|
70
|
+
1. Load the image: \`docker load -i ../image.tar\`${info.image ? '' : ' (this release has no image: build one from a `plain` release, `docker build plain`)'}
|
|
71
|
+
2. Create \`.env\` here with your tokens and addresses (see \`.env.example\`)
|
|
72
|
+
3. \`docker compose up -d\`
|
|
73
|
+
|
|
74
|
+
Update by loading the next release's image and changing the tag in \`compose.yaml\`.
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
export function envExample(info) {
|
|
78
|
+
return (info.envExample ??
|
|
79
|
+
'# The variables your hashsome.config.ts reads, for example:\n# HA_URL=\n# HA_TOKEN=\n');
|
|
80
|
+
}
|
|
81
|
+
// ── Helm ─────────────────────────────────────────────────────────────────────────────────────────
|
|
82
|
+
export function chartYaml(info) {
|
|
83
|
+
return `apiVersion: v2
|
|
84
|
+
name: ${info.name}
|
|
85
|
+
description: ${info.name}, a Hashsome dashboard server
|
|
86
|
+
type: application
|
|
87
|
+
version: 0.1.0
|
|
88
|
+
appVersion: "${info.tag}"
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
export function chartValues(info) {
|
|
92
|
+
return `# The image is loaded onto the node from image.tar (see import-image.sh), not pulled, hence Never.
|
|
93
|
+
image:
|
|
94
|
+
repository: ${info.name}
|
|
95
|
+
tag: "${info.tag}"
|
|
96
|
+
pullPolicy: Never
|
|
97
|
+
|
|
98
|
+
port: ${info.port}
|
|
99
|
+
|
|
100
|
+
# Your tokens and addresses, in a Secret you create (see NOTES). The chart never holds their values.
|
|
101
|
+
existingSecret: ""
|
|
102
|
+
|
|
103
|
+
# Extra environment variables that are not secret.
|
|
104
|
+
env: {}
|
|
105
|
+
|
|
106
|
+
# A private certificate authority for your backend: a ConfigMap holding the certificate.
|
|
107
|
+
caCert:
|
|
108
|
+
configMap: ""
|
|
109
|
+
key: ca.crt
|
|
110
|
+
|
|
111
|
+
service:
|
|
112
|
+
type: ClusterIP
|
|
113
|
+
port: 80
|
|
114
|
+
|
|
115
|
+
ingress:
|
|
116
|
+
enabled: false
|
|
117
|
+
className: traefik
|
|
118
|
+
host: ""
|
|
119
|
+
# A TLS secret for the host; leave empty for plain http.
|
|
120
|
+
tlsSecret: ""
|
|
121
|
+
annotations: {}
|
|
122
|
+
|
|
123
|
+
resources: {}
|
|
124
|
+
nodeSelector: {}
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
export function chartHelpers() {
|
|
128
|
+
return `{{- define "app.name" -}}{{ .Release.Name }}{{- end -}}
|
|
129
|
+
{{- define "app.labels" -}}
|
|
130
|
+
app.kubernetes.io/name: {{ .Chart.Name }}
|
|
131
|
+
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
132
|
+
{{- end -}}
|
|
133
|
+
{{- define "app.selector" -}}
|
|
134
|
+
app.kubernetes.io/name: {{ .Chart.Name }}
|
|
135
|
+
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
136
|
+
{{- end -}}
|
|
137
|
+
`;
|
|
138
|
+
}
|
|
139
|
+
export function chartDeployment() {
|
|
140
|
+
return `apiVersion: apps/v1
|
|
141
|
+
kind: Deployment
|
|
142
|
+
metadata:
|
|
143
|
+
name: {{ include "app.name" . }}
|
|
144
|
+
labels:
|
|
145
|
+
{{- include "app.labels" . | nindent 4 }}
|
|
146
|
+
spec:
|
|
147
|
+
# One replica, and the old pod goes before the new one starts: the server holds live
|
|
148
|
+
# connections to your backends, and two at once would just double them.
|
|
149
|
+
replicas: 1
|
|
150
|
+
strategy:
|
|
151
|
+
type: Recreate
|
|
152
|
+
selector:
|
|
153
|
+
matchLabels:
|
|
154
|
+
{{- include "app.selector" . | nindent 6 }}
|
|
155
|
+
template:
|
|
156
|
+
metadata:
|
|
157
|
+
labels:
|
|
158
|
+
{{- include "app.selector" . | nindent 8 }}
|
|
159
|
+
spec:
|
|
160
|
+
containers:
|
|
161
|
+
- name: app
|
|
162
|
+
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
|
163
|
+
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
|
164
|
+
ports:
|
|
165
|
+
- name: http
|
|
166
|
+
containerPort: {{ .Values.port }}
|
|
167
|
+
envFrom:
|
|
168
|
+
- secretRef:
|
|
169
|
+
name: {{ required "set existingSecret to the Secret holding your tokens" .Values.existingSecret }}
|
|
170
|
+
env:
|
|
171
|
+
- name: PORT
|
|
172
|
+
value: {{ .Values.port | quote }}
|
|
173
|
+
{{- if .Values.caCert.configMap }}
|
|
174
|
+
- name: NODE_EXTRA_CA_CERTS
|
|
175
|
+
value: /certs/{{ .Values.caCert.key }}
|
|
176
|
+
{{- end }}
|
|
177
|
+
{{- range $name, $value := .Values.env }}
|
|
178
|
+
- name: {{ $name }}
|
|
179
|
+
value: {{ $value | quote }}
|
|
180
|
+
{{- end }}
|
|
181
|
+
readinessProbe:
|
|
182
|
+
httpGet:
|
|
183
|
+
path: /healthz
|
|
184
|
+
port: http
|
|
185
|
+
livenessProbe:
|
|
186
|
+
httpGet:
|
|
187
|
+
path: /healthz
|
|
188
|
+
port: http
|
|
189
|
+
initialDelaySeconds: 10
|
|
190
|
+
{{- with .Values.resources }}
|
|
191
|
+
resources:
|
|
192
|
+
{{- toYaml . | nindent 12 }}
|
|
193
|
+
{{- end }}
|
|
194
|
+
{{- if .Values.caCert.configMap }}
|
|
195
|
+
volumeMounts:
|
|
196
|
+
- name: ca
|
|
197
|
+
mountPath: /certs
|
|
198
|
+
readOnly: true
|
|
199
|
+
{{- end }}
|
|
200
|
+
{{- if .Values.caCert.configMap }}
|
|
201
|
+
volumes:
|
|
202
|
+
- name: ca
|
|
203
|
+
configMap:
|
|
204
|
+
name: {{ .Values.caCert.configMap }}
|
|
205
|
+
{{- end }}
|
|
206
|
+
{{- with .Values.nodeSelector }}
|
|
207
|
+
nodeSelector:
|
|
208
|
+
{{- toYaml . | nindent 8 }}
|
|
209
|
+
{{- end }}
|
|
210
|
+
`;
|
|
211
|
+
}
|
|
212
|
+
export function chartService() {
|
|
213
|
+
return `apiVersion: v1
|
|
214
|
+
kind: Service
|
|
215
|
+
metadata:
|
|
216
|
+
name: {{ include "app.name" . }}
|
|
217
|
+
labels:
|
|
218
|
+
{{- include "app.labels" . | nindent 4 }}
|
|
219
|
+
spec:
|
|
220
|
+
type: {{ .Values.service.type }}
|
|
221
|
+
selector:
|
|
222
|
+
{{- include "app.selector" . | nindent 4 }}
|
|
223
|
+
ports:
|
|
224
|
+
- name: http
|
|
225
|
+
port: {{ .Values.service.port }}
|
|
226
|
+
targetPort: http
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
export function chartIngress() {
|
|
230
|
+
return `{{- if .Values.ingress.enabled }}
|
|
231
|
+
apiVersion: networking.k8s.io/v1
|
|
232
|
+
kind: Ingress
|
|
233
|
+
metadata:
|
|
234
|
+
name: {{ include "app.name" . }}
|
|
235
|
+
labels:
|
|
236
|
+
{{- include "app.labels" . | nindent 4 }}
|
|
237
|
+
{{- with .Values.ingress.annotations }}
|
|
238
|
+
annotations:
|
|
239
|
+
{{- toYaml . | nindent 4 }}
|
|
240
|
+
{{- end }}
|
|
241
|
+
spec:
|
|
242
|
+
{{- if .Values.ingress.className }}
|
|
243
|
+
ingressClassName: {{ .Values.ingress.className }}
|
|
244
|
+
{{- end }}
|
|
245
|
+
{{- if .Values.ingress.tlsSecret }}
|
|
246
|
+
tls:
|
|
247
|
+
- hosts:
|
|
248
|
+
- {{ required "set ingress.host" .Values.ingress.host }}
|
|
249
|
+
secretName: {{ .Values.ingress.tlsSecret }}
|
|
250
|
+
{{- end }}
|
|
251
|
+
rules:
|
|
252
|
+
- host: {{ required "set ingress.host" .Values.ingress.host }}
|
|
253
|
+
http:
|
|
254
|
+
paths:
|
|
255
|
+
- path: /
|
|
256
|
+
pathType: Prefix
|
|
257
|
+
backend:
|
|
258
|
+
service:
|
|
259
|
+
name: {{ include "app.name" . }}
|
|
260
|
+
port:
|
|
261
|
+
name: http
|
|
262
|
+
{{- end }}
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
265
|
+
export function chartNotes() {
|
|
266
|
+
return `{{ .Release.Name }} is deployed.
|
|
267
|
+
|
|
268
|
+
Your tokens and addresses come from the Secret "{{ .Values.existingSecret }}". Create it once, with the
|
|
269
|
+
variables your hashsome.config.ts reads (it is not part of this chart, so it is not in git):
|
|
270
|
+
|
|
271
|
+
kubectl create secret generic {{ .Values.existingSecret }} -n {{ .Release.Namespace }} \\
|
|
272
|
+
--from-literal=HA_URL=https://... --from-literal=HA_TOKEN=...
|
|
273
|
+
|
|
274
|
+
To change a token later, update the Secret and run: kubectl rollout restart deploy/{{ include "app.name" . }} -n {{ .Release.Namespace }}
|
|
275
|
+
{{- if .Values.ingress.enabled }}
|
|
276
|
+
|
|
277
|
+
It is served at http{{ if .Values.ingress.tlsSecret }}s{{ end }}://{{ .Values.ingress.host }}/
|
|
278
|
+
{{- end }}
|
|
279
|
+
`;
|
|
280
|
+
}
|
|
281
|
+
export function importScript(info) {
|
|
282
|
+
return `#!/bin/sh
|
|
283
|
+
# Loads the release's image into k3s's containerd on this machine. Run it on the server, from here.
|
|
284
|
+
set -eu
|
|
285
|
+
cd "$(dirname "$0")"
|
|
286
|
+
sudo k3s ctr -n k8s.io images import ../image.tar
|
|
287
|
+
echo "Imported ${imageRef(info)}"
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
export function helmReadme(info) {
|
|
291
|
+
return `# ${info.name}: Helm (k3s)
|
|
292
|
+
|
|
293
|
+
This folder is a Helm chart. Put it in your cluster's folder and add it to your helmfile.
|
|
294
|
+
|
|
295
|
+
1. On the server, load the image into k3s: \`./import-image.sh\`
|
|
296
|
+
(it imports \`../image.tar\`, so keep the release folder together, or edit the path)
|
|
297
|
+
2. Create the Secret with your tokens, once (see \`templates/NOTES.txt\`):
|
|
298
|
+
\`kubectl create secret generic ${info.name}-secrets --from-literal=HA_URL=... --from-literal=HA_TOKEN=...\`
|
|
299
|
+
3. Add a release to your helmfile:
|
|
300
|
+
|
|
301
|
+
\`\`\`yaml
|
|
302
|
+
releases:
|
|
303
|
+
- name: ${info.name}
|
|
304
|
+
chart: ./${info.name} # this folder
|
|
305
|
+
values:
|
|
306
|
+
- existingSecret: ${info.name}-secrets
|
|
307
|
+
ingress:
|
|
308
|
+
enabled: true
|
|
309
|
+
host: ${info.name}.lan
|
|
310
|
+
\`\`\`
|
|
311
|
+
|
|
312
|
+
4. \`helmfile apply\`
|
|
313
|
+
|
|
314
|
+
The image tag is \`${info.tag}\` (in \`values.yaml\`). A new release has a new tag, so k3s always picks
|
|
315
|
+
up the update. Built for \`${info.platform}\`; \`pullPolicy\` is \`Never\` because the image is imported,
|
|
316
|
+
not pulled.
|
|
317
|
+
`;
|
|
318
|
+
}
|
|
319
|
+
export function releaseReadme(info, targets) {
|
|
320
|
+
const lines = {
|
|
321
|
+
plain: '- `plain/`: the server bundle and the client. Runs with just Node, or builds into an image.',
|
|
322
|
+
compose: '- `compose/`: Docker Compose, for a machine running Docker.',
|
|
323
|
+
helm: '- `helm/`: a Helm chart for k3s/Kubernetes (loads the image, no registry).',
|
|
324
|
+
image: '- `image.tar`: just the container image, for your own manifests.',
|
|
325
|
+
};
|
|
326
|
+
return `# ${info.name} ${info.tag}
|
|
327
|
+
|
|
328
|
+
Built for \`${info.platform}\`. Deploy it as:
|
|
329
|
+
|
|
330
|
+
${targets.map((t) => lines[t]).join('\n')}
|
|
331
|
+
${info.image && !targets.includes('image') ? `- \`image.tar\`: the container image (\`${imageRef(info)}\`), for compose and helm.\n` : ''}
|
|
332
|
+
Your tokens and addresses are not in any of these files. They are set where it runs: an \`.env\`
|
|
333
|
+
file for compose, a Kubernetes Secret for helm, the environment for plain.
|
|
334
|
+
`;
|
|
335
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../../src/release/render.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type HashsomeConfig } from './config.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The entry of a packaged release: runs the server for an already-imported config, serving the
|
|
4
|
+
* client that sits next to the bundle (`./client`). `hashsome package` bundles this together with
|
|
5
|
+
* the project's `hashsome.config.ts` and its integrations into one file, so nothing else is installed
|
|
6
|
+
* where it runs.
|
|
7
|
+
*/
|
|
8
|
+
export declare function serve(user: HashsomeConfig, bundleUrl: string): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { resolveConfig } from './config.js';
|
|
4
|
+
import { startServer } from './server/production.js';
|
|
5
|
+
/**
|
|
6
|
+
* The entry of a packaged release: runs the server for an already-imported config, serving the
|
|
7
|
+
* client that sits next to the bundle (`./client`). `hashsome package` bundles this together with
|
|
8
|
+
* the project's `hashsome.config.ts` and its integrations into one file, so nothing else is installed
|
|
9
|
+
* where it runs.
|
|
10
|
+
*/
|
|
11
|
+
export function serve(user, bundleUrl) {
|
|
12
|
+
const here = dirname(fileURLToPath(bundleUrl));
|
|
13
|
+
startServer(resolveConfig(here, user), `${here}/client`);
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,aAAa,EAAuB,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAErD;;;;;GAKG;AACH,MAAM,UAAU,KAAK,CAAC,IAAoB,EAAE,SAAiB;IAC3D,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC;IAC/C,WAAW,CAAC,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,GAAG,IAAI,SAAS,CAAC,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { type Integration } from '@hashsome/core';
|
|
3
|
+
/**
|
|
4
|
+
* Serves `GET /_hashsome/asset/<integration>?path=<backend path>`: a file one of an integration's
|
|
5
|
+
* entities points at (artwork, a person's picture), fetched by the server with the integration's
|
|
6
|
+
* credentials so the browser never needs to reach, trust or log in to the backend. Only image
|
|
7
|
+
* responses are passed on. Returns `false` for a request that is not an asset request, so the
|
|
8
|
+
* caller can carry on.
|
|
9
|
+
*/
|
|
10
|
+
export declare function serveAsset(integrations: Integration[], request: Pick<IncomingMessage, 'method' | 'url'>, response: ServerResponse): Promise<boolean>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ASSET_PATH } from '@hashsome/core';
|
|
2
|
+
/** How long a browser may reuse a file; artwork URLs change when the picture does. */
|
|
3
|
+
const CACHE_CONTROL = 'private, max-age=3600';
|
|
4
|
+
/**
|
|
5
|
+
* Serves `GET /_hashsome/asset/<integration>?path=<backend path>`: a file one of an integration's
|
|
6
|
+
* entities points at (artwork, a person's picture), fetched by the server with the integration's
|
|
7
|
+
* credentials so the browser never needs to reach, trust or log in to the backend. Only image
|
|
8
|
+
* responses are passed on. Returns `false` for a request that is not an asset request, so the
|
|
9
|
+
* caller can carry on.
|
|
10
|
+
*/
|
|
11
|
+
export async function serveAsset(integrations, request, response) {
|
|
12
|
+
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
13
|
+
if (!url.pathname.startsWith(`${ASSET_PATH}/`)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const fail = (status, message) => {
|
|
17
|
+
response.statusCode = status;
|
|
18
|
+
response.setHeader('content-type', 'text/plain');
|
|
19
|
+
response.end(message);
|
|
20
|
+
return true;
|
|
21
|
+
};
|
|
22
|
+
if (request.method !== 'GET') {
|
|
23
|
+
return fail(405, 'Method not allowed');
|
|
24
|
+
}
|
|
25
|
+
const id = decodeURIComponent(url.pathname.slice(ASSET_PATH.length + 1));
|
|
26
|
+
const path = url.searchParams.get('path');
|
|
27
|
+
const integration = integrations.find((candidate) => candidate.id === id);
|
|
28
|
+
if (!integration?.fetchAsset || !path) {
|
|
29
|
+
return fail(404, 'No such asset');
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const upstream = await integration.fetchAsset(path);
|
|
33
|
+
const type = upstream.headers.get('content-type') ?? '';
|
|
34
|
+
if (!upstream.ok || !type.startsWith('image/')) {
|
|
35
|
+
return fail(upstream.ok ? 415 : 404, 'No such asset');
|
|
36
|
+
}
|
|
37
|
+
response.statusCode = 200;
|
|
38
|
+
response.setHeader('content-type', type);
|
|
39
|
+
response.setHeader('cache-control', CACHE_CONTROL);
|
|
40
|
+
response.end(Buffer.from(await upstream.arrayBuffer()));
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return fail(404, 'No such asset');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assets.js","sourceRoot":"","sources":["../../../src/server/assets.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAoB,MAAM,gBAAgB,CAAC;AAE9D,sFAAsF;AACtF,MAAM,aAAa,GAAG,uBAAuB,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,YAA2B,EAC3B,OAAgD,EAChD,QAAwB;IAExB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,IAAI,GAAG,EAAE,kBAAkB,CAAC,CAAC;IAC5D,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,UAAU,GAAG,CAAC,EAAE,CAAC;QAC/C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAc,EAAE,OAAe,EAAE,EAAE;QAC/C,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC;QAC7B,QAAQ,CAAC,SAAS,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;QACjD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACtB,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;IAEF,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;QAC7B,OAAO,IAAI,CAAC,GAAG,EAAE,oBAAoB,CAAC,CAAC;IACzC,CAAC;IAED,MAAM,EAAE,GAAG,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;IACzE,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,WAAW,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC1E,IAAI,CAAC,WAAW,EAAE,UAAU,IAAI,CAAC,IAAI,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;IACpC,CAAC;IAED,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;QACxD,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/C,OAAO,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;QACxD,CAAC;QAED,QAAQ,CAAC,UAAU,GAAG,GAAG,CAAC;QAC1B,QAAQ,CAAC,SAAS,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;QACzC,QAAQ,CAAC,SAAS,CAAC,eAAe,EAAE,aAAa,CAAC,CAAC;QACnD,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QACxD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;IACpC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Integration } from '@hashsome/core';
|
|
2
|
+
export interface StartOptions {
|
|
3
|
+
log?: (message: string) => void;
|
|
4
|
+
minDelayMs?: number;
|
|
5
|
+
maxDelayMs?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Connects every integration, retrying with exponential backoff until it
|
|
9
|
+
* succeeds. Never throws: an unreachable Home Assistant must not stop the
|
|
10
|
+
* server (dashboards show "unavailable" and recover).
|
|
11
|
+
*/
|
|
12
|
+
export declare function startIntegrations(integrations: Integration[], options?: StartOptions): () => void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connects every integration, retrying with exponential backoff until it
|
|
3
|
+
* succeeds. Never throws: an unreachable Home Assistant must not stop the
|
|
4
|
+
* server (dashboards show "unavailable" and recover).
|
|
5
|
+
*/
|
|
6
|
+
export function startIntegrations(integrations, options = {}) {
|
|
7
|
+
const log = options.log ?? (() => { });
|
|
8
|
+
const min = options.minDelayMs ?? 1000;
|
|
9
|
+
const max = options.maxDelayMs ?? 30_000;
|
|
10
|
+
const timers = new Set();
|
|
11
|
+
let stopped = false;
|
|
12
|
+
const attempt = (integration, retry) => {
|
|
13
|
+
if (stopped) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
integration.connect().then(() => log(`[${integration.id}] connected`), (error) => {
|
|
17
|
+
const delay = Math.min(max, min * 2 ** retry);
|
|
18
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
19
|
+
log(`[${integration.id}] connect failed (${reason}); retrying in ${delay}ms`);
|
|
20
|
+
const timer = setTimeout(() => {
|
|
21
|
+
timers.delete(timer);
|
|
22
|
+
attempt(integration, retry + 1);
|
|
23
|
+
}, delay);
|
|
24
|
+
timers.add(timer);
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
for (const integration of integrations) {
|
|
28
|
+
attempt(integration, 0);
|
|
29
|
+
}
|
|
30
|
+
return () => {
|
|
31
|
+
stopped = true;
|
|
32
|
+
for (const timer of timers) {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
}
|
|
35
|
+
for (const integration of integrations) {
|
|
36
|
+
integration.disconnect();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integrations.js","sourceRoot":"","sources":["../../../src/server/integrations.ts"],"names":[],"mappings":"AAQA;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,YAA2B,EAAE,OAAO,GAAiB,EAAE;IACvF,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACtC,MAAM,GAAG,GAAG,OAAO,CAAC,UAAU,IAAI,IAAI,CAAC;IACvC,MAAM,GAAG,GAAG,OAAO,CAAC,UAAU,IAAI,MAAM,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAiC,CAAC;IACxD,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,MAAM,OAAO,GAAG,CAAC,WAAwB,EAAE,KAAa,EAAE,EAAE;QAC1D,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO;QACT,CAAC;QAED,WAAW,CAAC,OAAO,EAAE,CAAC,IAAI,CACxB,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,WAAW,CAAC,EAAE,aAAa,CAAC,EAC1C,CAAC,KAAc,EAAE,EAAE;YACjB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC,IAAI,KAAK,CAAC,CAAC;YAC9C,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACtE,GAAG,CAAC,IAAI,WAAW,CAAC,EAAE,qBAAqB,MAAM,kBAAkB,KAAK,IAAI,CAAC,CAAC;YAC9E,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC5B,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACrB,OAAO,CAAC,WAAW,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;YAClC,CAAC,EAAE,KAAK,CAAC,CAAC;YAEV,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC,CACF,CAAC;IACJ,CAAC,CAAC;IAEF,KAAK,MAAM,WAAW,IAAI,YAAY,EAAE,CAAC;QACvC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAC1B,CAAC;IAED,OAAO,GAAG,EAAE;QACV,OAAO,GAAG,IAAI,CAAC;QACf,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;QAED,KAAK,MAAM,WAAW,IAAI,YAAY,EAAE,CAAC;YACvC,WAAW,CAAC,UAAU,EAAE,CAAC;QAC3B,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Integration } from '@hashsome/core';
|
|
2
|
+
import { type Express } from 'express';
|
|
3
|
+
import type { ResolvedConfig } from '../config.ts';
|
|
4
|
+
export declare function createApp(clientDir: string, integrations?: Integration[]): Express;
|
|
5
|
+
/** Where `hashsome build` puts the client, under a project. A packaged release keeps it elsewhere. */
|
|
6
|
+
export declare const BUILT_CLIENT: string;
|
|
7
|
+
export declare function startServer(config: ResolvedConfig, clientDir?: string): import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import express, {} from 'express';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { serveAsset } from './assets.js';
|
|
5
|
+
import { Proxy } from './proxy.js';
|
|
6
|
+
import { startIntegrations } from './integrations.js';
|
|
7
|
+
import { attachWebSocket } from './websocket.js';
|
|
8
|
+
export function createApp(clientDir, integrations = []) {
|
|
9
|
+
const app = express();
|
|
10
|
+
app.disable('x-powered-by');
|
|
11
|
+
app.get('/healthz', (_req, res) => {
|
|
12
|
+
res.json({ status: 'ok' });
|
|
13
|
+
});
|
|
14
|
+
app.use((req, res, next) => {
|
|
15
|
+
serveAsset(integrations, req, res).then((served) => (served ? undefined : next()), next);
|
|
16
|
+
});
|
|
17
|
+
app.get('/favicon.ico', (_req, res) => {
|
|
18
|
+
res.status(204).end();
|
|
19
|
+
});
|
|
20
|
+
app.use(express.static(clientDir, { index: false, maxAge: '1h' }));
|
|
21
|
+
// SPA fallback: client-side routing handles /{id}/...
|
|
22
|
+
app.use((_req, res) => {
|
|
23
|
+
res.sendFile(join(clientDir, 'index.html'));
|
|
24
|
+
});
|
|
25
|
+
return app;
|
|
26
|
+
}
|
|
27
|
+
/** Where `hashsome build` puts the client, under a project. A packaged release keeps it elsewhere. */
|
|
28
|
+
export const BUILT_CLIENT = join('build', 'client');
|
|
29
|
+
export function startServer(config, clientDir = join(config.root, BUILT_CLIENT)) {
|
|
30
|
+
const server = createServer(createApp(clientDir, config.integrations));
|
|
31
|
+
const proxy = new Proxy(config.integrations, { log: console.log });
|
|
32
|
+
attachWebSocket(server, proxy);
|
|
33
|
+
const stop = startIntegrations(config.integrations, { log: console.log });
|
|
34
|
+
server.listen(config.port, config.host, () => {
|
|
35
|
+
console.log(`hashsome listening on http://${config.host}:${config.port}`);
|
|
36
|
+
});
|
|
37
|
+
const shutdown = () => {
|
|
38
|
+
stop();
|
|
39
|
+
server.close(() => process.exit(0));
|
|
40
|
+
server.closeAllConnections();
|
|
41
|
+
};
|
|
42
|
+
process.on('SIGINT', shutdown);
|
|
43
|
+
process.on('SIGTERM', shutdown);
|
|
44
|
+
return server;
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"production.js","sourceRoot":"","sources":["../../../src/server/production.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Integration } from '@hashsome/core';
|
|
2
|
+
/** The subset of a `ws` WebSocket the proxy needs. */
|
|
3
|
+
export interface ProxySocket {
|
|
4
|
+
send(data: string): void;
|
|
5
|
+
on(event: 'message', listener: (data: {
|
|
6
|
+
toString(): string;
|
|
7
|
+
}) => void): unknown;
|
|
8
|
+
on(event: 'close' | 'error', listener: () => void): unknown;
|
|
9
|
+
readonly readyState: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Multiplexes browser connections onto server-held integrations. Tokens never
|
|
13
|
+
* reach the browser; current states are replayed from the integrations' stores
|
|
14
|
+
* so a waking tablet gets a full picture immediately.
|
|
15
|
+
*/
|
|
16
|
+
export interface ProxyOptions {
|
|
17
|
+
/** Where problems worth a line in the server log go (an unknown ref, a failed subscribe). */
|
|
18
|
+
log?: (message: string) => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Multiplexes browser connections onto server-held integrations. Tokens never
|
|
22
|
+
* reach the browser; current entities are replayed from the integrations' stores
|
|
23
|
+
* so a waking tablet gets a full picture immediately.
|
|
24
|
+
*
|
|
25
|
+
* The proxy does all the addressing: it parses `<integration>:<id>` and hands the integration only
|
|
26
|
+
* its local id. A subscription is resolved once the owning integration is connected — until then
|
|
27
|
+
* the browser has no answer (a component shows its loading state) — and an id the integration does
|
|
28
|
+
* not have is answered with `entity: null` and logged.
|
|
29
|
+
*/
|
|
30
|
+
export declare class Proxy {
|
|
31
|
+
#private;
|
|
32
|
+
constructor(integrations: Integration[], options?: ProxyOptions);
|
|
33
|
+
handleConnection(socket: ProxySocket): void;
|
|
34
|
+
}
|