pockethost 1.0.0-rc.1
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.md +16 -0
- package/dist/index.mjs +3156 -0
- package/dist/instance-app/migrations/1699198734_pockethost_defaults.js +39 -0
- package/dist/instance-app/pb_hooks/_ph_admin_sync.pb.js +71 -0
- package/dist/instance-app/pb_hooks/_ph_lib.js +19 -0
- package/dist/instance-app/tsconfig.json +8 -0
- package/dist/instance-app/types/goja.d.ts +58 -0
- package/dist/instance-app/types/lib.d.ts +5 -0
- package/dist/instance-app/types/types.d.ts +19573 -0
- package/dist/mothership-app/migrations/1705062576_collections_snapshot.js +1863 -0
- package/dist/mothership-app/migrations/1705083826_updated_stats.js +721 -0
- package/dist/mothership-app/migrations/1706060446_updated_instances.js +44 -0
- package/dist/mothership-app/pb_hooks/src/instances-create.pb.js +65 -0
- package/dist/mothership-app/pb_hooks/src/instances-delete.pb.js +69 -0
- package/dist/mothership-app/pb_hooks/src/instances-migrate-versions.pb.js +33 -0
- package/dist/mothership-app/pb_hooks/src/instances-notify-discord-after-create.pb.js +31 -0
- package/dist/mothership-app/pb_hooks/src/instances-notify-on-maintenance-mode.pb.js +52 -0
- package/dist/mothership-app/pb_hooks/src/instances-reset-idle.pb.js +7 -0
- package/dist/mothership-app/pb_hooks/src/instances-update.pb.js +121 -0
- package/dist/mothership-app/pb_hooks/src/instances-validate-before-create.pb.js +16 -0
- package/dist/mothership-app/pb_hooks/src/instances-validate-before-update.pb.js +43 -0
- package/dist/mothership-app/pb_hooks/src/lib.js +171 -0
- package/dist/mothership-app/pb_hooks/src/ls.pb.js +159 -0
- package/dist/mothership-app/pb_hooks/src/mail.pb.js +49 -0
- package/dist/mothership-app/pb_hooks/src/notifications.pb.js +62 -0
- package/dist/mothership-app/pb_hooks/src/random-words.js +2085 -0
- package/dist/mothership-app/pb_hooks/src/signup.pb.js +214 -0
- package/dist/mothership-app/pb_hooks/src/sns.pb.js +133 -0
- package/dist/mothership-app/pb_hooks/src/unsubscribe.pb.js +33 -0
- package/dist/mothership-app/pb_hooks/src/user-token.pb.js +33 -0
- package/dist/mothership-app/pb_hooks/src/user-welcome-msg.pb.js +32 -0
- package/dist/mothership-app/pb_hooks/src/versions-route.pb.js +12 -0
- package/dist/mothership-app/pb_hooks/src/versions.js +0 -0
- package/dist/mothership-app/pb_hooks/tsconfig.json +8 -0
- package/dist/mothership-app/pb_hooks/types/global.d.ts +1 -0
- package/dist/mothership-app/pb_hooks/types/goja.d.ts +58 -0
- package/dist/mothership-app/pb_hooks/types/lemon.d.ts +12 -0
- package/dist/mothership-app/pb_hooks/types/lib.d.ts +55 -0
- package/dist/mothership-app/pb_hooks/types/types.d.ts +19573 -0
- package/dist/mothership-app/ph_app/index.js +11 -0
- package/dist/mothership-app/ph_app/package-lock.json +657 -0
- package/dist/mothership-app/ph_app/package.json +14 -0
- package/package.json +144 -0
- package/readme.md +168 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3156 @@
|
|
|
1
|
+
// src/shared/CleanupManager.ts
|
|
2
|
+
import { reduce, values } from "@s-libs/micro-dash";
|
|
3
|
+
import { nanoid } from "nanoid";
|
|
4
|
+
|
|
5
|
+
// src/shared/Logger.ts
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import stringify from "json-stringify-safe";
|
|
8
|
+
|
|
9
|
+
// src/util/mergeConfig.ts
|
|
10
|
+
function mergeConfig(defaultConfig, ...partialConfigs) {
|
|
11
|
+
const result = { ...defaultConfig };
|
|
12
|
+
for (const partialConfig of partialConfigs) {
|
|
13
|
+
for (const key in partialConfig) {
|
|
14
|
+
if (Object.prototype.hasOwnProperty.call(partialConfig, key)) {
|
|
15
|
+
const value = partialConfig[key];
|
|
16
|
+
if (value !== void 0) {
|
|
17
|
+
result[key] = value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/shared/mkSingleton.ts
|
|
26
|
+
function mkSingleton(factory) {
|
|
27
|
+
let instance;
|
|
28
|
+
return (config) => {
|
|
29
|
+
if (instance && config) {
|
|
30
|
+
console.error(new Error().stack);
|
|
31
|
+
throw new Error("Attempted to initialize service twice.");
|
|
32
|
+
}
|
|
33
|
+
if (!instance && !config) {
|
|
34
|
+
console.error(new Error().stack);
|
|
35
|
+
throw new Error("Attempted to use service before initialization.");
|
|
36
|
+
}
|
|
37
|
+
if (!instance) {
|
|
38
|
+
instance = factory(config);
|
|
39
|
+
}
|
|
40
|
+
return instance;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/shared/Logger.ts
|
|
45
|
+
var isLevelLte = (a, b) => {
|
|
46
|
+
return LogLevels[a] <= LogLevels[b];
|
|
47
|
+
};
|
|
48
|
+
var isLevelGt = (a, b) => {
|
|
49
|
+
return LogLevels[a] > LogLevels[b];
|
|
50
|
+
};
|
|
51
|
+
var isLevelGte = (a, b) => {
|
|
52
|
+
return LogLevels[a] >= LogLevels[b];
|
|
53
|
+
};
|
|
54
|
+
var LogLevelConsoleMap = {
|
|
55
|
+
["trace" /* Trace */]: console.trace,
|
|
56
|
+
["raw" /* Raw */]: console.log,
|
|
57
|
+
["debug" /* Debug */]: console.debug,
|
|
58
|
+
["info" /* Info */]: console.info,
|
|
59
|
+
["warn" /* Warn */]: console.warn,
|
|
60
|
+
["error" /* Error */]: console.error,
|
|
61
|
+
["abort" /* Abort */]: console.error
|
|
62
|
+
};
|
|
63
|
+
var LogLevels = {
|
|
64
|
+
["trace" /* Trace */]: 0,
|
|
65
|
+
["raw" /* Raw */]: 1,
|
|
66
|
+
["debug" /* Debug */]: 2,
|
|
67
|
+
["info" /* Info */]: 3,
|
|
68
|
+
["warn" /* Warn */]: 4,
|
|
69
|
+
["error" /* Error */]: 5,
|
|
70
|
+
["abort" /* Abort */]: 6
|
|
71
|
+
};
|
|
72
|
+
var createLogger = (config) => {
|
|
73
|
+
const _config = mergeConfig(
|
|
74
|
+
{
|
|
75
|
+
level: "debug" /* Debug */,
|
|
76
|
+
pfx: [""]
|
|
77
|
+
},
|
|
78
|
+
config
|
|
79
|
+
);
|
|
80
|
+
const { pfx } = _config;
|
|
81
|
+
const setLevel = (level) => {
|
|
82
|
+
_config.level = level;
|
|
83
|
+
};
|
|
84
|
+
const _pfx = (s) => [(/* @__PURE__ */ new Date()).toISOString(), s, ...pfx].filter((v) => !!v).map((p) => `[${p}]`).join(" ");
|
|
85
|
+
const _log = (levelIn, ...args) => {
|
|
86
|
+
if (isLevelGte(levelIn, _config.level)) {
|
|
87
|
+
const pfx2 = args.shift();
|
|
88
|
+
while (args.length > 0) {
|
|
89
|
+
let arg = args.shift();
|
|
90
|
+
const t = typeof arg;
|
|
91
|
+
if (arg instanceof Error) {
|
|
92
|
+
args.unshift(...[arg.name, arg.message.toString()]);
|
|
93
|
+
if (isLevelLte(levelIn, "debug" /* Debug */) && arg.stack) {
|
|
94
|
+
args.unshift(...arg.stack.split(/\n/));
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (t === "string" && !!arg.match(/\n/)) {
|
|
99
|
+
args.unshift(...arg.split(/\n/));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (t === "object") {
|
|
103
|
+
args.unshift(...stringify(arg, null, 2).split(/\n/));
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (t === "function") {
|
|
107
|
+
arg = `<<function ${stringify(arg.toString())}>>`;
|
|
108
|
+
}
|
|
109
|
+
LogLevelConsoleMap[levelIn](...[pfx2, arg]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const raw = (...args) => {
|
|
114
|
+
_log("raw" /* Raw */, _pfx("RAW"), ...args);
|
|
115
|
+
};
|
|
116
|
+
const trace = (...args) => {
|
|
117
|
+
_log("trace" /* Trace */, _pfx(`TRACE`), ...args);
|
|
118
|
+
};
|
|
119
|
+
const dbg = (...args) => {
|
|
120
|
+
_log("debug" /* Debug */, _pfx(chalk.blueBright("DBG")), ...args);
|
|
121
|
+
};
|
|
122
|
+
const info = (...args) => {
|
|
123
|
+
_log(
|
|
124
|
+
"info" /* Info */,
|
|
125
|
+
_pfx(
|
|
126
|
+
isLevelGt("info" /* Info */, _config.level) ? chalk.gray(`INFO`) : ""
|
|
127
|
+
),
|
|
128
|
+
...args
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
const warn = (...args) => {
|
|
132
|
+
_log(
|
|
133
|
+
"warn" /* Warn */,
|
|
134
|
+
_pfx(chalk.yellow(chalk.cyanBright("WARN"))),
|
|
135
|
+
...args
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
const error = (...args) => {
|
|
139
|
+
_log("error" /* Error */, ...[_pfx(chalk.bgRed(`ERROR`)), ...args]);
|
|
140
|
+
};
|
|
141
|
+
const criticalError = (...args) => {
|
|
142
|
+
_log("error" /* Error */, ...[_pfx(chalk.bgRed(`ERROR`)), ...args]);
|
|
143
|
+
new Error().stack?.split(/\n/).forEach((line) => {
|
|
144
|
+
_log("debug" /* Debug */, _pfx(chalk.bgRed(`ERROR`)), line);
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
const abort = (...args) => {
|
|
148
|
+
_log("abort" /* Abort */, true, ...[_pfx(chalk.bgRed(`ABORT`)), ...args]);
|
|
149
|
+
throw new Error(`Fatal error: ${stringify(args)}`);
|
|
150
|
+
};
|
|
151
|
+
const create = (s, configOverride) => createLogger({
|
|
152
|
+
..._config,
|
|
153
|
+
...configOverride,
|
|
154
|
+
pfx: [..._config.pfx, s]
|
|
155
|
+
});
|
|
156
|
+
const breadcrumb = (s) => {
|
|
157
|
+
pfx.push(s);
|
|
158
|
+
return api;
|
|
159
|
+
};
|
|
160
|
+
const child = (extra) => create(stringify(extra));
|
|
161
|
+
const api = {
|
|
162
|
+
raw,
|
|
163
|
+
dbg,
|
|
164
|
+
warn,
|
|
165
|
+
info,
|
|
166
|
+
error,
|
|
167
|
+
criticalError,
|
|
168
|
+
create,
|
|
169
|
+
child,
|
|
170
|
+
trace,
|
|
171
|
+
debug: dbg,
|
|
172
|
+
breadcrumb,
|
|
173
|
+
abort,
|
|
174
|
+
shutdown() {
|
|
175
|
+
dbg(`Logger shutting down`);
|
|
176
|
+
},
|
|
177
|
+
setLevel
|
|
178
|
+
};
|
|
179
|
+
return api;
|
|
180
|
+
};
|
|
181
|
+
var LoggerService = mkSingleton(
|
|
182
|
+
(config = {}) => createLogger(config)
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// src/shared/CleanupManager.ts
|
|
186
|
+
var CLEANUP_DEFAULT_PRIORITY = 10;
|
|
187
|
+
var CLEANUP_PRIORITY_LAST = 1e3;
|
|
188
|
+
var createCleanupManager = (slug) => {
|
|
189
|
+
const _slug = slug || nanoid();
|
|
190
|
+
const { error, warn, dbg } = LoggerService().create(`cleanupManager:${_slug}`);
|
|
191
|
+
let i = 0;
|
|
192
|
+
const cleanups = {};
|
|
193
|
+
const add = (cb, priority = CLEANUP_DEFAULT_PRIORITY) => {
|
|
194
|
+
const idx = i++;
|
|
195
|
+
const cleanup = async () => {
|
|
196
|
+
await cb();
|
|
197
|
+
delete cleanups[idx];
|
|
198
|
+
};
|
|
199
|
+
cleanups[idx] = { cleanup, priority };
|
|
200
|
+
return cleanup;
|
|
201
|
+
};
|
|
202
|
+
let _shutdownP = void 0;
|
|
203
|
+
const shutdown = () => {
|
|
204
|
+
if (_shutdownP)
|
|
205
|
+
return _shutdownP;
|
|
206
|
+
const _cleanupFuncs = values(cleanups).sort((a, b) => a.priority - b.priority).map((v) => v.cleanup);
|
|
207
|
+
_shutdownP = reduce(
|
|
208
|
+
_cleanupFuncs,
|
|
209
|
+
(c, v) => {
|
|
210
|
+
return c.then(() => v());
|
|
211
|
+
},
|
|
212
|
+
Promise.resolve()
|
|
213
|
+
).catch((e) => {
|
|
214
|
+
error(
|
|
215
|
+
`Cleanup functions are failing. This should never happen, check all cleanup functions to make sure they are trapping their exceptions.`
|
|
216
|
+
);
|
|
217
|
+
throw e;
|
|
218
|
+
});
|
|
219
|
+
return _shutdownP;
|
|
220
|
+
};
|
|
221
|
+
return { add, shutdown };
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// src/shared/TimerManager.ts
|
|
225
|
+
import { forEach, values as values2 } from "@s-libs/micro-dash";
|
|
226
|
+
var createTimerManager = (config) => {
|
|
227
|
+
const { dbg, error } = LoggerService().create(`timerManager`);
|
|
228
|
+
let i = 0;
|
|
229
|
+
const cleanups = {};
|
|
230
|
+
let isShutDown = false;
|
|
231
|
+
const add = (cb, ms) => {
|
|
232
|
+
if (isShutDown)
|
|
233
|
+
throw new Error(`Already shut down`);
|
|
234
|
+
const idx = i++;
|
|
235
|
+
const tid = setTimeout(async () => {
|
|
236
|
+
cancel();
|
|
237
|
+
try {
|
|
238
|
+
await cb();
|
|
239
|
+
} catch (e) {
|
|
240
|
+
error(e);
|
|
241
|
+
}
|
|
242
|
+
}, ms);
|
|
243
|
+
const cancel = () => {
|
|
244
|
+
clearTimeout(tid);
|
|
245
|
+
delete cleanups[idx];
|
|
246
|
+
};
|
|
247
|
+
cleanups[idx] = cancel;
|
|
248
|
+
return cancel;
|
|
249
|
+
};
|
|
250
|
+
const shutdown = () => {
|
|
251
|
+
isShutDown = true;
|
|
252
|
+
dbg(`Canceling ${values2(cleanups.length).length} timers`);
|
|
253
|
+
forEach(cleanups, (c) => c());
|
|
254
|
+
dbg(`done`, cleanups);
|
|
255
|
+
};
|
|
256
|
+
const repeat = (cb, ms, initial = true) => {
|
|
257
|
+
let _unsub = void 0;
|
|
258
|
+
const _again = async () => {
|
|
259
|
+
const shouldRepeat = await cb();
|
|
260
|
+
if (shouldRepeat && !isShutDown)
|
|
261
|
+
_unsub = add(_again, ms);
|
|
262
|
+
};
|
|
263
|
+
add(_again, initial ? 0 : ms);
|
|
264
|
+
return () => {
|
|
265
|
+
_unsub?.();
|
|
266
|
+
_unsub = void 0;
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
return { add, shutdown, repeat };
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// src/shared/assert.ts
|
|
273
|
+
function assert(v, msg) {
|
|
274
|
+
if (!v) {
|
|
275
|
+
throw new Error(msg || `Assertion failure`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/shared/asyncExecutionGuard.ts
|
|
280
|
+
import { uniqueId } from "@s-libs/micro-dash";
|
|
281
|
+
import Bottleneck from "bottleneck";
|
|
282
|
+
var limiters = {};
|
|
283
|
+
var serialAsyncExecutionGuard = (cb, lane) => {
|
|
284
|
+
const uuid = uniqueId();
|
|
285
|
+
const _lane = lane || (() => uuid);
|
|
286
|
+
const wrapper = (...args) => {
|
|
287
|
+
const { dbg } = LoggerService().create("serialAsyncExecutionGuard");
|
|
288
|
+
const key = _lane(...args);
|
|
289
|
+
if (!limiters[key]) {
|
|
290
|
+
dbg(`New serial limiter with key ${key}`);
|
|
291
|
+
limiters[key] = new Bottleneck({ maxConcurrent: 1 });
|
|
292
|
+
}
|
|
293
|
+
const limiter = limiters[key];
|
|
294
|
+
return limiter.schedule(() => cb(...args));
|
|
295
|
+
};
|
|
296
|
+
return wrapper;
|
|
297
|
+
};
|
|
298
|
+
var singletons = {};
|
|
299
|
+
var singletonAsyncExecutionGuard = (cb, key) => {
|
|
300
|
+
const uuid = uniqueId();
|
|
301
|
+
const keyFactory = key || (() => uuid);
|
|
302
|
+
const wrapper = (...args) => {
|
|
303
|
+
const { dbg } = LoggerService().create(`singletonAsyncExecutionGuard`);
|
|
304
|
+
const key2 = keyFactory(...args);
|
|
305
|
+
if (singletons[key2]) {
|
|
306
|
+
return singletons[key2];
|
|
307
|
+
}
|
|
308
|
+
dbg(`New singleton guard with key ${key2}`);
|
|
309
|
+
singletons[key2] = cb(...args);
|
|
310
|
+
return singletons[key2];
|
|
311
|
+
};
|
|
312
|
+
return wrapper;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// src/shared/events.ts
|
|
316
|
+
import { values as values3 } from "@s-libs/micro-dash";
|
|
317
|
+
|
|
318
|
+
// src/shared/ioc.ts
|
|
319
|
+
var IoCManager = class {
|
|
320
|
+
services = /* @__PURE__ */ new Map();
|
|
321
|
+
register(key, instance) {
|
|
322
|
+
if (this.services.has(key)) {
|
|
323
|
+
throw new Error(`Service with key '${key}' already registered.`);
|
|
324
|
+
}
|
|
325
|
+
this.services.set(key, instance);
|
|
326
|
+
}
|
|
327
|
+
service(key) {
|
|
328
|
+
const service = this.services.get(key);
|
|
329
|
+
if (!service) {
|
|
330
|
+
throw new Error(`No service registered with key '${key}'.`);
|
|
331
|
+
}
|
|
332
|
+
return service;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// src/shared/newId.ts
|
|
337
|
+
import { customAlphabet } from "nanoid";
|
|
338
|
+
var nanoid2 = customAlphabet("abcdefghijklmnopqrstuvwxyz");
|
|
339
|
+
|
|
340
|
+
// src/shared/pocketbase-client-helpers/RestHelper.ts
|
|
341
|
+
import Ajv2 from "ajv";
|
|
342
|
+
import { ClientResponseError } from "pocketbase";
|
|
343
|
+
|
|
344
|
+
// src/shared/schema/Instance.ts
|
|
345
|
+
var INSTANCE_COLLECTION = "instances";
|
|
346
|
+
|
|
347
|
+
// src/shared/schema/Rest/index.ts
|
|
348
|
+
import Ajv from "ajv";
|
|
349
|
+
|
|
350
|
+
// src/shared/schema/Rest/GetUserTokenInfo.ts
|
|
351
|
+
var GetUserTokenPayloadSchema = {
|
|
352
|
+
type: "object",
|
|
353
|
+
properties: {
|
|
354
|
+
id: { type: "string" }
|
|
355
|
+
},
|
|
356
|
+
required: ["id"],
|
|
357
|
+
additionalProperties: false
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// src/shared/schema/Rest/UpdateInstance.ts
|
|
361
|
+
var SECRET_KEY_REGEX = /^[A-Z][A-Z0-9_]*$/;
|
|
362
|
+
var UpdateInstancePayloadSchema = {
|
|
363
|
+
type: "object",
|
|
364
|
+
properties: {
|
|
365
|
+
id: { type: "string" },
|
|
366
|
+
fields: {
|
|
367
|
+
type: "object",
|
|
368
|
+
properties: {
|
|
369
|
+
syncAdmin: { type: "boolean", nullable: true },
|
|
370
|
+
subdomain: { type: "string", nullable: true },
|
|
371
|
+
maintenance: { type: "boolean", nullable: true },
|
|
372
|
+
version: {
|
|
373
|
+
type: "string",
|
|
374
|
+
nullable: true
|
|
375
|
+
},
|
|
376
|
+
secrets: {
|
|
377
|
+
type: "object",
|
|
378
|
+
nullable: true,
|
|
379
|
+
patternProperties: {
|
|
380
|
+
[SECRET_KEY_REGEX.source]: {
|
|
381
|
+
anyOf: [{ type: "string" }]
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
required: []
|
|
385
|
+
},
|
|
386
|
+
dev: { type: "boolean", nullable: true },
|
|
387
|
+
cname: { type: "string", nullable: true },
|
|
388
|
+
notifyMaintenanceMode: { type: "boolean", nullable: true }
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
required: ["id", "fields"],
|
|
393
|
+
additionalProperties: false
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// src/shared/schema/Rest/index.ts
|
|
397
|
+
var ajv = new Ajv();
|
|
398
|
+
|
|
399
|
+
// src/shared/pocketbase-client-helpers/RestHelper.ts
|
|
400
|
+
var createRestHelper = (config) => {
|
|
401
|
+
const _logger = LoggerService().create(`RestHelper`);
|
|
402
|
+
const { client } = config;
|
|
403
|
+
const mkRest = (cmd, method, schema) => {
|
|
404
|
+
const validator = new Ajv2().compile(schema);
|
|
405
|
+
return async (payload) => {
|
|
406
|
+
const _restCallLogger = _logger.create(cmd);
|
|
407
|
+
const { dbg, error } = _restCallLogger;
|
|
408
|
+
dbg(`Executing REST call`);
|
|
409
|
+
if (!validator(payload)) {
|
|
410
|
+
throw new Error(`Invalid REST payload: ${validator.errors}`);
|
|
411
|
+
}
|
|
412
|
+
const _payload = { ...payload };
|
|
413
|
+
const url = `/api/${cmd}${method === "POST" /* Post */ ? "" : "/:id"}`.replace(/:([a-zA-Z]+)/g, (_2, key) => {
|
|
414
|
+
if (!(key in _payload)) {
|
|
415
|
+
throw new Error(`Payload must include '${key}`);
|
|
416
|
+
}
|
|
417
|
+
const value = _payload[key];
|
|
418
|
+
delete _payload[key];
|
|
419
|
+
return encodeURIComponent(value.toString());
|
|
420
|
+
});
|
|
421
|
+
const options = {
|
|
422
|
+
method
|
|
423
|
+
};
|
|
424
|
+
if (method !== "GET" /* Get */) {
|
|
425
|
+
options.body = _payload;
|
|
426
|
+
}
|
|
427
|
+
dbg({ url, options });
|
|
428
|
+
try {
|
|
429
|
+
const res = await client.send(url, options);
|
|
430
|
+
dbg(res);
|
|
431
|
+
return res;
|
|
432
|
+
} catch (e) {
|
|
433
|
+
if (e instanceof ClientResponseError) {
|
|
434
|
+
error(`REST error: ${e.originalError}`);
|
|
435
|
+
throw e.originalError;
|
|
436
|
+
}
|
|
437
|
+
error(`REST error: ${e}`);
|
|
438
|
+
throw e;
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
return { mkRest };
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// src/util/Settings.ts
|
|
446
|
+
import { boolean as castToBoolean } from "boolean";
|
|
447
|
+
import { existsSync, mkdirSync } from "fs";
|
|
448
|
+
var mkMaker = (caster) => (_default, config) => (name) => {
|
|
449
|
+
return {
|
|
450
|
+
get() {
|
|
451
|
+
const v = process.env[name];
|
|
452
|
+
if (typeof v === `undefined`) {
|
|
453
|
+
if (typeof _default === `undefined`)
|
|
454
|
+
throw new Error(`${name} must be defined`);
|
|
455
|
+
this.set(_default);
|
|
456
|
+
return this.get();
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
return caster.stringToType(v, config);
|
|
460
|
+
} catch (e) {
|
|
461
|
+
throw new Error(`${name}: ${e}`);
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
set(v) {
|
|
465
|
+
try {
|
|
466
|
+
process.env[name] = caster.typeToString(v, config);
|
|
467
|
+
} catch (e) {
|
|
468
|
+
throw new Error(`${name}: ${e}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
};
|
|
473
|
+
var mkBoolean = mkMaker({
|
|
474
|
+
stringToType: (v) => castToBoolean(v),
|
|
475
|
+
typeToString: (v) => `${v}`
|
|
476
|
+
});
|
|
477
|
+
var mkNumber = mkMaker({
|
|
478
|
+
stringToType: (s) => parseInt(s, 10),
|
|
479
|
+
typeToString: (v) => v.toString()
|
|
480
|
+
});
|
|
481
|
+
var mkPath = mkMaker({
|
|
482
|
+
stringToType: (v, options) => {
|
|
483
|
+
const { create = false, required = true } = options || {};
|
|
484
|
+
if (create) {
|
|
485
|
+
mkdirSync(v, { recursive: true });
|
|
486
|
+
}
|
|
487
|
+
if (required && !existsSync(v)) {
|
|
488
|
+
throw new Error(`${v} must exist.`);
|
|
489
|
+
}
|
|
490
|
+
return v;
|
|
491
|
+
},
|
|
492
|
+
typeToString: (v, options) => {
|
|
493
|
+
const { create = false, required = true } = options || {};
|
|
494
|
+
if (create) {
|
|
495
|
+
mkdirSync(v, { recursive: true });
|
|
496
|
+
}
|
|
497
|
+
if (required && !existsSync(v)) {
|
|
498
|
+
throw new Error(`${v} must exist.`);
|
|
499
|
+
}
|
|
500
|
+
return v;
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
var mkString = mkMaker({
|
|
504
|
+
typeToString: (v) => v,
|
|
505
|
+
stringToType: (v) => v
|
|
506
|
+
});
|
|
507
|
+
var mkCsvString = mkMaker({
|
|
508
|
+
typeToString: (v) => v.join(","),
|
|
509
|
+
stringToType: (s) => s.split(/,/).map((s2) => s2.trim()).filter((v) => !!v)
|
|
510
|
+
});
|
|
511
|
+
var SettingsService = (config) => {
|
|
512
|
+
const singleton = mkSingleton((config2) => {
|
|
513
|
+
const lookup = {};
|
|
514
|
+
for (const key in config2) {
|
|
515
|
+
const handler = config2[key](key);
|
|
516
|
+
Object.defineProperty(lookup, key, {
|
|
517
|
+
get: () => handler.get(),
|
|
518
|
+
set: (value) => handler.set(value),
|
|
519
|
+
enumerable: true
|
|
520
|
+
});
|
|
521
|
+
handler.get();
|
|
522
|
+
}
|
|
523
|
+
return lookup;
|
|
524
|
+
});
|
|
525
|
+
return singleton(config);
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// src/constants.ts
|
|
529
|
+
import { forEach as forEach2 } from "@s-libs/micro-dash";
|
|
530
|
+
import dotenv from "dotenv";
|
|
531
|
+
import { findUpSync } from "find-up";
|
|
532
|
+
import { dirname, join, resolve } from "path";
|
|
533
|
+
var loadedEnvs = dotenv.config({ path: `.env` });
|
|
534
|
+
var _PH_HOME = process.env.PH_HOME || join(process.env.HOME || resolve(`~`), `.pockethost`);
|
|
535
|
+
var _PH_PROJECT_ROOT = dirname(findUpSync("package.json"));
|
|
536
|
+
var _IS_DEV = process.env.NODE_ENV === "development";
|
|
537
|
+
console.log({ _PH_HOME, _PH_PROJECT_ROOT });
|
|
538
|
+
var SETTINGS = {
|
|
539
|
+
UPGRADE_MODE: mkBoolean(false),
|
|
540
|
+
PH_HOME: mkPath(_PH_HOME),
|
|
541
|
+
PH_VERSIONS: mkPath(join(_PH_HOME, `versions.js`), { required: false }),
|
|
542
|
+
PH_PROJECT_ROOT: mkPath(_PH_PROJECT_ROOT),
|
|
543
|
+
DEBUG: mkBoolean(_IS_DEV),
|
|
544
|
+
HTTP_PROTOCOL: mkString("https:"),
|
|
545
|
+
APP_URL: mkString(`https://app.pockethost.io`),
|
|
546
|
+
BLOG_URL: mkString(`https://pockethost.io`),
|
|
547
|
+
APEX_DOMAIN: mkString(`pockethost.io`),
|
|
548
|
+
IPCIDR_LIST: mkCsvString([`127.0.0.1/32`]),
|
|
549
|
+
DAEMON_PORT: mkNumber(3e3),
|
|
550
|
+
DAEMON_PB_IDLE_TTL: mkNumber(1e3 * 60 * 5),
|
|
551
|
+
// 5 minutes
|
|
552
|
+
MOTHERSHIP_URL: mkString(`https://pockethost-central.pockethost.io`),
|
|
553
|
+
MOTHERSHIP_NAME: mkString(`pockethost-central`),
|
|
554
|
+
MOTHERSHIP_INTERNAL_HOST: mkString(`localhost`),
|
|
555
|
+
MOTHERSHIP_ADMIN_USERNAME: mkString(),
|
|
556
|
+
MOTHERSHIP_ADMIN_PASSWORD: mkString(),
|
|
557
|
+
MOTHERSHIP_MIGRATIONS_DIR: mkPath(
|
|
558
|
+
join(_PH_PROJECT_ROOT, "src", "mothership-app", "migrations")
|
|
559
|
+
),
|
|
560
|
+
MOTHERSHIP_HOOKS_DIR: mkPath(
|
|
561
|
+
join(_PH_PROJECT_ROOT, "src", "mothership-app", `pb_hooks`, `src`)
|
|
562
|
+
),
|
|
563
|
+
MOTHERSHIP_APP_DIR: mkPath(
|
|
564
|
+
join(_PH_PROJECT_ROOT, "src", "mothership-app", `ph_app`),
|
|
565
|
+
{
|
|
566
|
+
required: false
|
|
567
|
+
}
|
|
568
|
+
),
|
|
569
|
+
MOTHERSHIP_SEMVER: mkString(""),
|
|
570
|
+
MOTHERSHIP_PORT: mkNumber(8091),
|
|
571
|
+
INITIAL_PORT_POOL_SIZE: mkNumber(20),
|
|
572
|
+
DATA_ROOT: mkPath(join(_PH_HOME, "data")),
|
|
573
|
+
NODE_ENV: mkString(`production`),
|
|
574
|
+
IS_DEV: mkBoolean(_IS_DEV),
|
|
575
|
+
TRACE: mkBoolean(false),
|
|
576
|
+
PH_BIN_CACHE: mkPath(join(_PH_HOME, ".pbincache"), { create: true }),
|
|
577
|
+
PH_FTP_PORT: mkNumber(21),
|
|
578
|
+
SSL_KEY: mkPath(join(_PH_PROJECT_ROOT, `ssl`, `localhost.key`)),
|
|
579
|
+
SSL_CERT: mkPath(join(_PH_PROJECT_ROOT, `ssl`, `localhost.crt`)),
|
|
580
|
+
PH_FTP_PASV_IP: mkString(`0.0.0.0`),
|
|
581
|
+
PH_FTP_PASV_PORT_MIN: mkNumber(1e4),
|
|
582
|
+
PH_FTP_PASV_PORT_MAX: mkNumber(2e4),
|
|
583
|
+
EDGE_APEX_DOMAIN: mkString(`pockethost.lvh.me`),
|
|
584
|
+
EDGE_MAX_ACTIVE_INSTANCES: mkNumber(20),
|
|
585
|
+
INSTANCE_APP_HOOKS_DIR: mkPath(
|
|
586
|
+
join(_PH_PROJECT_ROOT, `src`, `instance-app`, `pb_hooks`),
|
|
587
|
+
{ create: true }
|
|
588
|
+
),
|
|
589
|
+
INSTANCE_APP_MIGRATIONS_DIR: mkPath(
|
|
590
|
+
join(_PH_PROJECT_ROOT, `src`, `instance-app`, `migrations`),
|
|
591
|
+
{ create: true }
|
|
592
|
+
),
|
|
593
|
+
DISCORD_POCKETSTREAM_URL: mkString(""),
|
|
594
|
+
TEST_EMAIL: mkString(),
|
|
595
|
+
LS_WEBHOOK_SECRET: mkString(""),
|
|
596
|
+
SYSLOGD_PORT: mkNumber(6514),
|
|
597
|
+
DISCORD_HEALTH_CHANNEL_URL: mkString(""),
|
|
598
|
+
DOCKER_CONTAINER_HOST: mkString(`host.docker.internal`)
|
|
599
|
+
};
|
|
600
|
+
(() => {
|
|
601
|
+
let passed = true;
|
|
602
|
+
forEach2(loadedEnvs.parsed, (v, k) => {
|
|
603
|
+
if (!(k in SETTINGS)) {
|
|
604
|
+
passed = false;
|
|
605
|
+
console.error(`.env key ${k} is not a known setting.`);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
if (!passed) {
|
|
609
|
+
}
|
|
610
|
+
})();
|
|
611
|
+
var DefaultSettingsService = mkSingleton(
|
|
612
|
+
(settings2) => {
|
|
613
|
+
const _settings = SettingsService(settings2);
|
|
614
|
+
ioc.register("settings", _settings);
|
|
615
|
+
logConstants();
|
|
616
|
+
return _settings;
|
|
617
|
+
}
|
|
618
|
+
);
|
|
619
|
+
var ioc = new IoCManager();
|
|
620
|
+
var settings = () => ioc.service("settings");
|
|
621
|
+
var UPGRADE_MODE = () => settings().UPGRADE_MODE;
|
|
622
|
+
var PH_VERSIONS = () => settings().PH_VERSIONS;
|
|
623
|
+
var DEBUG = () => settings().DEBUG;
|
|
624
|
+
var APP_URL = () => settings().APP_URL;
|
|
625
|
+
var BLOG_URL = () => settings().BLOG_URL;
|
|
626
|
+
var APEX_DOMAIN = () => settings().APEX_DOMAIN;
|
|
627
|
+
var IPCIDR_LIST = () => settings().IPCIDR_LIST;
|
|
628
|
+
var DAEMON_PORT = () => settings().DAEMON_PORT;
|
|
629
|
+
var DAEMON_PB_IDLE_TTL = () => settings().DAEMON_PB_IDLE_TTL;
|
|
630
|
+
var MOTHERSHIP_URL = () => settings().MOTHERSHIP_URL;
|
|
631
|
+
var MOTHERSHIP_INTERNAL_HOST = () => settings().MOTHERSHIP_INTERNAL_HOST;
|
|
632
|
+
var MOTHERSHIP_NAME = () => settings().MOTHERSHIP_NAME;
|
|
633
|
+
var MOTHERSHIP_ADMIN_USERNAME = () => settings().MOTHERSHIP_ADMIN_USERNAME;
|
|
634
|
+
var MOTHERSHIP_ADMIN_PASSWORD = () => settings().MOTHERSHIP_ADMIN_PASSWORD;
|
|
635
|
+
var MOTHERSHIP_MIGRATIONS_DIR = () => settings().MOTHERSHIP_MIGRATIONS_DIR;
|
|
636
|
+
var MOTHERSHIP_HOOKS_DIR = () => settings().MOTHERSHIP_HOOKS_DIR;
|
|
637
|
+
var MOTHERSHIP_APP_DIR = () => settings().MOTHERSHIP_APP_DIR;
|
|
638
|
+
var MOTHERSHIP_SEMVER = () => settings().MOTHERSHIP_SEMVER;
|
|
639
|
+
var MOTHERSHIP_PORT = () => settings().MOTHERSHIP_PORT;
|
|
640
|
+
var INITIAL_PORT_POOL_SIZE = () => settings().INITIAL_PORT_POOL_SIZE;
|
|
641
|
+
var DATA_ROOT = () => settings().DATA_ROOT;
|
|
642
|
+
var IS_DEV = () => settings().IS_DEV;
|
|
643
|
+
var PH_BIN_CACHE = () => settings().PH_BIN_CACHE;
|
|
644
|
+
var PH_FTP_PORT = () => settings().PH_FTP_PORT;
|
|
645
|
+
var SSL_KEY = () => settings().SSL_KEY;
|
|
646
|
+
var SSL_CERT = () => settings().SSL_CERT;
|
|
647
|
+
var PH_FTP_PASV_IP = () => settings().PH_FTP_PASV_IP;
|
|
648
|
+
var PH_FTP_PASV_PORT_MIN = () => settings().PH_FTP_PASV_PORT_MIN;
|
|
649
|
+
var PH_FTP_PASV_PORT_MAX = () => settings().PH_FTP_PASV_PORT_MAX;
|
|
650
|
+
var EDGE_APEX_DOMAIN = () => settings().EDGE_APEX_DOMAIN;
|
|
651
|
+
var INSTANCE_APP_HOOK_DIR = () => settings().INSTANCE_APP_HOOKS_DIR;
|
|
652
|
+
var INSTANCE_APP_MIGRATIONS_DIR = () => settings().INSTANCE_APP_MIGRATIONS_DIR;
|
|
653
|
+
var TEST_EMAIL = () => settings().TEST_EMAIL;
|
|
654
|
+
var LS_WEBHOOK_SECRET = () => settings().LS_WEBHOOK_SECRET;
|
|
655
|
+
var SYSLOGD_PORT = () => settings().SYSLOGD_PORT;
|
|
656
|
+
var DISCORD_HEALTH_CHANNEL_URL = () => settings().DISCORD_HEALTH_CHANNEL_URL;
|
|
657
|
+
var DOCKER_CONTAINER_HOST = () => settings().DOCKER_CONTAINER_HOST;
|
|
658
|
+
var MOTHERSHIP_DATA_ROOT = () => INSTANCE_DATA_ROOT(MOTHERSHIP_NAME());
|
|
659
|
+
var MOTHERSHIP_DATA_DB = () => join(MOTHERSHIP_DATA_ROOT(), `pb_data`, `data.db`);
|
|
660
|
+
var MOTHERSHIP_INTERNAL_URL = (path = "") => `http://${MOTHERSHIP_INTERNAL_HOST()}:${MOTHERSHIP_PORT()}${path}`;
|
|
661
|
+
var INSTANCE_DATA_ROOT = (id) => join(DATA_ROOT(), id);
|
|
662
|
+
var mkContainerHomePath = (...path) => join(`/home/pockethost`, ...path.filter((v) => !!v));
|
|
663
|
+
var mkAppUrl = (path = "") => `${APP_URL()}${path}`;
|
|
664
|
+
var mkBlogUrl = (path = "") => `${BLOG_URL()}${path}`;
|
|
665
|
+
var mkDocUrl = (path = "") => mkBlogUrl(join("/docs", path));
|
|
666
|
+
var mkEdgeSubdomain = (subdomain) => `${settings().HTTP_PROTOCOL}//${subdomain}.${settings().EDGE_APEX_DOMAIN}`;
|
|
667
|
+
var mkEdgeUrl = (subdomain, path = "") => `${mkEdgeSubdomain(subdomain)}${path}`;
|
|
668
|
+
var mkInstanceDataPath = (instanceId, ...path) => join(settings().DATA_ROOT, instanceId, ...path);
|
|
669
|
+
var logConstants = () => {
|
|
670
|
+
forEach2(settings(), (v, k) => {
|
|
671
|
+
console.log(`${k}: ${v}`);
|
|
672
|
+
});
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
// src/cli/index.ts
|
|
676
|
+
import { program as program2 } from "commander";
|
|
677
|
+
|
|
678
|
+
// package.json
|
|
679
|
+
var package_default = {
|
|
680
|
+
name: "pockethost",
|
|
681
|
+
version: "1.0.0-rc.1",
|
|
682
|
+
author: {
|
|
683
|
+
name: "Ben Allfree",
|
|
684
|
+
url: "https://github.com/benallfree"
|
|
685
|
+
},
|
|
686
|
+
license: "MIT",
|
|
687
|
+
main: "dist/index.mjs",
|
|
688
|
+
scripts: {
|
|
689
|
+
"check:types": "concurrently 'pnpm:check:types:*'",
|
|
690
|
+
"check:types:dashboard": "cd frontends/dashboard && pnpm check:types",
|
|
691
|
+
"check:types:pockethost": "tsc --noEmit --skipLibCheck",
|
|
692
|
+
lint: 'prettier -c "./**/*.{ts,js,cjs,svelte,json}"',
|
|
693
|
+
"lint:fix": 'prettier -w "./**/*.{ts,js,cjs,svelte,json}"',
|
|
694
|
+
"download-versions": "tsx ./src/cli/download.ts",
|
|
695
|
+
build: "concurrently 'pnpm:build:*'",
|
|
696
|
+
"build-cli": "tsx build.ts",
|
|
697
|
+
"build-frontends": "concurrently 'pnpm:build:frontend:*'",
|
|
698
|
+
"build:frontend:dashboard": "cd frontends/dashboard && pnpm build",
|
|
699
|
+
"build:docker": "cd src/services/PocketBaseService && docker build . -t pockethost-instance",
|
|
700
|
+
"build:frontend:lander": "cd frontends/lander && pnpm build",
|
|
701
|
+
"build:frontend:superadmin": "cd frontends/superadmin && pnpm build",
|
|
702
|
+
dev: "NODE_ENV=development concurrently 'pnpm:dev:*'",
|
|
703
|
+
"dev-cli": "nodemon --signal SIGTERM dist/index.mjs",
|
|
704
|
+
"dev:lander": "cd frontends/lander && pnpm start",
|
|
705
|
+
"dev:dashboard": "cd frontends/dashboard && pnpm dev",
|
|
706
|
+
"dev:superadmin": "cd frontends/superadmin && pnpm dev",
|
|
707
|
+
"prod:proxy": "dotenv tsx ./src/cli/proxy/server.ts",
|
|
708
|
+
"prod:edge:daemon": "tsx src/cli/edge-daemon.ts",
|
|
709
|
+
"prod:edge:ftp": "tsx src/cli/edge-ftp.ts",
|
|
710
|
+
"prod:edge:health": "tsx src/cli/edge-health.ts",
|
|
711
|
+
"prod:edge:syslog": "tsx src/cli/edge-syslogd.ts",
|
|
712
|
+
"prod:mothership": "tsx src/cli/mothership.ts",
|
|
713
|
+
plop: "plop",
|
|
714
|
+
nofile: "cat /proc/sys/fs/file-nr",
|
|
715
|
+
mail: "tsx ./src/cli/sendmail.ts",
|
|
716
|
+
watch: "node build.cjs"
|
|
717
|
+
},
|
|
718
|
+
prettier: {
|
|
719
|
+
semi: false,
|
|
720
|
+
useTabs: false,
|
|
721
|
+
singleQuote: true,
|
|
722
|
+
trailingComma: "all",
|
|
723
|
+
plugins: [
|
|
724
|
+
"./node_modules/prettier-plugin-organize-imports/index.js",
|
|
725
|
+
"./node_modules/prettier-plugin-svelte/plugin.js",
|
|
726
|
+
"./node_modules/prettier-plugin-jsdoc/dist/index.js"
|
|
727
|
+
]
|
|
728
|
+
},
|
|
729
|
+
type: "module",
|
|
730
|
+
dependencies: {
|
|
731
|
+
"@s-libs/micro-dash": "^16.1.0",
|
|
732
|
+
"@types/node-os-utils": "^1.3.4",
|
|
733
|
+
"@types/winston-syslog": "^2.4.3",
|
|
734
|
+
ajv: "^8.12.0",
|
|
735
|
+
boolean: "^3.2.0",
|
|
736
|
+
bottleneck: "^2.19.5",
|
|
737
|
+
commander: "^11.1.0",
|
|
738
|
+
cors: "^2.8.5",
|
|
739
|
+
decompress: "^4.2.1",
|
|
740
|
+
"decompress-unzip": "https://github.com/pockethost/decompress-unzip.git#6ef397b9a2df11d39c7b26ce779e123833844751",
|
|
741
|
+
dockerode: "^3.3.5",
|
|
742
|
+
dotenv: "^16.3.1",
|
|
743
|
+
"event-source-polyfill": "^1.0.31",
|
|
744
|
+
eventsource: "^2.0.2",
|
|
745
|
+
"exit-hook": "^4.0.0",
|
|
746
|
+
express: "^4.18.2",
|
|
747
|
+
"express-async-errors": "^3.1.1",
|
|
748
|
+
"find-up": "^6.3.0",
|
|
749
|
+
"ftp-srv": "github:pockethost/ftp-srv#0fc708bae0d5d7a55ce948767f082d6fcfb2af59",
|
|
750
|
+
"get-port": "^6.1.2",
|
|
751
|
+
glob: "^10.3.10",
|
|
752
|
+
"http-proxy": "^1.18.1",
|
|
753
|
+
"http-proxy-middleware": "^2.0.6",
|
|
754
|
+
"ip-cidr": "^3.1.0",
|
|
755
|
+
"json-stringify-safe": "^5.0.1",
|
|
756
|
+
knex: "^2.5.1",
|
|
757
|
+
memorystream: "^0.3.1",
|
|
758
|
+
nanoid: "^5.0.2",
|
|
759
|
+
"node-fetch": "^3.3.2",
|
|
760
|
+
"node-os-utils": "^1.3.7",
|
|
761
|
+
pocketbase: "^0.20.1",
|
|
762
|
+
semver: "^7.5.4",
|
|
763
|
+
sqlite3: "^5.1.6",
|
|
764
|
+
"syslog-parse": "^2.0.0",
|
|
765
|
+
tail: "^2.2.6",
|
|
766
|
+
tmp: "^0.2.1",
|
|
767
|
+
"url-pattern": "^1.0.3",
|
|
768
|
+
vhost: "^3.0.2",
|
|
769
|
+
winston: "^3.11.0",
|
|
770
|
+
"winston-syslog": "^2.7.0"
|
|
771
|
+
},
|
|
772
|
+
devDependencies: {
|
|
773
|
+
"@swc/cli": "^0.1.62",
|
|
774
|
+
"@swc/core": "^1.3.95",
|
|
775
|
+
"@types/bootstrap": "^5.2.8",
|
|
776
|
+
"@types/copyfiles": "^2.4.4",
|
|
777
|
+
"@types/cors": "^2.8.17",
|
|
778
|
+
"@types/d3-scale": "^4.0.6",
|
|
779
|
+
"@types/d3-scale-chromatic": "^3.0.1",
|
|
780
|
+
"@types/decompress": "^4.2.6",
|
|
781
|
+
"@types/dockerode": "^3.3.21",
|
|
782
|
+
"@types/eventsource": "^1.1.14",
|
|
783
|
+
"@types/express": "^4.17.21",
|
|
784
|
+
"@types/http-proxy": "^1.17.13",
|
|
785
|
+
"@types/js-cookie": "^3.0.5",
|
|
786
|
+
"@types/json-stringify-safe": "^5.0.2",
|
|
787
|
+
"@types/marked": "^4.3.2",
|
|
788
|
+
"@types/memorystream": "^0.3.3",
|
|
789
|
+
"@types/ncp": "^2.0.8",
|
|
790
|
+
"@types/node": "^20.8.10",
|
|
791
|
+
"@types/semver": "^7.5.4",
|
|
792
|
+
"@types/tail": "^2.2.2",
|
|
793
|
+
"@types/tmp": "^0.2.5",
|
|
794
|
+
"@types/unzipper": "^0.10.8",
|
|
795
|
+
"@types/vhost": "^3.0.9",
|
|
796
|
+
chalk: "^5.3.0",
|
|
797
|
+
"chokidar-cli": "^3.0.0",
|
|
798
|
+
concurrently: "^8.2.2",
|
|
799
|
+
copyfiles: "^2.4.1",
|
|
800
|
+
"date-fns": "^2.30.0",
|
|
801
|
+
"dotenv-cli": "^7.3.0",
|
|
802
|
+
esbuild: "^0.20.0",
|
|
803
|
+
"esbuild-node-externals": "^1.13.0",
|
|
804
|
+
"js-yaml": "^4.1.0",
|
|
805
|
+
ncp: "^2.0.0",
|
|
806
|
+
nodemon: "^3.0.3",
|
|
807
|
+
ora: "^7.0.1",
|
|
808
|
+
plop: "^4.0.0",
|
|
809
|
+
"postinstall-postinstall": "^2.1.0",
|
|
810
|
+
prettier: "^3.0.3",
|
|
811
|
+
"prettier-plugin-jsdoc": "^1.3.0",
|
|
812
|
+
"prettier-plugin-organize-imports": "^3.2.3",
|
|
813
|
+
"prettier-plugin-svelte": "^3.0.3",
|
|
814
|
+
rizzdown: "^0.0.6",
|
|
815
|
+
svelte: "^4.2.2",
|
|
816
|
+
tslib: "^2.6.2",
|
|
817
|
+
tsx: "^3.14.0",
|
|
818
|
+
"type-fest": "^4.6.0",
|
|
819
|
+
typescript: "^5.2.2"
|
|
820
|
+
},
|
|
821
|
+
packageManager: "pnpm@8.10.2"
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
// src/util/PromiseAllocator.ts
|
|
825
|
+
function PromiseAllocator(initialPoolSize, initializer) {
|
|
826
|
+
const available = [];
|
|
827
|
+
const free = (item) => {
|
|
828
|
+
available.push(item);
|
|
829
|
+
};
|
|
830
|
+
for (let i = 0; i < initialPoolSize; i++) {
|
|
831
|
+
initializer().then((item) => {
|
|
832
|
+
available.push(item);
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
const alloc = async () => {
|
|
836
|
+
if (available.length > 0) {
|
|
837
|
+
const item = available.pop();
|
|
838
|
+
if (item) {
|
|
839
|
+
return [item, () => free(item)];
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
const newItem = await initializer();
|
|
843
|
+
return [newItem, () => free(newItem)];
|
|
844
|
+
};
|
|
845
|
+
return {
|
|
846
|
+
alloc,
|
|
847
|
+
free
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// src/util/SyslogLogger.ts
|
|
852
|
+
import * as winston from "winston";
|
|
853
|
+
import "winston-syslog";
|
|
854
|
+
function SyslogLogger(instanceId, target) {
|
|
855
|
+
const syslogTransport = new winston.transports.Syslog({
|
|
856
|
+
host: `localhost`,
|
|
857
|
+
port: SYSLOGD_PORT(),
|
|
858
|
+
app_name: instanceId
|
|
859
|
+
});
|
|
860
|
+
const logger = winston.createLogger({
|
|
861
|
+
format: winston.format.printf((info) => {
|
|
862
|
+
return info.message;
|
|
863
|
+
}),
|
|
864
|
+
transports: [syslogTransport]
|
|
865
|
+
});
|
|
866
|
+
const { error, warn } = LoggerService().create("SyslogLogger").breadcrumb(instanceId).breadcrumb(target);
|
|
867
|
+
const api = {
|
|
868
|
+
info: (msg) => {
|
|
869
|
+
logger.info(msg);
|
|
870
|
+
},
|
|
871
|
+
error: (msg) => {
|
|
872
|
+
logger.error(msg);
|
|
873
|
+
},
|
|
874
|
+
shutdown: () => {
|
|
875
|
+
syslogTransport.close?.();
|
|
876
|
+
logger.close();
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
return api;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/util/downloadAndExtract.ts
|
|
883
|
+
import decompress from "decompress";
|
|
884
|
+
import decompressUnzip from "decompress-unzip";
|
|
885
|
+
import { chmodSync, createWriteStream } from "fs";
|
|
886
|
+
import fetch from "node-fetch";
|
|
887
|
+
import { dirname as dirname2 } from "path";
|
|
888
|
+
var downloadFile = async (url, path) => {
|
|
889
|
+
const { body } = await fetch(url);
|
|
890
|
+
assert(body, `Body is null`);
|
|
891
|
+
const fileStream = createWriteStream(path);
|
|
892
|
+
await new Promise((resolve3, reject) => {
|
|
893
|
+
body.pipe(fileStream);
|
|
894
|
+
body.on("error", reject);
|
|
895
|
+
fileStream.on("finish", resolve3);
|
|
896
|
+
});
|
|
897
|
+
};
|
|
898
|
+
var _unsafe_downloadAndExtract = async (url, binPath, logger) => {
|
|
899
|
+
const { dbg, info, error } = logger.create("downloadAndExtract");
|
|
900
|
+
dbg(`Fetching ${url}`);
|
|
901
|
+
const res = await fetch(url);
|
|
902
|
+
const { body } = res;
|
|
903
|
+
if (!body) {
|
|
904
|
+
throw new Error(`Body expected for ${url}`);
|
|
905
|
+
}
|
|
906
|
+
const versionPath = dirname2(binPath);
|
|
907
|
+
const zipPath = `${versionPath}.zip`;
|
|
908
|
+
dbg(`Downloading ${url} to ${zipPath}`);
|
|
909
|
+
await downloadFile(url, zipPath);
|
|
910
|
+
dbg(`Extracting ${zipPath} to ${versionPath}`);
|
|
911
|
+
await decompress(zipPath, versionPath, { plugins: [decompressUnzip()] });
|
|
912
|
+
chmodSync(binPath, 509);
|
|
913
|
+
};
|
|
914
|
+
var downloadAndExtract = singletonAsyncExecutionGuard(
|
|
915
|
+
_unsafe_downloadAndExtract,
|
|
916
|
+
(url) => url
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
// src/util/env.ts
|
|
920
|
+
import { boolean } from "boolean";
|
|
921
|
+
|
|
922
|
+
// src/util/exit.ts
|
|
923
|
+
import exitHook, { asyncExitHook as _, gracefulExit as __ } from "exit-hook";
|
|
924
|
+
var asyncExitHook = (cb) => _(cb, { wait: 5e3 });
|
|
925
|
+
var gracefulExit = async (signal) => {
|
|
926
|
+
__(signal);
|
|
927
|
+
await new Promise((resolve3) => {
|
|
928
|
+
process.on("exit", resolve3);
|
|
929
|
+
});
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
// src/util/internal.ts
|
|
933
|
+
var mkInternalAddress = (port) => `127.0.0.1:${port}`;
|
|
934
|
+
var mkInternalUrl = (port) => `http://${mkInternalAddress(port)}`;
|
|
935
|
+
|
|
936
|
+
// src/util/now.ts
|
|
937
|
+
var now = () => +/* @__PURE__ */ new Date();
|
|
938
|
+
|
|
939
|
+
// src/util/process.ts
|
|
940
|
+
["unhandledRejection", "uncaughtException"].forEach((type) => {
|
|
941
|
+
process.on(type, (e) => {
|
|
942
|
+
console.error(`${e}`);
|
|
943
|
+
const debug = (() => {
|
|
944
|
+
try {
|
|
945
|
+
return ioc.service("settings").DEBUG;
|
|
946
|
+
} catch {
|
|
947
|
+
return true;
|
|
948
|
+
}
|
|
949
|
+
})();
|
|
950
|
+
if (debug) {
|
|
951
|
+
console.error(e.stack);
|
|
952
|
+
gracefulExit();
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
// src/util/smartFetch.ts
|
|
958
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
959
|
+
import fetch2 from "node-fetch";
|
|
960
|
+
import { dirname as dirname3 } from "path";
|
|
961
|
+
var smartFetch = async (url, path) => {
|
|
962
|
+
const { dbg } = LoggerService().create(`smartFetch`);
|
|
963
|
+
const data = await (async () => {
|
|
964
|
+
try {
|
|
965
|
+
const res = await fetch2(url);
|
|
966
|
+
if (res.status !== 200) {
|
|
967
|
+
throw new Error(`API appears to be down`);
|
|
968
|
+
}
|
|
969
|
+
const data2 = await res.json();
|
|
970
|
+
return data2;
|
|
971
|
+
} catch (e) {
|
|
972
|
+
dbg(`Caught error on fetch: ${e}`);
|
|
973
|
+
if (!existsSync2(path)) {
|
|
974
|
+
throw new Error(`API down and no cache`);
|
|
975
|
+
}
|
|
976
|
+
dbg(`Using data from cache`);
|
|
977
|
+
return JSON.parse(readFileSync(path).toString());
|
|
978
|
+
}
|
|
979
|
+
})();
|
|
980
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
981
|
+
writeFileSync(path, JSON.stringify(data));
|
|
982
|
+
return data;
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
// src/util/tryFetch.ts
|
|
986
|
+
import fetch3 from "node-fetch";
|
|
987
|
+
var TRYFETCH_RETRY_MS = 50;
|
|
988
|
+
var tryFetch = async (url, config) => {
|
|
989
|
+
const { preflight, retryMs } = {
|
|
990
|
+
preflight: async () => true,
|
|
991
|
+
retryMs: TRYFETCH_RETRY_MS,
|
|
992
|
+
...config
|
|
993
|
+
};
|
|
994
|
+
const logger = LoggerService().create(`tryFetch`).breadcrumb(url);
|
|
995
|
+
const { dbg } = logger;
|
|
996
|
+
return new Promise((resolve3, reject) => {
|
|
997
|
+
const again = () => setTimeout(_real_tryFetch, retryMs);
|
|
998
|
+
const _real_tryFetch = async () => {
|
|
999
|
+
if (preflight) {
|
|
1000
|
+
try {
|
|
1001
|
+
dbg(`Preflight: CHECK`);
|
|
1002
|
+
const shouldFetch = await preflight();
|
|
1003
|
+
if (!shouldFetch) {
|
|
1004
|
+
dbg(`Preflight: NOT READY`);
|
|
1005
|
+
again();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
dbg(`Preflight: READY`);
|
|
1009
|
+
} catch (e) {
|
|
1010
|
+
dbg(`Preflight: ABORT`);
|
|
1011
|
+
reject(e);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
try {
|
|
1016
|
+
dbg(`Fetch: START`);
|
|
1017
|
+
const res = await fetch3(url, { signal: AbortSignal.timeout(500) });
|
|
1018
|
+
dbg(`Fetch: SUCCESS`);
|
|
1019
|
+
resolve3(res);
|
|
1020
|
+
} catch (e) {
|
|
1021
|
+
dbg(`Fetch: ERROR: trying again in ${retryMs}ms | ${e}`);
|
|
1022
|
+
again();
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
_real_tryFetch();
|
|
1026
|
+
});
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
// src/services/FtpService/index.ts
|
|
1030
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1031
|
+
import { FtpSrv } from "ftp-srv";
|
|
1032
|
+
import pocketbaseEs from "pocketbase";
|
|
1033
|
+
|
|
1034
|
+
// src/services/FtpService/PhFs.ts
|
|
1035
|
+
import { compact, map } from "@s-libs/micro-dash";
|
|
1036
|
+
import {
|
|
1037
|
+
constants,
|
|
1038
|
+
createReadStream,
|
|
1039
|
+
createWriteStream as createWriteStream2,
|
|
1040
|
+
existsSync as existsSync3,
|
|
1041
|
+
mkdirSync as mkdirSync3
|
|
1042
|
+
} from "fs";
|
|
1043
|
+
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
1044
|
+
import { isAbsolute, join as join2, normalize, resolve as resolve2, sep } from "path";
|
|
1045
|
+
|
|
1046
|
+
// src/services/FtpService/fs-async.ts
|
|
1047
|
+
import fs from "fs";
|
|
1048
|
+
import { promisify } from "util";
|
|
1049
|
+
var stat = promisify(fs.stat);
|
|
1050
|
+
var readdir = promisify(fs.readdir);
|
|
1051
|
+
var access = promisify(fs.access);
|
|
1052
|
+
var unlink = promisify(fs.unlink);
|
|
1053
|
+
var rmdir = promisify(fs.rmdir);
|
|
1054
|
+
var mkdir = promisify(fs.mkdir);
|
|
1055
|
+
var rename = promisify(fs.rename);
|
|
1056
|
+
var chmod = promisify(fs.chmod);
|
|
1057
|
+
|
|
1058
|
+
// src/services/FtpService/PhFs.ts
|
|
1059
|
+
var nanoid3 = customAlphabet2(`abcdefghijklmnop`);
|
|
1060
|
+
var UNIX_SEP_REGEX = /\//g;
|
|
1061
|
+
var WIN_SEP_REGEX = /\\/g;
|
|
1062
|
+
var PhFs = class {
|
|
1063
|
+
log;
|
|
1064
|
+
connection;
|
|
1065
|
+
cwd;
|
|
1066
|
+
_root;
|
|
1067
|
+
client;
|
|
1068
|
+
constructor(connection, client, logger) {
|
|
1069
|
+
const cwd = `/`;
|
|
1070
|
+
const root = DATA_ROOT();
|
|
1071
|
+
this.connection = connection;
|
|
1072
|
+
this.client = client;
|
|
1073
|
+
this.log = logger.create(`PhFs`);
|
|
1074
|
+
this.cwd = normalize((cwd || "/").replace(WIN_SEP_REGEX, "/"));
|
|
1075
|
+
this._root = resolve2(root || process.cwd());
|
|
1076
|
+
}
|
|
1077
|
+
get root() {
|
|
1078
|
+
return this._root;
|
|
1079
|
+
}
|
|
1080
|
+
async _resolvePath(path = ".") {
|
|
1081
|
+
const { dbg } = this.log.create(`_resolvePath`);
|
|
1082
|
+
const resolvedPath = path.replace(WIN_SEP_REGEX, "/");
|
|
1083
|
+
const joinedPath = isAbsolute(resolvedPath) ? normalize(resolvedPath) : join2("/", this.cwd, resolvedPath);
|
|
1084
|
+
const [empty, subdomain, rootFolderName, ...pathFromRootFolder] = joinedPath.split("/");
|
|
1085
|
+
dbg({
|
|
1086
|
+
joinedPath,
|
|
1087
|
+
subdomain,
|
|
1088
|
+
rootFolderName,
|
|
1089
|
+
restOfPath: pathFromRootFolder
|
|
1090
|
+
});
|
|
1091
|
+
if (rootFolderName) {
|
|
1092
|
+
if (!isInstanceRootFolder(rootFolderName)) {
|
|
1093
|
+
throw new Error(`Accessing ${rootFolderName} is not allowed.`);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
const fsPathParts = [this._root];
|
|
1097
|
+
const instance = await (async () => {
|
|
1098
|
+
if (subdomain) {
|
|
1099
|
+
const instance2 = await this.client.collection(`instances`).getFirstListItem(`subdomain='${subdomain}'`);
|
|
1100
|
+
if (!instance2) {
|
|
1101
|
+
throw new Error(`${subdomain} not found.`);
|
|
1102
|
+
}
|
|
1103
|
+
fsPathParts.push(instance2.id);
|
|
1104
|
+
if (rootFolderName) {
|
|
1105
|
+
dbg({ rootFolderName, instance: instance2 });
|
|
1106
|
+
if (MAINTENANCE_ONLY_FOLDER_NAMES.includes(
|
|
1107
|
+
rootFolderName
|
|
1108
|
+
) && !instance2.maintenance) {
|
|
1109
|
+
throw new Error(
|
|
1110
|
+
`Instance must be in maintenance mode to access ${rootFolderName}`
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
fsPathParts.push(rootFolderName);
|
|
1114
|
+
const rootFolderFsPath = resolve2(
|
|
1115
|
+
join2(...fsPathParts).replace(UNIX_SEP_REGEX, sep).replace(WIN_SEP_REGEX, sep)
|
|
1116
|
+
);
|
|
1117
|
+
if (!existsSync3(rootFolderFsPath)) {
|
|
1118
|
+
mkdirSync3(rootFolderFsPath);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (resolvedPath.length > 0)
|
|
1122
|
+
fsPathParts.push(...pathFromRootFolder);
|
|
1123
|
+
return instance2;
|
|
1124
|
+
}
|
|
1125
|
+
})();
|
|
1126
|
+
const fsPath = resolve2(
|
|
1127
|
+
join2(...fsPathParts).replace(UNIX_SEP_REGEX, sep).replace(WIN_SEP_REGEX, sep)
|
|
1128
|
+
);
|
|
1129
|
+
const clientPath = joinedPath.replace(WIN_SEP_REGEX, "/");
|
|
1130
|
+
dbg({
|
|
1131
|
+
clientPath,
|
|
1132
|
+
fsPath,
|
|
1133
|
+
subdomain,
|
|
1134
|
+
rootFolderName,
|
|
1135
|
+
restOfPath: pathFromRootFolder,
|
|
1136
|
+
instance
|
|
1137
|
+
});
|
|
1138
|
+
return {
|
|
1139
|
+
clientPath,
|
|
1140
|
+
fsPath,
|
|
1141
|
+
subdomain,
|
|
1142
|
+
rootFolderName,
|
|
1143
|
+
pathFromRootFolder,
|
|
1144
|
+
instance
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
currentDirectory() {
|
|
1148
|
+
return this.cwd;
|
|
1149
|
+
}
|
|
1150
|
+
async chdir(path = ".") {
|
|
1151
|
+
const { fsPath, clientPath } = await this._resolvePath(path);
|
|
1152
|
+
return stat(fsPath).then((stat2) => {
|
|
1153
|
+
if (!stat2.isDirectory())
|
|
1154
|
+
throw new Error("Not a valid directory");
|
|
1155
|
+
}).then(() => {
|
|
1156
|
+
this.cwd = clientPath;
|
|
1157
|
+
return this.currentDirectory();
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
async list(path = ".") {
|
|
1161
|
+
const { dbg, error } = this.log.create(`list`).breadcrumb(`cwd:${this.cwd}`).breadcrumb(path);
|
|
1162
|
+
const { fsPath, subdomain, rootFolderName, instance } = await this._resolvePath(path);
|
|
1163
|
+
if (subdomain === "") {
|
|
1164
|
+
const instances = await this.client.collection(`instances`).getFullList();
|
|
1165
|
+
return instances.map((i) => {
|
|
1166
|
+
return {
|
|
1167
|
+
isDirectory: () => true,
|
|
1168
|
+
mode: 493,
|
|
1169
|
+
size: 0,
|
|
1170
|
+
mtime: Date.parse(i.updated),
|
|
1171
|
+
name: i.subdomain
|
|
1172
|
+
};
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
if (!instance) {
|
|
1176
|
+
throw new Error(
|
|
1177
|
+
`Something as gone wrong. An instance without a subdomain is not possible.`
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
if (!rootFolderName) {
|
|
1181
|
+
return INSTANCE_ROOT_FOLDER_NAMES.map((name) => ({
|
|
1182
|
+
isDirectory: () => true,
|
|
1183
|
+
mode: 493,
|
|
1184
|
+
size: 0,
|
|
1185
|
+
mtime: Date.parse(instance.updated),
|
|
1186
|
+
name
|
|
1187
|
+
}));
|
|
1188
|
+
}
|
|
1189
|
+
return readdir(fsPath).then((fileNames) => {
|
|
1190
|
+
return Promise.all(
|
|
1191
|
+
map(fileNames, (fileName) => {
|
|
1192
|
+
const filePath = join2(fsPath, fileName);
|
|
1193
|
+
return access(filePath, constants.F_OK).then(() => {
|
|
1194
|
+
return stat(filePath).then((stat2) => {
|
|
1195
|
+
const _stat = stat2;
|
|
1196
|
+
_stat.name = fileName;
|
|
1197
|
+
return _stat;
|
|
1198
|
+
});
|
|
1199
|
+
}).catch(() => null);
|
|
1200
|
+
})
|
|
1201
|
+
);
|
|
1202
|
+
}).then(compact);
|
|
1203
|
+
}
|
|
1204
|
+
async get(fileName) {
|
|
1205
|
+
const { fsPath, subdomain, instance, rootFolderName, pathFromRootFolder } = await this._resolvePath(fileName);
|
|
1206
|
+
const { dbg, error } = this.log.create(`get`).breadcrumb(`cwd:${this.cwd}`).breadcrumb(fileName).breadcrumb(fsPath);
|
|
1207
|
+
dbg(`get`);
|
|
1208
|
+
if (!subdomain) {
|
|
1209
|
+
return {
|
|
1210
|
+
isDirectory: () => true,
|
|
1211
|
+
mode: 493,
|
|
1212
|
+
size: 0,
|
|
1213
|
+
mtime: +/* @__PURE__ */ new Date(),
|
|
1214
|
+
name: "/"
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
if (!instance) {
|
|
1218
|
+
throw new Error(`Expected instance here`);
|
|
1219
|
+
}
|
|
1220
|
+
if (!rootFolderName) {
|
|
1221
|
+
return {
|
|
1222
|
+
isDirectory: () => true,
|
|
1223
|
+
mode: 493,
|
|
1224
|
+
size: 0,
|
|
1225
|
+
mtime: Date.parse(instance.updated),
|
|
1226
|
+
name: subdomain
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
if (!pathFromRootFolder) {
|
|
1230
|
+
return {
|
|
1231
|
+
isDirectory: () => true,
|
|
1232
|
+
mode: 493,
|
|
1233
|
+
size: 0,
|
|
1234
|
+
mtime: Date.parse(instance.updated),
|
|
1235
|
+
name: rootFolderName
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
return stat(fsPath).then((stat2) => {
|
|
1239
|
+
const _stat = stat2;
|
|
1240
|
+
_stat.name = fileName;
|
|
1241
|
+
return _stat;
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
async write(fileName, options) {
|
|
1245
|
+
const { dbg, error } = this.log.create(`write`).breadcrumb(`cwd:${this.cwd}`).breadcrumb(fileName);
|
|
1246
|
+
dbg(`write`);
|
|
1247
|
+
const { fsPath, clientPath, rootFolderName, pathFromRootFolder, instance } = await this._resolvePath(fileName);
|
|
1248
|
+
assert(instance, `Instance expected here`);
|
|
1249
|
+
const { append, start } = options || {};
|
|
1250
|
+
if (pathFromRootFolder.length === 0) {
|
|
1251
|
+
throw new Error(`write not allowed at this level`);
|
|
1252
|
+
}
|
|
1253
|
+
const stream = createWriteStream2(fsPath, {
|
|
1254
|
+
flags: !append ? "w+" : "a+",
|
|
1255
|
+
start
|
|
1256
|
+
});
|
|
1257
|
+
stream.once("error", (e) => {
|
|
1258
|
+
error(`write(${fileName}) error`);
|
|
1259
|
+
error(e);
|
|
1260
|
+
unlink(fsPath);
|
|
1261
|
+
});
|
|
1262
|
+
stream.once("close", async () => {
|
|
1263
|
+
dbg(`write(${fileName}) closing`);
|
|
1264
|
+
stream.end();
|
|
1265
|
+
await this.restartInstanceGuard(rootFolderName, instance);
|
|
1266
|
+
});
|
|
1267
|
+
return {
|
|
1268
|
+
stream,
|
|
1269
|
+
clientPath
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
async read(fileName, options) {
|
|
1273
|
+
const { dbg, error } = this.log.create(`read`).breadcrumb(`cwd:${this.cwd}`).breadcrumb(fileName);
|
|
1274
|
+
dbg(`read`);
|
|
1275
|
+
const { fsPath, clientPath, pathFromRootFolder } = await this._resolvePath(fileName);
|
|
1276
|
+
const { start } = options || {};
|
|
1277
|
+
if (pathFromRootFolder.length === 0) {
|
|
1278
|
+
throw new Error(`read not allowed at this level`);
|
|
1279
|
+
}
|
|
1280
|
+
return stat(fsPath).then((stat2) => {
|
|
1281
|
+
if (stat2.isDirectory())
|
|
1282
|
+
throw new Error("Cannot read a directory");
|
|
1283
|
+
}).then(() => {
|
|
1284
|
+
const stream = createReadStream(fsPath, { flags: "r", start });
|
|
1285
|
+
return {
|
|
1286
|
+
stream,
|
|
1287
|
+
clientPath
|
|
1288
|
+
};
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
async delete(path) {
|
|
1292
|
+
const { dbg, error } = this.log.create(`delete`).breadcrumb(`cwd:${this.cwd}`).breadcrumb(path);
|
|
1293
|
+
dbg(`delete`);
|
|
1294
|
+
const { fsPath, clientPath, pathFromRootFolder, rootFolderName, instance } = await this._resolvePath(path);
|
|
1295
|
+
assert(instance, `Instance expected here`);
|
|
1296
|
+
if (pathFromRootFolder.length === 0) {
|
|
1297
|
+
throw new Error(`delete not allowed at this level`);
|
|
1298
|
+
}
|
|
1299
|
+
return stat(fsPath).then((stat2) => {
|
|
1300
|
+
const _cleanup = () => this.restartInstanceGuard(rootFolderName, instance);
|
|
1301
|
+
if (stat2.isDirectory())
|
|
1302
|
+
return rmdir(fsPath).then(_cleanup);
|
|
1303
|
+
else
|
|
1304
|
+
return unlink(fsPath).then(_cleanup);
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
async mkdir(path) {
|
|
1308
|
+
const { dbg, error } = this.log.create(`mkdir`).breadcrumb(`cwd:${this.cwd}`).breadcrumb(path);
|
|
1309
|
+
dbg(`mkdir`);
|
|
1310
|
+
const { fsPath, clientPath, pathFromRootFolder } = await this._resolvePath(path);
|
|
1311
|
+
if (pathFromRootFolder.length === 0) {
|
|
1312
|
+
throw new Error(`mkdir not allowed at this level`);
|
|
1313
|
+
}
|
|
1314
|
+
return mkdir(fsPath, { recursive: true }).then(() => fsPath);
|
|
1315
|
+
}
|
|
1316
|
+
async rename(from, to) {
|
|
1317
|
+
const { dbg, error } = this.log.create(`rename`).breadcrumb(`cwd:${this.cwd}`).breadcrumb(from).breadcrumb(to);
|
|
1318
|
+
dbg(`rename`);
|
|
1319
|
+
const {
|
|
1320
|
+
fsPath: fromPath,
|
|
1321
|
+
pathFromRootFolder: fromPathFromRootFolder,
|
|
1322
|
+
rootFolderName: fromRootFolderName,
|
|
1323
|
+
instance
|
|
1324
|
+
} = await this._resolvePath(from);
|
|
1325
|
+
const {
|
|
1326
|
+
fsPath: toPath,
|
|
1327
|
+
pathFromRootFolder: toPathFromRootFolder,
|
|
1328
|
+
rootFolderName: toRootFolderName
|
|
1329
|
+
} = await this._resolvePath(to);
|
|
1330
|
+
assert(instance, `Instance expected here`);
|
|
1331
|
+
if (fromPathFromRootFolder.length === 0) {
|
|
1332
|
+
throw new Error(`rename not allowed at this level`);
|
|
1333
|
+
}
|
|
1334
|
+
if (toPathFromRootFolder.length === 0) {
|
|
1335
|
+
throw new Error(`rename not allowed at this level`);
|
|
1336
|
+
}
|
|
1337
|
+
return rename(fromPath, toPath).then(
|
|
1338
|
+
() => Promise.all([
|
|
1339
|
+
this.restartInstanceGuard(fromRootFolderName, instance),
|
|
1340
|
+
this.restartInstanceGuard(toRootFolderName, instance)
|
|
1341
|
+
])
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
async chmod(path, mode) {
|
|
1345
|
+
const { dbg, error } = this.log.create(`chmod`).breadcrumb(`cwd:${this.cwd}`).breadcrumb(path).breadcrumb(mode.toString());
|
|
1346
|
+
dbg(`chmod`);
|
|
1347
|
+
const { fsPath, clientPath, pathFromRootFolder } = await this._resolvePath(path);
|
|
1348
|
+
if (pathFromRootFolder.length === 0) {
|
|
1349
|
+
throw new Error(`chmod not allowed at this level`);
|
|
1350
|
+
}
|
|
1351
|
+
return;
|
|
1352
|
+
return chmod(fsPath, mode);
|
|
1353
|
+
}
|
|
1354
|
+
getUniqueName() {
|
|
1355
|
+
return nanoid3(30);
|
|
1356
|
+
}
|
|
1357
|
+
async restartInstanceGuard(rootFolderName, instance) {
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1361
|
+
// src/services/FtpService/index.ts
|
|
1362
|
+
var MAINTENANCE_ONLY_FOLDER_NAMES = ["pb_data" /* PbData */];
|
|
1363
|
+
var INSTANCE_ROOT_FOLDER_NAMES = [
|
|
1364
|
+
"pb_data" /* PbData */,
|
|
1365
|
+
"pb_public" /* PbPublic */,
|
|
1366
|
+
"pb_migrations" /* PbMigrations */,
|
|
1367
|
+
"pb_hooks" /* PbHooks */
|
|
1368
|
+
];
|
|
1369
|
+
function isInstanceRootFolder(name) {
|
|
1370
|
+
return INSTANCE_ROOT_FOLDER_NAMES.includes(name);
|
|
1371
|
+
}
|
|
1372
|
+
var ftpService = mkSingleton((config = {}) => {
|
|
1373
|
+
const { mothershipUrl } = mergeConfig(
|
|
1374
|
+
{
|
|
1375
|
+
mothershipUrl: MOTHERSHIP_INTERNAL_URL()
|
|
1376
|
+
},
|
|
1377
|
+
config
|
|
1378
|
+
);
|
|
1379
|
+
const tls = {
|
|
1380
|
+
key: readFileSync2(SSL_KEY()),
|
|
1381
|
+
cert: readFileSync2(SSL_CERT())
|
|
1382
|
+
};
|
|
1383
|
+
const _ftpServiceLogger = LoggerService().create("FtpService");
|
|
1384
|
+
const { dbg, info } = _ftpServiceLogger;
|
|
1385
|
+
const ftpServer = new FtpSrv({
|
|
1386
|
+
url: "ftp://0.0.0.0:" + PH_FTP_PORT(),
|
|
1387
|
+
anonymous: false,
|
|
1388
|
+
log: _ftpServiceLogger.create(`ftpServer`),
|
|
1389
|
+
tls,
|
|
1390
|
+
pasv_url: PH_FTP_PASV_IP(),
|
|
1391
|
+
pasv_max: PH_FTP_PASV_PORT_MAX(),
|
|
1392
|
+
pasv_min: PH_FTP_PASV_PORT_MIN()
|
|
1393
|
+
});
|
|
1394
|
+
ftpServer.on(
|
|
1395
|
+
"login",
|
|
1396
|
+
async ({ connection, username, password }, resolve3, reject) => {
|
|
1397
|
+
dbg(`Got a connection`);
|
|
1398
|
+
dbg(`Finding ${mothershipUrl}`);
|
|
1399
|
+
const client = new pocketbaseEs(mothershipUrl);
|
|
1400
|
+
try {
|
|
1401
|
+
await client.collection("users").authWithPassword(username, password);
|
|
1402
|
+
dbg(`Logged in`);
|
|
1403
|
+
const fs4 = new PhFs(connection, client, _ftpServiceLogger);
|
|
1404
|
+
resolve3({ fs: fs4 });
|
|
1405
|
+
} catch (e) {
|
|
1406
|
+
reject(new Error(`Invalid username or password`));
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
);
|
|
1411
|
+
ftpServer.listen().then(() => {
|
|
1412
|
+
dbg("Ftp server started...");
|
|
1413
|
+
});
|
|
1414
|
+
exitHook(() => {
|
|
1415
|
+
dbg(`Closing FTP server`);
|
|
1416
|
+
ftpServer.close();
|
|
1417
|
+
});
|
|
1418
|
+
return {};
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
// src/services/InstanceLoggerService/index.ts
|
|
1422
|
+
import * as fs2 from "fs";
|
|
1423
|
+
import { Tail } from "tail";
|
|
1424
|
+
import * as winston2 from "winston";
|
|
1425
|
+
var loggers = {};
|
|
1426
|
+
function InstanceLogger(instanceId, target, options = {}) {
|
|
1427
|
+
const { dbg, info } = LoggerService().create(instanceId).breadcrumb(target);
|
|
1428
|
+
const { ttl } = mergeConfig({ ttl: 0 }, options);
|
|
1429
|
+
dbg({ ttl });
|
|
1430
|
+
const loggerKey = `${instanceId}_${target}`;
|
|
1431
|
+
if (loggers[loggerKey]) {
|
|
1432
|
+
dbg(`Logger exists, using cache`);
|
|
1433
|
+
return loggers[loggerKey];
|
|
1434
|
+
}
|
|
1435
|
+
const logDirectory = mkInstanceDataPath(instanceId, `logs`);
|
|
1436
|
+
if (!fs2.existsSync(logDirectory)) {
|
|
1437
|
+
dbg(`Creating ${logDirectory}`);
|
|
1438
|
+
fs2.mkdirSync(logDirectory, { recursive: true });
|
|
1439
|
+
}
|
|
1440
|
+
const logFile = mkInstanceDataPath(instanceId, `logs`, `${target}.log`);
|
|
1441
|
+
const cm = createCleanupManager();
|
|
1442
|
+
const fileTransport = new winston2.transports.File({
|
|
1443
|
+
filename: logFile,
|
|
1444
|
+
maxsize: 100 * 1024 * 1024,
|
|
1445
|
+
// 100MB
|
|
1446
|
+
maxFiles: 10,
|
|
1447
|
+
tailable: true,
|
|
1448
|
+
zippedArchive: true
|
|
1449
|
+
});
|
|
1450
|
+
const logger = winston2.createLogger({
|
|
1451
|
+
format: winston2.format.combine(
|
|
1452
|
+
winston2.format.timestamp(),
|
|
1453
|
+
winston2.format.json(),
|
|
1454
|
+
winston2.format.printf((info2) => {
|
|
1455
|
+
return JSON.stringify({
|
|
1456
|
+
stream: info2.level === "info" ? "stdout" : "stderr",
|
|
1457
|
+
time: info2.timestamp,
|
|
1458
|
+
message: info2.message
|
|
1459
|
+
});
|
|
1460
|
+
})
|
|
1461
|
+
),
|
|
1462
|
+
transports: [fileTransport]
|
|
1463
|
+
});
|
|
1464
|
+
cm.add(() => {
|
|
1465
|
+
dbg(`Deleting and closing`);
|
|
1466
|
+
delete loggers[loggerKey];
|
|
1467
|
+
fileTransport.close?.();
|
|
1468
|
+
logger.close();
|
|
1469
|
+
});
|
|
1470
|
+
const { error, warn } = LoggerService().create("InstanceLogger").breadcrumb(instanceId).breadcrumb(target);
|
|
1471
|
+
const resetTtl = /* @__PURE__ */ (() => {
|
|
1472
|
+
let tid;
|
|
1473
|
+
return () => {
|
|
1474
|
+
if (!ttl)
|
|
1475
|
+
return;
|
|
1476
|
+
clearTimeout(tid);
|
|
1477
|
+
tid = setTimeout(() => {
|
|
1478
|
+
dbg(`Logger timeout`);
|
|
1479
|
+
api.shutdown();
|
|
1480
|
+
}, ttl);
|
|
1481
|
+
};
|
|
1482
|
+
})();
|
|
1483
|
+
const api = {
|
|
1484
|
+
info: (msg) => {
|
|
1485
|
+
resetTtl();
|
|
1486
|
+
dbg(`info: `, msg);
|
|
1487
|
+
logger.info(msg);
|
|
1488
|
+
},
|
|
1489
|
+
error: (msg) => {
|
|
1490
|
+
resetTtl();
|
|
1491
|
+
dbg(`error: `, msg);
|
|
1492
|
+
logger.error(msg);
|
|
1493
|
+
},
|
|
1494
|
+
tail: (linesBack, data) => {
|
|
1495
|
+
if (ttl) {
|
|
1496
|
+
throw new Error(`Cannot tail with ttl active`);
|
|
1497
|
+
}
|
|
1498
|
+
const logFile2 = mkInstanceDataPath(instanceId, `logs`, `${target}.log`);
|
|
1499
|
+
let tid;
|
|
1500
|
+
cm.add(() => clearTimeout(tid));
|
|
1501
|
+
const check = () => {
|
|
1502
|
+
try {
|
|
1503
|
+
const tail = new Tail(logFile2, { nLines: linesBack });
|
|
1504
|
+
tail.on("line", (line) => {
|
|
1505
|
+
try {
|
|
1506
|
+
const entry = JSON.parse(line);
|
|
1507
|
+
data(entry);
|
|
1508
|
+
} catch (e) {
|
|
1509
|
+
data({
|
|
1510
|
+
level: "info",
|
|
1511
|
+
message: line,
|
|
1512
|
+
time: (/* @__PURE__ */ new Date()).toISOString()
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
tail.on("error", (e) => {
|
|
1517
|
+
error(`Caught a tail error ${e}`);
|
|
1518
|
+
});
|
|
1519
|
+
cm.add(() => tail.unwatch());
|
|
1520
|
+
} catch (e) {
|
|
1521
|
+
warn(e);
|
|
1522
|
+
tid = setTimeout(check, 1e3);
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
check();
|
|
1526
|
+
const unsub = asyncExitHook(() => cm.shutdown());
|
|
1527
|
+
return () => {
|
|
1528
|
+
cm.shutdown();
|
|
1529
|
+
unsub();
|
|
1530
|
+
};
|
|
1531
|
+
},
|
|
1532
|
+
shutdown: () => cm.shutdown()
|
|
1533
|
+
};
|
|
1534
|
+
loggers[loggerKey] = api;
|
|
1535
|
+
return api;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// src/services/InstanceService/index.ts
|
|
1539
|
+
import { flatten, map as map2, values as values4 } from "@s-libs/micro-dash";
|
|
1540
|
+
import Bottleneck2 from "bottleneck";
|
|
1541
|
+
import { globSync } from "glob";
|
|
1542
|
+
import stringify2 from "json-stringify-safe";
|
|
1543
|
+
import { basename, join as join3 } from "path";
|
|
1544
|
+
var instanceService = mkSingleton(
|
|
1545
|
+
async (config) => {
|
|
1546
|
+
const { instanceApiTimeoutMs, instanceApiCheckIntervalMs } = config;
|
|
1547
|
+
const instanceServiceLogger = LoggerService().create("InstanceService");
|
|
1548
|
+
const { dbg, raw, error, warn } = instanceServiceLogger;
|
|
1549
|
+
const { client } = await MothershipAdminClientService();
|
|
1550
|
+
const pbService = await PocketbaseService();
|
|
1551
|
+
const instanceApis = {};
|
|
1552
|
+
const getInstanceApi = (instance) => {
|
|
1553
|
+
const _logger = instanceServiceLogger.create(`getInstanceApi`);
|
|
1554
|
+
const { id, subdomain, version: version2 } = instance;
|
|
1555
|
+
_logger.breadcrumb(`${subdomain}:${id}:${version2}`);
|
|
1556
|
+
const { dbg: dbg2, trace } = _logger;
|
|
1557
|
+
return new Promise((resolve3, reject) => {
|
|
1558
|
+
let maxTries = instanceApiTimeoutMs / instanceApiCheckIntervalMs;
|
|
1559
|
+
const retry = (interval = instanceApiCheckIntervalMs) => {
|
|
1560
|
+
maxTries--;
|
|
1561
|
+
if (maxTries <= 0) {
|
|
1562
|
+
reject(new Error(`Timeout obtaining instance API.`));
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
dbg2(`${maxTries} tries remaining. Retrying in ${interval}ms`);
|
|
1566
|
+
setTimeout(_check, interval);
|
|
1567
|
+
};
|
|
1568
|
+
const _check = () => {
|
|
1569
|
+
dbg2(`Checking for existing instance API`);
|
|
1570
|
+
const instanceApi = instanceApis[id];
|
|
1571
|
+
if (!instanceApi) {
|
|
1572
|
+
dbg2(`No API found, creating`);
|
|
1573
|
+
createInstanceApi(instance);
|
|
1574
|
+
retry(0);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
try {
|
|
1578
|
+
if (instanceApi.status() === "healthy" /* Healthy */) {
|
|
1579
|
+
dbg2(`API found and healthy, returning`);
|
|
1580
|
+
resolve3(instanceApi);
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
} catch (e) {
|
|
1584
|
+
dbg2(`Instance is in an error state, returning error`);
|
|
1585
|
+
reject(e);
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
dbg2(`API found but not healthy, waiting`);
|
|
1589
|
+
retry();
|
|
1590
|
+
};
|
|
1591
|
+
_check();
|
|
1592
|
+
});
|
|
1593
|
+
};
|
|
1594
|
+
const createInstanceApi = (instance) => {
|
|
1595
|
+
const { id, subdomain, version: version2 } = instance;
|
|
1596
|
+
const systemInstanceLogger = instanceServiceLogger.create(
|
|
1597
|
+
`${subdomain}:${id}:${version2}`
|
|
1598
|
+
);
|
|
1599
|
+
const { dbg: dbg2, warn: warn2, error: error2, info } = systemInstanceLogger;
|
|
1600
|
+
if (instanceApis[id]) {
|
|
1601
|
+
throw new Error(
|
|
1602
|
+
`Attempted to create an instance API when one is already available for ${id}`
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
const shutdownManager = createCleanupManager();
|
|
1606
|
+
shutdownManager.add(async () => {
|
|
1607
|
+
dbg2(`Deleting from cache`);
|
|
1608
|
+
delete instanceApis[id];
|
|
1609
|
+
dbg2(`There are ${values4(instanceApis).length} still in cache`);
|
|
1610
|
+
}, CLEANUP_PRIORITY_LAST);
|
|
1611
|
+
shutdownManager.add(async () => {
|
|
1612
|
+
dbg2(`Shutting down`);
|
|
1613
|
+
status = "shutdown" /* ShuttingDown */;
|
|
1614
|
+
});
|
|
1615
|
+
info(`Starting`);
|
|
1616
|
+
let status = "starting" /* Starting */;
|
|
1617
|
+
let internalUrl = "";
|
|
1618
|
+
let startRequest = () => {
|
|
1619
|
+
throw new Error(`Not ready yet`);
|
|
1620
|
+
};
|
|
1621
|
+
let _shutdownReason;
|
|
1622
|
+
const api = {
|
|
1623
|
+
status: () => {
|
|
1624
|
+
if (_shutdownReason)
|
|
1625
|
+
throw _shutdownReason;
|
|
1626
|
+
return status;
|
|
1627
|
+
},
|
|
1628
|
+
internalUrl: () => {
|
|
1629
|
+
if (status !== "healthy" /* Healthy */) {
|
|
1630
|
+
throw new Error(
|
|
1631
|
+
`Attempt to access instance URL when instance is not in a healthy state.`
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
return internalUrl;
|
|
1635
|
+
},
|
|
1636
|
+
startRequest: () => {
|
|
1637
|
+
if (status !== "healthy" /* Healthy */) {
|
|
1638
|
+
throw new Error(
|
|
1639
|
+
`Attempt to start an instance request when instance is not in a healthy state.`
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
return startRequest();
|
|
1643
|
+
},
|
|
1644
|
+
shutdown: async (reason) => {
|
|
1645
|
+
dbg2(`Shutting down`);
|
|
1646
|
+
if (reason) {
|
|
1647
|
+
_shutdownReason = reason;
|
|
1648
|
+
error2(`Panic shutdown for ${reason}`);
|
|
1649
|
+
} else {
|
|
1650
|
+
dbg2(`Graceful shutdown`);
|
|
1651
|
+
}
|
|
1652
|
+
if (status === "shutdown" /* ShuttingDown */) {
|
|
1653
|
+
warn2(`Already shutting down`);
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
return shutdownManager.shutdown();
|
|
1657
|
+
}
|
|
1658
|
+
};
|
|
1659
|
+
const _safeShutdown = async (reason) => {
|
|
1660
|
+
if (status === "shutdown" /* ShuttingDown */ && reason) {
|
|
1661
|
+
warn2(`Already shutting down, ${reason} will not be reported.`);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
return api.shutdown(reason);
|
|
1665
|
+
};
|
|
1666
|
+
instanceApis[id] = api;
|
|
1667
|
+
const healthyGuard = () => {
|
|
1668
|
+
if (status !== "shutdown" /* ShuttingDown */)
|
|
1669
|
+
return;
|
|
1670
|
+
throw new Error(
|
|
1671
|
+
`HealthyGuard detected instance is shutting down. Aborting further initialization.`
|
|
1672
|
+
);
|
|
1673
|
+
};
|
|
1674
|
+
const clientLimiter = new Bottleneck2({ maxConcurrent: 1 });
|
|
1675
|
+
const updateInstanceStatus = clientLimiter.wrap(
|
|
1676
|
+
client.updateInstanceStatus
|
|
1677
|
+
);
|
|
1678
|
+
const updateInstance = clientLimiter.wrap(client.updateInstance);
|
|
1679
|
+
(async () => {
|
|
1680
|
+
const { version: version3 } = instance;
|
|
1681
|
+
dbg2(`Obtaining port`);
|
|
1682
|
+
const [newPort, releasePort] = await PortService().alloc();
|
|
1683
|
+
shutdownManager.add(() => {
|
|
1684
|
+
dbg2(`Releasing port`);
|
|
1685
|
+
releasePort();
|
|
1686
|
+
}, CLEANUP_PRIORITY_LAST);
|
|
1687
|
+
systemInstanceLogger.breadcrumb(`port:${newPort}`);
|
|
1688
|
+
dbg2(`Found port`);
|
|
1689
|
+
healthyGuard();
|
|
1690
|
+
const userInstanceLogger = InstanceLogger(instance.id, `exec`);
|
|
1691
|
+
dbg2(`Starting instance`);
|
|
1692
|
+
dbg2(`Set instance status: starting`);
|
|
1693
|
+
healthyGuard();
|
|
1694
|
+
await updateInstanceStatus(instance.id, "starting" /* Starting */);
|
|
1695
|
+
shutdownManager.add(async () => {
|
|
1696
|
+
dbg2(`Set instance status: idle`);
|
|
1697
|
+
await updateInstanceStatus(id, "idle" /* Idle */).catch(error2);
|
|
1698
|
+
});
|
|
1699
|
+
healthyGuard();
|
|
1700
|
+
const spawnArgs = {
|
|
1701
|
+
subdomain: instance.subdomain,
|
|
1702
|
+
instanceId: instance.id,
|
|
1703
|
+
port: newPort,
|
|
1704
|
+
dev: instance.dev,
|
|
1705
|
+
extraBinds: flatten([
|
|
1706
|
+
globSync(join3(INSTANCE_APP_MIGRATIONS_DIR(), "*.js")).map(
|
|
1707
|
+
(file) => `${file}:${mkContainerHomePath(
|
|
1708
|
+
`pb_migrations/${basename(file)}`
|
|
1709
|
+
)}:ro`
|
|
1710
|
+
),
|
|
1711
|
+
globSync(join3(INSTANCE_APP_HOOK_DIR(), "*.js")).map(
|
|
1712
|
+
(file) => `${file}:${mkContainerHomePath(
|
|
1713
|
+
`pb_hooks/${basename(file)}`
|
|
1714
|
+
)}:ro`
|
|
1715
|
+
)
|
|
1716
|
+
]),
|
|
1717
|
+
env: {
|
|
1718
|
+
...instance.secrets,
|
|
1719
|
+
PH_APP_NAME: instance.subdomain,
|
|
1720
|
+
PH_INSTANCE_URL: mkEdgeUrl(instance.subdomain)
|
|
1721
|
+
},
|
|
1722
|
+
version: version3
|
|
1723
|
+
};
|
|
1724
|
+
if (instance.syncAdmin) {
|
|
1725
|
+
const id2 = instance.uid;
|
|
1726
|
+
dbg2(`Fetching token info for uid ${id2}`);
|
|
1727
|
+
const { email, tokenKey, passwordHash } = await client.getUserTokenInfo({ id: id2 });
|
|
1728
|
+
dbg2(`Token info is`, { email, tokenKey, passwordHash });
|
|
1729
|
+
spawnArgs.env.ADMIN_SYNC = stringify2({
|
|
1730
|
+
id: id2,
|
|
1731
|
+
email,
|
|
1732
|
+
tokenKey,
|
|
1733
|
+
passwordHash
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
const childProcess = await (async () => {
|
|
1737
|
+
try {
|
|
1738
|
+
const cp = await pbService.spawn(spawnArgs);
|
|
1739
|
+
return cp;
|
|
1740
|
+
} catch (e) {
|
|
1741
|
+
warn2(`Error spawning: ${e}`);
|
|
1742
|
+
userInstanceLogger.error(`Error spawning: ${e}`);
|
|
1743
|
+
if (UPGRADE_MODE()) {
|
|
1744
|
+
} else {
|
|
1745
|
+
await updateInstance(instance.id, {
|
|
1746
|
+
maintenance: true
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
throw new Error(
|
|
1750
|
+
`Could not launch container. Instance has been placed in maintenance mode. Please review your instance logs at https://app.pockethost.io/app/instances/${instance.id} or contact support at https://pockethost.io/support`
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
})();
|
|
1754
|
+
const { exitCode } = childProcess;
|
|
1755
|
+
exitCode.then((code) => {
|
|
1756
|
+
info(`Processes exited with ${code}.`);
|
|
1757
|
+
if (code !== 0 && !UPGRADE_MODE()) {
|
|
1758
|
+
shutdownManager.add(async () => {
|
|
1759
|
+
userInstanceLogger.error(
|
|
1760
|
+
`Putting instance in maintenance mode because it shut down with return code ${code}. `
|
|
1761
|
+
);
|
|
1762
|
+
error2(
|
|
1763
|
+
`Putting instance in maintenance mode because it shut down with return code ${code}. `
|
|
1764
|
+
);
|
|
1765
|
+
await updateInstance(instance.id, {
|
|
1766
|
+
maintenance: true
|
|
1767
|
+
});
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
setImmediate(() => {
|
|
1771
|
+
_safeShutdown().catch(error2);
|
|
1772
|
+
});
|
|
1773
|
+
});
|
|
1774
|
+
shutdownManager.add(async () => {
|
|
1775
|
+
dbg2(`killing ${id}`);
|
|
1776
|
+
await childProcess.kill().catch(error2);
|
|
1777
|
+
dbg2(`killed ${id}`);
|
|
1778
|
+
});
|
|
1779
|
+
const tm = createTimerManager({});
|
|
1780
|
+
shutdownManager.add(() => tm.shutdown());
|
|
1781
|
+
let openRequestCount = 0;
|
|
1782
|
+
let lastRequest = now();
|
|
1783
|
+
internalUrl = mkInternalUrl(newPort);
|
|
1784
|
+
const RECHECK_TTL = 1e3;
|
|
1785
|
+
startRequest = () => {
|
|
1786
|
+
lastRequest = now();
|
|
1787
|
+
openRequestCount++;
|
|
1788
|
+
const id2 = openRequestCount;
|
|
1789
|
+
dbg2(`started new request`);
|
|
1790
|
+
return () => {
|
|
1791
|
+
openRequestCount--;
|
|
1792
|
+
dbg2(`ended request (${openRequestCount} still open)`);
|
|
1793
|
+
};
|
|
1794
|
+
};
|
|
1795
|
+
{
|
|
1796
|
+
tm.repeat(async () => {
|
|
1797
|
+
dbg2(`idle check: ${openRequestCount} open requests`);
|
|
1798
|
+
if (openRequestCount === 0 && lastRequest + DAEMON_PB_IDLE_TTL() < now()) {
|
|
1799
|
+
info(`idle for ${DAEMON_PB_IDLE_TTL()}, shutting down`);
|
|
1800
|
+
healthyGuard();
|
|
1801
|
+
await _safeShutdown().catch(error2);
|
|
1802
|
+
return false;
|
|
1803
|
+
} else {
|
|
1804
|
+
dbg2(`${openRequestCount} requests remain open`);
|
|
1805
|
+
}
|
|
1806
|
+
return true;
|
|
1807
|
+
}, RECHECK_TTL);
|
|
1808
|
+
}
|
|
1809
|
+
dbg2(`${internalUrl} is running`);
|
|
1810
|
+
status = "healthy" /* Healthy */;
|
|
1811
|
+
healthyGuard();
|
|
1812
|
+
await updateInstanceStatus(instance.id, "running" /* Running */);
|
|
1813
|
+
})().catch((e) => {
|
|
1814
|
+
warn2(
|
|
1815
|
+
`Instance failed to start with ${e}`,
|
|
1816
|
+
e.originalError?.message
|
|
1817
|
+
);
|
|
1818
|
+
_safeShutdown(e).catch(error2);
|
|
1819
|
+
});
|
|
1820
|
+
return api;
|
|
1821
|
+
};
|
|
1822
|
+
const getInstance = async (host) => {
|
|
1823
|
+
{
|
|
1824
|
+
dbg(`Trying to get instance by host: ${host}`);
|
|
1825
|
+
const instance = await client.getInstanceByCname(host).catch((e) => {
|
|
1826
|
+
if (e.status !== 404) {
|
|
1827
|
+
throw new Error(
|
|
1828
|
+
`Unexpected response ${JSON.stringify(e)} from mothership`
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
});
|
|
1832
|
+
if (instance) {
|
|
1833
|
+
dbg(`${host} is a cname`);
|
|
1834
|
+
if (!instance.cname_active) {
|
|
1835
|
+
throw new Error(
|
|
1836
|
+
`CNAME not active for this instance. See dashboard.`
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
return instance;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
const idOrSubdomain = host.replace(`.${EDGE_APEX_DOMAIN()}`, "");
|
|
1843
|
+
{
|
|
1844
|
+
dbg(`Trying to get instance by ID: ${idOrSubdomain}`);
|
|
1845
|
+
const instance = await client.getInstanceById(idOrSubdomain).catch((e) => {
|
|
1846
|
+
if (e.status !== 404) {
|
|
1847
|
+
throw new Error(
|
|
1848
|
+
`Unexpected response ${JSON.stringify(e)} from mothership`
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
if (instance) {
|
|
1853
|
+
dbg(`${idOrSubdomain} is an instance ID`);
|
|
1854
|
+
return instance;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
{
|
|
1858
|
+
dbg(`Trying to get instance by subdomain: ${idOrSubdomain}`);
|
|
1859
|
+
const instance = await client.getInstanceBySubdomain(idOrSubdomain).catch((e) => {
|
|
1860
|
+
if (e.status !== 404) {
|
|
1861
|
+
throw new Error(
|
|
1862
|
+
`Unexpected response ${JSON.stringify(e)} from mothership`
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
if (instance) {
|
|
1867
|
+
dbg(`${idOrSubdomain} is a subdomain`);
|
|
1868
|
+
return instance;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
dbg(`${idOrSubdomain} is neither an cname nor a subdomain`);
|
|
1872
|
+
};
|
|
1873
|
+
(await proxyService()).use(async (req, res, next) => {
|
|
1874
|
+
const logger = LoggerService().create(`InstanceRequest`);
|
|
1875
|
+
const { dbg: dbg2 } = logger;
|
|
1876
|
+
const { host, proxy: proxy2 } = res.locals;
|
|
1877
|
+
const instance = await getInstance(host);
|
|
1878
|
+
if (!instance) {
|
|
1879
|
+
res.status(404).end(`${host} not found`);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
const owner = instance.expand.uid;
|
|
1883
|
+
if (!owner) {
|
|
1884
|
+
throw new Error(`Instance owner is invalid`);
|
|
1885
|
+
}
|
|
1886
|
+
dbg2(`Checking for maintenance mode`);
|
|
1887
|
+
if (instance.maintenance) {
|
|
1888
|
+
throw new Error(
|
|
1889
|
+
`This instance is in Maintenance Mode. See ${mkDocUrl(
|
|
1890
|
+
`usage/maintenance`
|
|
1891
|
+
)} for more information.`
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
dbg2(`Checking for verified account`);
|
|
1895
|
+
if (!owner.verified) {
|
|
1896
|
+
throw new Error(`Log in at ${mkAppUrl()} to verify your account.`);
|
|
1897
|
+
}
|
|
1898
|
+
const api = await getInstanceApi(instance);
|
|
1899
|
+
const endRequest = api.startRequest();
|
|
1900
|
+
res.on("close", endRequest);
|
|
1901
|
+
if (req.closed) {
|
|
1902
|
+
throw new Error(`Request already closed.`);
|
|
1903
|
+
}
|
|
1904
|
+
dbg2(
|
|
1905
|
+
`Forwarding proxy request for ${req.url} to instance ${api.internalUrl()}`
|
|
1906
|
+
);
|
|
1907
|
+
proxy2.web(req, res, { target: api.internalUrl() });
|
|
1908
|
+
});
|
|
1909
|
+
asyncExitHook(async () => {
|
|
1910
|
+
dbg(`Shutting down instance manager`);
|
|
1911
|
+
const p = Promise.all(map2(instanceApis, (api) => api.shutdown()));
|
|
1912
|
+
await p;
|
|
1913
|
+
});
|
|
1914
|
+
return {};
|
|
1915
|
+
}
|
|
1916
|
+
);
|
|
1917
|
+
|
|
1918
|
+
// src/services/MothershipAdminClientService/createAdminPbClient.ts
|
|
1919
|
+
import { default as PocketBase } from "pocketbase";
|
|
1920
|
+
|
|
1921
|
+
// src/services/MothershipAdminClientService/InstanceMIxin.ts
|
|
1922
|
+
var createInstanceMixin = (context) => {
|
|
1923
|
+
const { logger } = context;
|
|
1924
|
+
const { dbg, raw } = logger.create("InstanceMixin");
|
|
1925
|
+
const { client } = context;
|
|
1926
|
+
const createInstance = (payload) => {
|
|
1927
|
+
return client.collection(INSTANCE_COLLECTION).create(payload);
|
|
1928
|
+
};
|
|
1929
|
+
const getInstanceBySubdomain = (subdomain) => client.collection(INSTANCE_COLLECTION).getFirstListItem(`subdomain = '${subdomain}'`, { expand: "uid" });
|
|
1930
|
+
const getInstanceByCname = (host) => client.collection(INSTANCE_COLLECTION).getFirstListItem(`cname = '${host}'`, {
|
|
1931
|
+
expand: "uid"
|
|
1932
|
+
});
|
|
1933
|
+
const getInstanceById = async (instanceId) => client.collection(INSTANCE_COLLECTION).getOne(instanceId, { expand: "uid" });
|
|
1934
|
+
const updateInstance = async (instanceId, fields) => {
|
|
1935
|
+
await client.collection(INSTANCE_COLLECTION).update(instanceId, fields);
|
|
1936
|
+
};
|
|
1937
|
+
const updateInstanceStatus = async (instanceId, status) => {
|
|
1938
|
+
await updateInstance(instanceId, { status });
|
|
1939
|
+
};
|
|
1940
|
+
const getInstance = async (instanceId) => {
|
|
1941
|
+
return client.collection(INSTANCE_COLLECTION).getOne(instanceId, { expand: "uid" });
|
|
1942
|
+
};
|
|
1943
|
+
const getInstances = async () => {
|
|
1944
|
+
return client.collection(INSTANCE_COLLECTION).getFullList();
|
|
1945
|
+
};
|
|
1946
|
+
return {
|
|
1947
|
+
getInstanceById,
|
|
1948
|
+
getInstances,
|
|
1949
|
+
updateInstance,
|
|
1950
|
+
updateInstanceStatus,
|
|
1951
|
+
getInstanceBySubdomain,
|
|
1952
|
+
getInstanceByCname,
|
|
1953
|
+
getInstance,
|
|
1954
|
+
createInstance
|
|
1955
|
+
};
|
|
1956
|
+
};
|
|
1957
|
+
|
|
1958
|
+
// src/services/MothershipAdminClientService/createAdminPbClient.ts
|
|
1959
|
+
var createAdminPbClient = (url) => {
|
|
1960
|
+
const _clientLogger = LoggerService().create("PbClient");
|
|
1961
|
+
const { info } = _clientLogger;
|
|
1962
|
+
info(`Initializing client: ${url}`);
|
|
1963
|
+
const client = new PocketBase(url);
|
|
1964
|
+
client.autoCancellation(false);
|
|
1965
|
+
const adminAuthViaEmail = (email, password) => client.admins.authWithPassword(email, password);
|
|
1966
|
+
const createFirstAdmin = (email, password) => client.admins.create({ email, password, passwordConfirm: password }).catch((res) => {
|
|
1967
|
+
console.log({ email, password });
|
|
1968
|
+
console.log(JSON.stringify(res, null, 2));
|
|
1969
|
+
return res;
|
|
1970
|
+
});
|
|
1971
|
+
const context = { client, logger: _clientLogger };
|
|
1972
|
+
const instanceApi = createInstanceMixin(context);
|
|
1973
|
+
const restHelper = createRestHelper({ client });
|
|
1974
|
+
const { mkRest } = restHelper;
|
|
1975
|
+
const getUserTokenInfo = mkRest(
|
|
1976
|
+
"userToken" /* UserToken */,
|
|
1977
|
+
"GET" /* Get */,
|
|
1978
|
+
GetUserTokenPayloadSchema
|
|
1979
|
+
);
|
|
1980
|
+
const api = {
|
|
1981
|
+
client,
|
|
1982
|
+
url,
|
|
1983
|
+
createFirstAdmin,
|
|
1984
|
+
adminAuthViaEmail,
|
|
1985
|
+
getUserTokenInfo,
|
|
1986
|
+
...instanceApi
|
|
1987
|
+
};
|
|
1988
|
+
return api;
|
|
1989
|
+
};
|
|
1990
|
+
|
|
1991
|
+
// src/services/MothershipAdminClientService/index.ts
|
|
1992
|
+
var MothershipAdminClientService = mkSingleton(
|
|
1993
|
+
async (cfg = {}) => {
|
|
1994
|
+
const { url, username, password } = mergeConfig(
|
|
1995
|
+
{
|
|
1996
|
+
url: MOTHERSHIP_URL(),
|
|
1997
|
+
username: MOTHERSHIP_ADMIN_USERNAME(),
|
|
1998
|
+
password: MOTHERSHIP_ADMIN_PASSWORD()
|
|
1999
|
+
},
|
|
2000
|
+
cfg
|
|
2001
|
+
);
|
|
2002
|
+
const _clientLogger = LoggerService().create(`client singleton`);
|
|
2003
|
+
const { dbg, error } = _clientLogger;
|
|
2004
|
+
const client = createAdminPbClient(url);
|
|
2005
|
+
while (true) {
|
|
2006
|
+
try {
|
|
2007
|
+
await client.adminAuthViaEmail(username, password);
|
|
2008
|
+
dbg(`Logged in as admin`);
|
|
2009
|
+
break;
|
|
2010
|
+
} catch (e) {
|
|
2011
|
+
dbg(`Creating first admin account`);
|
|
2012
|
+
try {
|
|
2013
|
+
await client.createFirstAdmin(username, password);
|
|
2014
|
+
await client.adminAuthViaEmail(username, password);
|
|
2015
|
+
dbg(`Logged in`);
|
|
2016
|
+
} catch (e2) {
|
|
2017
|
+
error(`CANNOT AUTHENTICATE TO ${url}`);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
return {
|
|
2022
|
+
client
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
);
|
|
2026
|
+
|
|
2027
|
+
// src/services/PocketBaseService/index.ts
|
|
2028
|
+
import { map as map3 } from "@s-libs/micro-dash";
|
|
2029
|
+
import Docker from "dockerode";
|
|
2030
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2031
|
+
import MemoryStream from "memorystream";
|
|
2032
|
+
import { gte } from "semver";
|
|
2033
|
+
|
|
2034
|
+
// src/services/PocketbaseReleaseVersionService/index.ts
|
|
2035
|
+
import { keys } from "@s-libs/micro-dash";
|
|
2036
|
+
import { chmodSync as chmodSync2, existsSync as existsSync5 } from "fs";
|
|
2037
|
+
import { glob } from "glob";
|
|
2038
|
+
import { basename as basename2, join as join4 } from "path";
|
|
2039
|
+
import { maxSatisfying, rsort } from "semver";
|
|
2040
|
+
var PocketbaseReleaseVersionService = mkSingleton(
|
|
2041
|
+
async (config = {}) => {
|
|
2042
|
+
const _serviceLogger = LoggerService().create(
|
|
2043
|
+
"PocketbaseReleaseVersionService"
|
|
2044
|
+
);
|
|
2045
|
+
const { dbg, error, warn, abort } = _serviceLogger;
|
|
2046
|
+
dbg(`Initializing`);
|
|
2047
|
+
const { cachePath, checkIntervalMs } = mergeConfig(
|
|
2048
|
+
{
|
|
2049
|
+
cachePath: PH_BIN_CACHE(),
|
|
2050
|
+
checkIntervalMs: 1e3 * 5 * 60
|
|
2051
|
+
},
|
|
2052
|
+
config
|
|
2053
|
+
);
|
|
2054
|
+
const binPaths = {};
|
|
2055
|
+
let maxVersion = "";
|
|
2056
|
+
const tm = createTimerManager();
|
|
2057
|
+
const check = async () => {
|
|
2058
|
+
const versions = await glob(join4(cachePath, `v*/`));
|
|
2059
|
+
versions.forEach((path) => {
|
|
2060
|
+
const version2 = basename2(path);
|
|
2061
|
+
const sanitizedTagName = version2.slice(1);
|
|
2062
|
+
dbg(`Found a version ${sanitizedTagName}`);
|
|
2063
|
+
const binPath = join4(path, `pocketbase`);
|
|
2064
|
+
dbg(`Checking ${binPath}`);
|
|
2065
|
+
if (existsSync5(binPath)) {
|
|
2066
|
+
chmodSync2(binPath, 509);
|
|
2067
|
+
}
|
|
2068
|
+
binPaths[sanitizedTagName] = binPath;
|
|
2069
|
+
});
|
|
2070
|
+
maxVersion = `~${rsort(keys(binPaths))[0]}`;
|
|
2071
|
+
dbg({ maxVersion });
|
|
2072
|
+
return true;
|
|
2073
|
+
};
|
|
2074
|
+
await check().catch(error);
|
|
2075
|
+
tm.repeat(check, checkIntervalMs, false);
|
|
2076
|
+
const getLatestVersion = () => maxVersion;
|
|
2077
|
+
const getVersion = (semVer = maxVersion) => {
|
|
2078
|
+
const version2 = maxSatisfying(keys(binPaths), semVer);
|
|
2079
|
+
if (!version2)
|
|
2080
|
+
throw new Error(
|
|
2081
|
+
`No version satisfies ${semVer} (${keys(binPaths).join(", ")})`
|
|
2082
|
+
);
|
|
2083
|
+
const binPath = binPaths[version2];
|
|
2084
|
+
if (!binPath)
|
|
2085
|
+
throw new Error(`binPath for ${version2} not found`);
|
|
2086
|
+
return {
|
|
2087
|
+
version: version2,
|
|
2088
|
+
binPath
|
|
2089
|
+
};
|
|
2090
|
+
};
|
|
2091
|
+
return {
|
|
2092
|
+
getLatestVersion,
|
|
2093
|
+
getVersion
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
);
|
|
2097
|
+
|
|
2098
|
+
// src/services/PocketBaseService/index.ts
|
|
2099
|
+
var INSTANCE_IMAGE_NAME = `pockethost-instance`;
|
|
2100
|
+
var createPocketbaseService = async (config) => {
|
|
2101
|
+
const _serviceLogger = LoggerService().create("PocketbaseService");
|
|
2102
|
+
const { dbg, error, warn, abort } = _serviceLogger;
|
|
2103
|
+
const { getLatestVersion, getVersion } = await PocketbaseReleaseVersionService();
|
|
2104
|
+
const maxVersion = getLatestVersion();
|
|
2105
|
+
const _spawn = async (cfg) => {
|
|
2106
|
+
const cm = createCleanupManager();
|
|
2107
|
+
const logger = LoggerService().create("spawn");
|
|
2108
|
+
const { dbg: dbg2, warn: warn2, error: error2 } = logger;
|
|
2109
|
+
const port = cfg.port || await (async () => {
|
|
2110
|
+
const [defaultPort, freeDefaultPort] = await PortService().alloc();
|
|
2111
|
+
cm.add(freeDefaultPort);
|
|
2112
|
+
return defaultPort;
|
|
2113
|
+
})();
|
|
2114
|
+
const _cfg = {
|
|
2115
|
+
version: maxVersion,
|
|
2116
|
+
port,
|
|
2117
|
+
extraBinds: [],
|
|
2118
|
+
env: {},
|
|
2119
|
+
stderr: new MemoryStream(),
|
|
2120
|
+
stdout: new MemoryStream(),
|
|
2121
|
+
dev: false,
|
|
2122
|
+
...cfg
|
|
2123
|
+
};
|
|
2124
|
+
const {
|
|
2125
|
+
version: version2,
|
|
2126
|
+
subdomain,
|
|
2127
|
+
instanceId,
|
|
2128
|
+
extraBinds,
|
|
2129
|
+
env,
|
|
2130
|
+
stderr,
|
|
2131
|
+
stdout,
|
|
2132
|
+
dev
|
|
2133
|
+
} = _cfg;
|
|
2134
|
+
logger.breadcrumb(subdomain).breadcrumb(instanceId);
|
|
2135
|
+
const iLogger = SyslogLogger(instanceId, "exec");
|
|
2136
|
+
const _version = version2 || maxVersion;
|
|
2137
|
+
const realVersion = await getVersion(_version);
|
|
2138
|
+
const binPath = realVersion.binPath;
|
|
2139
|
+
if (!existsSync6(binPath)) {
|
|
2140
|
+
throw new Error(
|
|
2141
|
+
`PocketBase binary (${binPath}) not found. Contact pockethost.io.`
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
const docker = new Docker();
|
|
2145
|
+
iLogger.info(`Starting instance`);
|
|
2146
|
+
const _stdoutData = (data) => {
|
|
2147
|
+
dbg2(data.toString());
|
|
2148
|
+
};
|
|
2149
|
+
stdout.on("data", _stdoutData);
|
|
2150
|
+
const _stdErrData = (data) => {
|
|
2151
|
+
dbg2(data.toString());
|
|
2152
|
+
};
|
|
2153
|
+
stderr.on("data", _stdErrData);
|
|
2154
|
+
const Binds = [
|
|
2155
|
+
`${mkInstanceDataPath(instanceId)}:${mkContainerHomePath()}`,
|
|
2156
|
+
`${binPath}:${mkContainerHomePath(`pocketbase`)}:ro`
|
|
2157
|
+
];
|
|
2158
|
+
if (extraBinds.length > 0) {
|
|
2159
|
+
Binds.push(...extraBinds);
|
|
2160
|
+
}
|
|
2161
|
+
const createOptions = {
|
|
2162
|
+
Image: INSTANCE_IMAGE_NAME,
|
|
2163
|
+
Env: map3(
|
|
2164
|
+
{
|
|
2165
|
+
...env,
|
|
2166
|
+
DEV: dev && gte(realVersion.version, `0.20.1`),
|
|
2167
|
+
PH_APEX_DOMAIN: APEX_DOMAIN()
|
|
2168
|
+
},
|
|
2169
|
+
(v, k) => `${k}=${v}`
|
|
2170
|
+
),
|
|
2171
|
+
name: `${subdomain}-${+/* @__PURE__ */ new Date()}`,
|
|
2172
|
+
HostConfig: {
|
|
2173
|
+
AutoRemove: true,
|
|
2174
|
+
PortBindings: {
|
|
2175
|
+
"8090/tcp": [{ HostPort: `${port}` }]
|
|
2176
|
+
},
|
|
2177
|
+
Binds,
|
|
2178
|
+
Ulimits: [
|
|
2179
|
+
{
|
|
2180
|
+
Name: "nofile",
|
|
2181
|
+
Soft: 1024,
|
|
2182
|
+
Hard: 4096
|
|
2183
|
+
}
|
|
2184
|
+
],
|
|
2185
|
+
LogConfig: {
|
|
2186
|
+
Type: "syslog",
|
|
2187
|
+
Config: {
|
|
2188
|
+
"syslog-address": `udp://${DOCKER_CONTAINER_HOST()}:${SYSLOGD_PORT()}`,
|
|
2189
|
+
tag: instanceId
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
},
|
|
2193
|
+
Tty: false,
|
|
2194
|
+
ExposedPorts: {
|
|
2195
|
+
[`8090/tcp`]: {}
|
|
2196
|
+
}
|
|
2197
|
+
// User: 'pockethost',
|
|
2198
|
+
};
|
|
2199
|
+
createOptions.Cmd = ["node", `index.mjs`];
|
|
2200
|
+
createOptions.WorkingDir = `/bootstrap`;
|
|
2201
|
+
createOptions.Cmd = ["./pocketbase", `serve`, `--http`, `0.0.0.0:8090`];
|
|
2202
|
+
if (dev) {
|
|
2203
|
+
createOptions.Cmd.push(`--dev`);
|
|
2204
|
+
}
|
|
2205
|
+
createOptions.WorkingDir = `/home/pockethost`;
|
|
2206
|
+
logger.info(`Spawning ${instanceId}`);
|
|
2207
|
+
dbg2({ createOptions });
|
|
2208
|
+
let container = void 0;
|
|
2209
|
+
let started = false;
|
|
2210
|
+
let stopped = false;
|
|
2211
|
+
const exitCode = new Promise(async (resolveExit) => {
|
|
2212
|
+
new Promise((resolveContainer, rejectContainer) => {
|
|
2213
|
+
docker.run(
|
|
2214
|
+
INSTANCE_IMAGE_NAME,
|
|
2215
|
+
[""],
|
|
2216
|
+
// Supplied by createOptions
|
|
2217
|
+
[stdout, stderr],
|
|
2218
|
+
createOptions,
|
|
2219
|
+
(err, data) => {
|
|
2220
|
+
const { StatusCode } = data || {};
|
|
2221
|
+
dbg2({ err, data });
|
|
2222
|
+
container = void 0;
|
|
2223
|
+
stopped = true;
|
|
2224
|
+
unsub();
|
|
2225
|
+
if (StatusCode > 0 && StatusCode < 125 || err) {
|
|
2226
|
+
iLogger.error(
|
|
2227
|
+
`Unexpected stop with code ${StatusCode} and error ${err}`
|
|
2228
|
+
);
|
|
2229
|
+
error2(
|
|
2230
|
+
`${instanceId} stopped unexpectedly with code ${StatusCode} and error ${err}`
|
|
2231
|
+
);
|
|
2232
|
+
resolveExit(StatusCode || 999);
|
|
2233
|
+
rejectContainer();
|
|
2234
|
+
} else {
|
|
2235
|
+
resolveExit(0);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
).on("container", (container2) => {
|
|
2239
|
+
dbg2(`Got container`, container2);
|
|
2240
|
+
started = true;
|
|
2241
|
+
resolveContainer(container2);
|
|
2242
|
+
});
|
|
2243
|
+
}).then((_c) => {
|
|
2244
|
+
dbg2(`Container has been assigned`);
|
|
2245
|
+
container = _c;
|
|
2246
|
+
}).catch(() => {
|
|
2247
|
+
dbg2(`Container will not be assigned`);
|
|
2248
|
+
iLogger.error(`Could not start container`);
|
|
2249
|
+
error2(`${instanceId} could not start container`);
|
|
2250
|
+
});
|
|
2251
|
+
});
|
|
2252
|
+
exitCode.then((code) => {
|
|
2253
|
+
iLogger.info(`Process exited with code ${code}`);
|
|
2254
|
+
});
|
|
2255
|
+
const url = mkInternalUrl(port);
|
|
2256
|
+
logger.breadcrumb(url);
|
|
2257
|
+
dbg2(`Making exit hook for ${url}`);
|
|
2258
|
+
const unsub = asyncExitHook(async () => {
|
|
2259
|
+
dbg2(`Exiting process ${instanceId}`);
|
|
2260
|
+
await api.kill();
|
|
2261
|
+
dbg2(`Process ${instanceId} exited`);
|
|
2262
|
+
});
|
|
2263
|
+
await tryFetch(`${url}/api/health`, {
|
|
2264
|
+
preflight: async () => {
|
|
2265
|
+
dbg2({ stopped, started, container: !!container });
|
|
2266
|
+
if (stopped)
|
|
2267
|
+
throw new Error(`Container stopped`);
|
|
2268
|
+
return started && !!container;
|
|
2269
|
+
}
|
|
2270
|
+
});
|
|
2271
|
+
const api = {
|
|
2272
|
+
url,
|
|
2273
|
+
exitCode,
|
|
2274
|
+
kill: async () => {
|
|
2275
|
+
dbg2(`Killing`);
|
|
2276
|
+
unsub();
|
|
2277
|
+
if (!container) {
|
|
2278
|
+
dbg2(`Already exited`);
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
iLogger.info(`Stopping instance`);
|
|
2282
|
+
dbg2(`Stopping instance`);
|
|
2283
|
+
await container.stop({ signal: `SIGKILL` }).catch(error2);
|
|
2284
|
+
dbg2(`Instance stopped`);
|
|
2285
|
+
const code = await exitCode;
|
|
2286
|
+
dbg2(`Instance exited with ${code}`);
|
|
2287
|
+
iLogger.info(`Instance stopped`);
|
|
2288
|
+
stderr.off("data", _stdErrData);
|
|
2289
|
+
stdout.off("data", _stdoutData);
|
|
2290
|
+
container = void 0;
|
|
2291
|
+
await cm.shutdown();
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
2294
|
+
return api;
|
|
2295
|
+
};
|
|
2296
|
+
return {
|
|
2297
|
+
spawn: _spawn
|
|
2298
|
+
};
|
|
2299
|
+
};
|
|
2300
|
+
var PocketbaseService = mkSingleton(createPocketbaseService);
|
|
2301
|
+
|
|
2302
|
+
// src/services/PocketbaseReleaseDownloadService/index.ts
|
|
2303
|
+
import { keys as keys2 } from "@s-libs/micro-dash";
|
|
2304
|
+
import Bottleneck3 from "bottleneck";
|
|
2305
|
+
import { chmodSync as chmodSync3, existsSync as existsSync7, writeFileSync as writeFileSync2 } from "fs";
|
|
2306
|
+
import { join as join5 } from "path";
|
|
2307
|
+
import { rsort as rsort2 } from "semver";
|
|
2308
|
+
|
|
2309
|
+
// src/services/PocketbaseReleaseDownloadService/expandAndSortSemVers.ts
|
|
2310
|
+
function compareSemVer(a, b) {
|
|
2311
|
+
let splitA = a.split(".").map((x) => x === "*" ? Number.MAX_SAFE_INTEGER : parseInt(x));
|
|
2312
|
+
let splitB = b.split(".").map((x) => x === "*" ? Number.MAX_SAFE_INTEGER : parseInt(x));
|
|
2313
|
+
for (let i = 0; i < 3; i++) {
|
|
2314
|
+
if (splitA[i] !== splitB[i]) {
|
|
2315
|
+
return splitB[i] - splitA[i];
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
return 0;
|
|
2319
|
+
}
|
|
2320
|
+
function expandAndSortSemVers(semvers) {
|
|
2321
|
+
let expandedVersions = /* @__PURE__ */ new Set();
|
|
2322
|
+
const addWildcards = (version2) => {
|
|
2323
|
+
const parts = version2.split(".");
|
|
2324
|
+
if (parts.length === 3) {
|
|
2325
|
+
if (parts[0] !== "0")
|
|
2326
|
+
expandedVersions.add(`${parts[0]}.*.*`);
|
|
2327
|
+
expandedVersions.add(`${parts[0]}.${parts[1]}.*`);
|
|
2328
|
+
if (parts[0] === "0" && parts[1] !== "0")
|
|
2329
|
+
expandedVersions.add(`0.${parts[1]}.*`);
|
|
2330
|
+
}
|
|
2331
|
+
};
|
|
2332
|
+
semvers.forEach((version2) => {
|
|
2333
|
+
expandedVersions.add(version2);
|
|
2334
|
+
addWildcards(version2);
|
|
2335
|
+
});
|
|
2336
|
+
return Array.from(expandedVersions).sort(compareSemVer);
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
// src/services/PocketbaseReleaseDownloadService/index.ts
|
|
2340
|
+
var PocketbaseReleaseDownloadService = mkSingleton(
|
|
2341
|
+
(config = {}) => {
|
|
2342
|
+
const { binCachePath, versionsCachePath, onlyOne } = mergeConfig(
|
|
2343
|
+
{
|
|
2344
|
+
binCachePath: PH_BIN_CACHE(),
|
|
2345
|
+
versionsCachePath: PH_VERSIONS(),
|
|
2346
|
+
onlyOne: false
|
|
2347
|
+
},
|
|
2348
|
+
config
|
|
2349
|
+
);
|
|
2350
|
+
const _serviceLogger = LoggerService().create(
|
|
2351
|
+
"PocketbaseReleaseDownloadService"
|
|
2352
|
+
);
|
|
2353
|
+
const { dbg, info, error, warn, abort } = _serviceLogger;
|
|
2354
|
+
dbg(`Initializing`);
|
|
2355
|
+
const osName = "linux";
|
|
2356
|
+
const cpuArchitecture = process.arch === "x64" ? "amd64" : process.arch;
|
|
2357
|
+
dbg({ osName, cpuArchitecture });
|
|
2358
|
+
const binPaths = {};
|
|
2359
|
+
let maxVersion = "";
|
|
2360
|
+
const check = async () => {
|
|
2361
|
+
info(`Fetching info for PocketBase releases...`);
|
|
2362
|
+
let releases = await smartFetch(
|
|
2363
|
+
`https://api.github.com/repos/pocketbase/pocketbase/releases?per_page=100`,
|
|
2364
|
+
join5(binCachePath, `releases.json`)
|
|
2365
|
+
);
|
|
2366
|
+
const filteredReleases = releases.filter((release) => !release.prerelease).map((release) => {
|
|
2367
|
+
const { tag_name, assets } = release;
|
|
2368
|
+
const sanitizedTagName = tag_name.slice(1);
|
|
2369
|
+
const path = join5(binCachePath, tag_name);
|
|
2370
|
+
const url = assets.find((v) => {
|
|
2371
|
+
return v.name.includes(osName) && v.name.includes(cpuArchitecture);
|
|
2372
|
+
})?.browser_download_url;
|
|
2373
|
+
return { url, sanitizedTagName, path };
|
|
2374
|
+
}).filter(
|
|
2375
|
+
(release) => !!release.url
|
|
2376
|
+
);
|
|
2377
|
+
if (filteredReleases.length === 0)
|
|
2378
|
+
return;
|
|
2379
|
+
const limiter = new Bottleneck3({ maxConcurrent: 5 });
|
|
2380
|
+
const promises = (onlyOne ? [filteredReleases[0]] : filteredReleases).map(
|
|
2381
|
+
(release) => limiter.schedule(async () => {
|
|
2382
|
+
const { url, sanitizedTagName, path } = release;
|
|
2383
|
+
const binPath = join5(path, `pocketbase`);
|
|
2384
|
+
info(`Checking ${binPath}`);
|
|
2385
|
+
if (existsSync7(binPath)) {
|
|
2386
|
+
chmodSync3(binPath, 509);
|
|
2387
|
+
} else {
|
|
2388
|
+
info(`Downloading ${url}...`);
|
|
2389
|
+
await downloadAndExtract(url, binPath, _serviceLogger);
|
|
2390
|
+
}
|
|
2391
|
+
binPaths[sanitizedTagName] = binPath;
|
|
2392
|
+
})
|
|
2393
|
+
);
|
|
2394
|
+
await Promise.all(promises);
|
|
2395
|
+
console.log(`***keys`, keys2(binPaths));
|
|
2396
|
+
const sortedSemVers = expandAndSortSemVers(keys2(binPaths));
|
|
2397
|
+
writeFileSync2(
|
|
2398
|
+
versionsCachePath,
|
|
2399
|
+
`module.exports = ${JSON.stringify({ versions: sortedSemVers })}`
|
|
2400
|
+
);
|
|
2401
|
+
info(`Saved to ${versionsCachePath}`, sortedSemVers);
|
|
2402
|
+
if (keys2(binPaths).length === 0) {
|
|
2403
|
+
throw new Error(
|
|
2404
|
+
`No version found, probably mismatched architecture and OS (${osName}/${cpuArchitecture})`
|
|
2405
|
+
);
|
|
2406
|
+
}
|
|
2407
|
+
maxVersion = `~${rsort2(keys2(binPaths))[0]}`;
|
|
2408
|
+
info(`Highest PocketBase version is ${maxVersion}`);
|
|
2409
|
+
};
|
|
2410
|
+
return {
|
|
2411
|
+
check
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
);
|
|
2415
|
+
|
|
2416
|
+
// src/services/PortService.ts
|
|
2417
|
+
import getPort from "get-port";
|
|
2418
|
+
var PortService = mkSingleton((config = {}) => {
|
|
2419
|
+
const { maxPorts } = mergeConfig(
|
|
2420
|
+
{ maxPorts: INITIAL_PORT_POOL_SIZE() },
|
|
2421
|
+
config
|
|
2422
|
+
);
|
|
2423
|
+
return PromiseAllocator(maxPorts, getPort);
|
|
2424
|
+
});
|
|
2425
|
+
|
|
2426
|
+
// src/services/ProxyService.ts
|
|
2427
|
+
import cors from "cors";
|
|
2428
|
+
import express from "express";
|
|
2429
|
+
import "express-async-errors";
|
|
2430
|
+
import { default as httpProxy } from "http-proxy";
|
|
2431
|
+
var proxyService = mkSingleton(async (config) => {
|
|
2432
|
+
const _proxyLogger = LoggerService().create("ProxyService");
|
|
2433
|
+
const { dbg, error, info, trace, warn } = _proxyLogger;
|
|
2434
|
+
const { coreInternalUrl } = config;
|
|
2435
|
+
const proxy2 = httpProxy.createProxyServer({});
|
|
2436
|
+
proxy2.on("error", (err, req, res, target) => {
|
|
2437
|
+
warn(`Proxy error ${err} on ${req.url} (${req.headers.host})`);
|
|
2438
|
+
});
|
|
2439
|
+
const server = express();
|
|
2440
|
+
server.use(cors());
|
|
2441
|
+
server.get("/_api/health", (req, res, next) => {
|
|
2442
|
+
res.json({ status: "ok" });
|
|
2443
|
+
res.end;
|
|
2444
|
+
});
|
|
2445
|
+
server.use((req, res, next) => {
|
|
2446
|
+
const host = req.headers.host;
|
|
2447
|
+
if (!host) {
|
|
2448
|
+
throw new Error(`Host not found`);
|
|
2449
|
+
}
|
|
2450
|
+
res.locals.host = host;
|
|
2451
|
+
next();
|
|
2452
|
+
});
|
|
2453
|
+
server.use((req, res, next) => {
|
|
2454
|
+
res.locals.coreInternalUrl = coreInternalUrl;
|
|
2455
|
+
next();
|
|
2456
|
+
});
|
|
2457
|
+
server.use((req, res, next) => {
|
|
2458
|
+
const url = new URL(`http://${req.headers.host}${req.url}`);
|
|
2459
|
+
const country = req.headers["cf-ipcountry"] || "<ct>";
|
|
2460
|
+
const ip = req.headers["x-forwarded-for"] || "<ip>";
|
|
2461
|
+
const method = req.method || "<m>";
|
|
2462
|
+
const sig = [
|
|
2463
|
+
method.padStart(10),
|
|
2464
|
+
country.padStart(5),
|
|
2465
|
+
ip.padEnd(45),
|
|
2466
|
+
url.toString()
|
|
2467
|
+
].join(" ");
|
|
2468
|
+
info(`Incoming request ${sig}`);
|
|
2469
|
+
res.locals.sig = sig;
|
|
2470
|
+
next();
|
|
2471
|
+
});
|
|
2472
|
+
server.use((req, res, next) => {
|
|
2473
|
+
res.locals.proxy = proxy2;
|
|
2474
|
+
next();
|
|
2475
|
+
});
|
|
2476
|
+
server.listen(DAEMON_PORT(), () => {
|
|
2477
|
+
info(`daemon listening on port ${DAEMON_PORT()}`);
|
|
2478
|
+
});
|
|
2479
|
+
asyncExitHook(async () => {
|
|
2480
|
+
info(`Shutting down proxy server`);
|
|
2481
|
+
});
|
|
2482
|
+
const use = server.use.bind(server);
|
|
2483
|
+
return { use };
|
|
2484
|
+
});
|
|
2485
|
+
|
|
2486
|
+
// src/services/RealtimeLog.ts
|
|
2487
|
+
import { text } from "node:stream/consumers";
|
|
2488
|
+
import pocketbaseEs2 from "pocketbase";
|
|
2489
|
+
var mkEvent = (name, data) => {
|
|
2490
|
+
return `event: ${name}
|
|
2491
|
+
data: ${JSON.stringify(data)}
|
|
2492
|
+
|
|
2493
|
+
`;
|
|
2494
|
+
};
|
|
2495
|
+
var realtimeLog = mkSingleton(async (config) => {
|
|
2496
|
+
;
|
|
2497
|
+
(await proxyService()).use("/logs", async (req, res, next) => {
|
|
2498
|
+
const logger = LoggerService().create(`RealtimeLogger`);
|
|
2499
|
+
const { dbg, error, trace } = logger;
|
|
2500
|
+
dbg(`Got a logging request`);
|
|
2501
|
+
const { coreInternalUrl } = res.locals;
|
|
2502
|
+
dbg(`Got a log request`);
|
|
2503
|
+
const parsed = new URL(req.url, `https://${req.headers.host}`);
|
|
2504
|
+
const json = await text(req);
|
|
2505
|
+
dbg(`JSON payload is`, json);
|
|
2506
|
+
const payload = JSON.parse(json);
|
|
2507
|
+
dbg(`Parsed payload is`, parsed);
|
|
2508
|
+
const { instanceId, auth, n: nInitialRecords } = payload;
|
|
2509
|
+
if (!instanceId) {
|
|
2510
|
+
throw new Error(`instanceId query param required in ${req.url}`);
|
|
2511
|
+
}
|
|
2512
|
+
if (!auth) {
|
|
2513
|
+
throw new Error(`Expected 'auth' query param, but found ${req.url}`);
|
|
2514
|
+
}
|
|
2515
|
+
const client = new pocketbaseEs2(coreInternalUrl);
|
|
2516
|
+
client.authStore.loadFromCookie(auth);
|
|
2517
|
+
dbg(`Cookie here is`, client.authStore.isValid);
|
|
2518
|
+
await client.collection("users").authRefresh();
|
|
2519
|
+
if (!client.authStore.isValid) {
|
|
2520
|
+
throw new Error(`Cookie is invalid her`);
|
|
2521
|
+
}
|
|
2522
|
+
const user = client.authStore.model;
|
|
2523
|
+
if (!user) {
|
|
2524
|
+
throw new Error(`Valid user expected here`);
|
|
2525
|
+
}
|
|
2526
|
+
dbg(`Cookie auth passed)`);
|
|
2527
|
+
dbg(`Got a log request for instance ID ${instanceId}`);
|
|
2528
|
+
const instance = await client.collection("instances").getOne(instanceId);
|
|
2529
|
+
if (!instance) {
|
|
2530
|
+
throw new Error(`instanceId ${instanceId} not found for user ${user.id}`);
|
|
2531
|
+
}
|
|
2532
|
+
dbg(`Instance is `, instance);
|
|
2533
|
+
const instanceLogger = InstanceLogger(instanceId, `exec`);
|
|
2534
|
+
res.writeHead(200, {
|
|
2535
|
+
"Content-Type": "text/event-stream; charset=UTF-8",
|
|
2536
|
+
Connection: "keep-alive",
|
|
2537
|
+
"Cache-Control": "no-store"
|
|
2538
|
+
});
|
|
2539
|
+
const unsub = instanceLogger.tail(100, (entry) => {
|
|
2540
|
+
const evt = mkEvent(`log`, entry);
|
|
2541
|
+
res.write(evt);
|
|
2542
|
+
});
|
|
2543
|
+
res.on("close", () => {
|
|
2544
|
+
unsub();
|
|
2545
|
+
instanceLogger.shutdown();
|
|
2546
|
+
});
|
|
2547
|
+
});
|
|
2548
|
+
return {};
|
|
2549
|
+
});
|
|
2550
|
+
|
|
2551
|
+
// src/services/SqliteService/index.ts
|
|
2552
|
+
import knex from "knex";
|
|
2553
|
+
var SqliteService = mkSingleton((config) => {
|
|
2554
|
+
const { dbg, trace } = LoggerService().create(`sqliteService`);
|
|
2555
|
+
const connections = {};
|
|
2556
|
+
const cm = createCleanupManager();
|
|
2557
|
+
const _unsafe_getDatabase = async (filename) => {
|
|
2558
|
+
const _dbLogger = LoggerService().create(`SqliteService`);
|
|
2559
|
+
_dbLogger.breadcrumb(filename);
|
|
2560
|
+
const { dbg: dbg2, error, abort } = _dbLogger;
|
|
2561
|
+
trace(`Fetching database`, connections);
|
|
2562
|
+
if (!connections[filename]) {
|
|
2563
|
+
dbg2(`Not yet opened`);
|
|
2564
|
+
const api = await (async () => {
|
|
2565
|
+
dbg2(`Opening ${filename}`);
|
|
2566
|
+
const db = knex({
|
|
2567
|
+
client: "sqlite3",
|
|
2568
|
+
// or 'better-sqlite3'
|
|
2569
|
+
connection: {
|
|
2570
|
+
filename
|
|
2571
|
+
}
|
|
2572
|
+
});
|
|
2573
|
+
cm.add(() => {
|
|
2574
|
+
dbg2(`Closing connection`);
|
|
2575
|
+
db.destroy();
|
|
2576
|
+
});
|
|
2577
|
+
return db;
|
|
2578
|
+
})().catch(error);
|
|
2579
|
+
if (!api) {
|
|
2580
|
+
throw new Error(`Unable to connect to SQLite`);
|
|
2581
|
+
}
|
|
2582
|
+
connections[filename] = api;
|
|
2583
|
+
}
|
|
2584
|
+
return connections[filename];
|
|
2585
|
+
};
|
|
2586
|
+
const getDatabase = serialAsyncExecutionGuard(
|
|
2587
|
+
_unsafe_getDatabase,
|
|
2588
|
+
(fileName) => fileName
|
|
2589
|
+
);
|
|
2590
|
+
const shutdown = async () => {
|
|
2591
|
+
dbg(`Shutting down sqlite service`);
|
|
2592
|
+
await cm.shutdown();
|
|
2593
|
+
};
|
|
2594
|
+
return {
|
|
2595
|
+
getDatabase,
|
|
2596
|
+
shutdown
|
|
2597
|
+
};
|
|
2598
|
+
});
|
|
2599
|
+
|
|
2600
|
+
// src/cli/commands/DownloadCommand/index.ts
|
|
2601
|
+
import { Command } from "commander";
|
|
2602
|
+
var DownloadCommand = () => {
|
|
2603
|
+
const cmd = new Command(`download`).description(`Download all PocketBase versions`).action(async (options) => {
|
|
2604
|
+
const logger = LoggerService().create(`DownloadCommand`);
|
|
2605
|
+
const { dbg, error, info, warn } = logger;
|
|
2606
|
+
info(`Starting`);
|
|
2607
|
+
const { check } = PocketbaseReleaseDownloadService({});
|
|
2608
|
+
await check();
|
|
2609
|
+
});
|
|
2610
|
+
return cmd;
|
|
2611
|
+
};
|
|
2612
|
+
|
|
2613
|
+
// src/cli/commands/EdgeDaemonServeCommand/index.ts
|
|
2614
|
+
import { Command as Command2 } from "commander";
|
|
2615
|
+
|
|
2616
|
+
// src/cli/commands/EdgeDaemonServeCommand/serve.ts
|
|
2617
|
+
async function serve(info) {
|
|
2618
|
+
tryFetch(`${MOTHERSHIP_INTERNAL_URL(`/api/health`)}`, {});
|
|
2619
|
+
const udService = await PocketbaseReleaseVersionService({
|
|
2620
|
+
cachePath: PH_BIN_CACHE(),
|
|
2621
|
+
checkIntervalMs: 1e3 * 5 * 60
|
|
2622
|
+
});
|
|
2623
|
+
await PortService({});
|
|
2624
|
+
await PocketbaseService({});
|
|
2625
|
+
info(`Serving`);
|
|
2626
|
+
await MothershipAdminClientService({
|
|
2627
|
+
url: MOTHERSHIP_INTERNAL_URL(),
|
|
2628
|
+
username: MOTHERSHIP_ADMIN_USERNAME(),
|
|
2629
|
+
password: MOTHERSHIP_ADMIN_PASSWORD()
|
|
2630
|
+
});
|
|
2631
|
+
await proxyService({
|
|
2632
|
+
coreInternalUrl: MOTHERSHIP_INTERNAL_URL()
|
|
2633
|
+
});
|
|
2634
|
+
await SqliteService({});
|
|
2635
|
+
await realtimeLog({});
|
|
2636
|
+
await instanceService({
|
|
2637
|
+
instanceApiCheckIntervalMs: 50,
|
|
2638
|
+
instanceApiTimeoutMs: 5e3
|
|
2639
|
+
});
|
|
2640
|
+
const errorHandler = (err, req, res, next) => {
|
|
2641
|
+
res.status(500).send(err.toString());
|
|
2642
|
+
};
|
|
2643
|
+
(await proxyService()).use(errorHandler);
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
// src/cli/commands/EdgeDaemonServeCommand/index.ts
|
|
2647
|
+
var EdgeDaemonServeCommand = () => {
|
|
2648
|
+
const cmd = new Command2(`edge:daemon:serve`).description(`Run an edge daemon server`).action(async (options) => {
|
|
2649
|
+
const logger = LoggerService().create(`EdgeDaemonServeCommand`);
|
|
2650
|
+
const { dbg, error, info, warn } = logger;
|
|
2651
|
+
info(`Starting`);
|
|
2652
|
+
await serve(info);
|
|
2653
|
+
info(`Hooking into process exit event`);
|
|
2654
|
+
});
|
|
2655
|
+
return cmd;
|
|
2656
|
+
};
|
|
2657
|
+
|
|
2658
|
+
// src/cli/commands/EdgeFtpServeCommand/index.ts
|
|
2659
|
+
import { Command as Command3 } from "commander";
|
|
2660
|
+
var EdgeFtpServeCommand = () => {
|
|
2661
|
+
const cmd = new Command3(`edge:ftp:serve`).description(`Run an edge FTP server`).action(async (options) => {
|
|
2662
|
+
const logger = LoggerService().create(`EdgeFtpServeCommand`);
|
|
2663
|
+
const { dbg, error, info, warn } = logger;
|
|
2664
|
+
info(`Starting`);
|
|
2665
|
+
tryFetch(`${MOTHERSHIP_INTERNAL_URL(`/api/health`)}`, {});
|
|
2666
|
+
await ftpService({
|
|
2667
|
+
mothershipUrl: MOTHERSHIP_INTERNAL_URL()
|
|
2668
|
+
});
|
|
2669
|
+
});
|
|
2670
|
+
return cmd;
|
|
2671
|
+
};
|
|
2672
|
+
|
|
2673
|
+
// src/cli/commands/EdgeSyslogCommand/index.ts
|
|
2674
|
+
import { Command as Command4 } from "commander";
|
|
2675
|
+
import * as dgram from "dgram";
|
|
2676
|
+
import parse from "syslog-parse";
|
|
2677
|
+
var EdgeSyslogServeCommand = () => {
|
|
2678
|
+
const cmd = new Command4(`edge:syslog:serve`).description(`Run an edge syslog server`).action(async (options) => {
|
|
2679
|
+
const logger = LoggerService().create(`EdgeSyslogServeCommand`);
|
|
2680
|
+
const { dbg, error, info, warn } = logger;
|
|
2681
|
+
info(`Starting`);
|
|
2682
|
+
const PORT = SYSLOGD_PORT();
|
|
2683
|
+
const HOST = "0.0.0.0";
|
|
2684
|
+
const server = dgram.createSocket("udp4");
|
|
2685
|
+
server.on("error", (err) => {
|
|
2686
|
+
console.log(`Server error:
|
|
2687
|
+
${err.stack}`);
|
|
2688
|
+
server.close();
|
|
2689
|
+
});
|
|
2690
|
+
server.on("message", (msg, rinfo) => {
|
|
2691
|
+
const raw = msg.toString();
|
|
2692
|
+
const parsed = parse(raw);
|
|
2693
|
+
if (!parsed) {
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2696
|
+
dbg(parsed);
|
|
2697
|
+
const { process: instanceId, severity, message } = parsed;
|
|
2698
|
+
const logger2 = InstanceLogger(instanceId, `exec`, { ttl: 5e3 });
|
|
2699
|
+
if (severity === "info") {
|
|
2700
|
+
logger2.info(message);
|
|
2701
|
+
} else {
|
|
2702
|
+
logger2.error(message);
|
|
2703
|
+
}
|
|
2704
|
+
});
|
|
2705
|
+
server.on("listening", () => {
|
|
2706
|
+
const address = server.address();
|
|
2707
|
+
console.log(`Server listening ${address.address}:${address.port}`);
|
|
2708
|
+
});
|
|
2709
|
+
server.bind(PORT, HOST);
|
|
2710
|
+
});
|
|
2711
|
+
return cmd;
|
|
2712
|
+
};
|
|
2713
|
+
|
|
2714
|
+
// src/cli/commands/HealthCommand/index.ts
|
|
2715
|
+
import { Command as Command5 } from "commander";
|
|
2716
|
+
|
|
2717
|
+
// src/cli/commands/HealthCommand/checkHealth.ts
|
|
2718
|
+
import Bottleneck4 from "bottleneck";
|
|
2719
|
+
import { execSync } from "child_process";
|
|
2720
|
+
import fetch4 from "node-fetch";
|
|
2721
|
+
import { default as osu } from "node-os-utils";
|
|
2722
|
+
import { freemem } from "os";
|
|
2723
|
+
var checkHealth = async () => {
|
|
2724
|
+
const { cpu, drive, openfiles, proc } = osu;
|
|
2725
|
+
const DISCORD_URL = DISCORD_HEALTH_CHANNEL_URL();
|
|
2726
|
+
const { dbg, error, info, warn } = LoggerService().create("edge-health.ts");
|
|
2727
|
+
info(`Starting`);
|
|
2728
|
+
const _exec = (cmd) => execSync(cmd, { shell: "/bin/bash", maxBuffer: 1024 * 1024 * 10 }).toString().split(`
|
|
2729
|
+
`);
|
|
2730
|
+
const openFiles = _exec(`lsof -n | awk '$4 ~ /^[0-9]/ {print}'`);
|
|
2731
|
+
const [freeSpace] = _exec(`df -h / | awk 'NR==2{print $4}'`);
|
|
2732
|
+
const SAMPLE = {
|
|
2733
|
+
Command: '"docker-entrypoint.s\u2026"',
|
|
2734
|
+
CreatedAt: "2024-01-23 04:36:09 +0000 UTC",
|
|
2735
|
+
ID: "6e0921e84391",
|
|
2736
|
+
Image: "pockethost-instance",
|
|
2737
|
+
Labels: "",
|
|
2738
|
+
LocalVolumes: "0",
|
|
2739
|
+
Mounts: "/home/pocketho\u2026,/home/pocketho\u2026,/home/pocketho\u2026,/home/pocketho\u2026,/home/pocketho\u2026",
|
|
2740
|
+
Names: "kekbase-1705984569777",
|
|
2741
|
+
Networks: "bridge",
|
|
2742
|
+
Ports: "0.0.0.0:44447->8090/tcp, :::44447->8090/tcp",
|
|
2743
|
+
RunningFor: "7 hours ago",
|
|
2744
|
+
Size: "0B (virtual 146MB)",
|
|
2745
|
+
State: "running",
|
|
2746
|
+
Status: "Up 7 hours"
|
|
2747
|
+
};
|
|
2748
|
+
const containers = _exec(`docker ps --format '{{json .}}'`).filter((line) => line.trim()).map((line) => {
|
|
2749
|
+
dbg(line);
|
|
2750
|
+
return line;
|
|
2751
|
+
}).map((line) => JSON.parse(line)).map((rec) => {
|
|
2752
|
+
const name = rec.Names.replace(/-\d+/, "");
|
|
2753
|
+
const port = parseInt(rec.Ports.match(/:(\d+)/)?.[1] || "0", 10);
|
|
2754
|
+
const mem = rec.Size.match(/(\d+MB)/)?.[1] || "0MB";
|
|
2755
|
+
const created = new Date(rec.CreatedAt);
|
|
2756
|
+
return {
|
|
2757
|
+
name,
|
|
2758
|
+
priority: 0,
|
|
2759
|
+
emoji: ":octopus:",
|
|
2760
|
+
port,
|
|
2761
|
+
isHealthy: false,
|
|
2762
|
+
url: `http://localhost:${port}/api/health`,
|
|
2763
|
+
ago: rec.RunningFor,
|
|
2764
|
+
mem,
|
|
2765
|
+
created
|
|
2766
|
+
};
|
|
2767
|
+
});
|
|
2768
|
+
function getFreeMemoryInGB() {
|
|
2769
|
+
const freeMemoryBytes = freemem();
|
|
2770
|
+
const freeMemoryGB = freeMemoryBytes / Math.pow(1024, 3);
|
|
2771
|
+
return freeMemoryGB.toFixed(2);
|
|
2772
|
+
}
|
|
2773
|
+
function splitIntoChunks(lines, maxChars = 2e3) {
|
|
2774
|
+
const chunks = [];
|
|
2775
|
+
let currentChunk = "";
|
|
2776
|
+
lines.forEach((line) => {
|
|
2777
|
+
if (currentChunk.length + line.length + 1 > maxChars) {
|
|
2778
|
+
chunks.push(currentChunk);
|
|
2779
|
+
currentChunk = "";
|
|
2780
|
+
}
|
|
2781
|
+
currentChunk += line + "\n";
|
|
2782
|
+
});
|
|
2783
|
+
if (currentChunk) {
|
|
2784
|
+
chunks.push(currentChunk);
|
|
2785
|
+
}
|
|
2786
|
+
return chunks;
|
|
2787
|
+
}
|
|
2788
|
+
const limiter = new Bottleneck4({ maxConcurrent: 1 });
|
|
2789
|
+
const send = (lines) => Promise.all(
|
|
2790
|
+
splitIntoChunks(lines).map(
|
|
2791
|
+
(content) => limiter.schedule(
|
|
2792
|
+
() => fetch4(DISCORD_URL, {
|
|
2793
|
+
method: "POST",
|
|
2794
|
+
body: JSON.stringify({
|
|
2795
|
+
content
|
|
2796
|
+
}),
|
|
2797
|
+
headers: { "content-type": "application/json" }
|
|
2798
|
+
})
|
|
2799
|
+
)
|
|
2800
|
+
)
|
|
2801
|
+
);
|
|
2802
|
+
const driveInfo = await drive.info(`/`);
|
|
2803
|
+
await send([
|
|
2804
|
+
`===================`,
|
|
2805
|
+
`Server: SFO-1`,
|
|
2806
|
+
`${/* @__PURE__ */ new Date()}`,
|
|
2807
|
+
`CPUs: ${cpu.count()}`,
|
|
2808
|
+
`CPU Usage: ${await cpu.usage()}%`,
|
|
2809
|
+
`Free RAM: ${getFreeMemoryInGB()}GB`,
|
|
2810
|
+
`Free disk: ${driveInfo.freeGb}GB`,
|
|
2811
|
+
`Open files: ${openFiles.length}`,
|
|
2812
|
+
`Containers: ${containers.length}`
|
|
2813
|
+
]);
|
|
2814
|
+
const checks = [
|
|
2815
|
+
{
|
|
2816
|
+
name: `edge proxy`,
|
|
2817
|
+
priority: 10,
|
|
2818
|
+
emoji: `:park:`,
|
|
2819
|
+
isHealthy: false,
|
|
2820
|
+
url: `https://proxy.pockethost.io/_api/health`
|
|
2821
|
+
},
|
|
2822
|
+
{
|
|
2823
|
+
name: `edge daemon`,
|
|
2824
|
+
priority: 8,
|
|
2825
|
+
emoji: `:imp:`,
|
|
2826
|
+
isHealthy: false,
|
|
2827
|
+
url: `http://localhost:${DAEMON_PORT()}/_api/health`
|
|
2828
|
+
},
|
|
2829
|
+
{
|
|
2830
|
+
name: `mothership`,
|
|
2831
|
+
priority: 9,
|
|
2832
|
+
emoji: `:flying_saucer:`,
|
|
2833
|
+
isHealthy: false,
|
|
2834
|
+
url: `http://localhost:${MOTHERSHIP_PORT()}/api/health`
|
|
2835
|
+
},
|
|
2836
|
+
...containers
|
|
2837
|
+
];
|
|
2838
|
+
await Promise.all(
|
|
2839
|
+
checks.map(async (check) => {
|
|
2840
|
+
const { url } = check;
|
|
2841
|
+
dbg({ container: check });
|
|
2842
|
+
try {
|
|
2843
|
+
const res = await fetch4(url);
|
|
2844
|
+
dbg({ url, status: res.status });
|
|
2845
|
+
check.isHealthy = res.status === 200;
|
|
2846
|
+
return true;
|
|
2847
|
+
} catch (e) {
|
|
2848
|
+
dbg(`${url}: ${e}`);
|
|
2849
|
+
check.isHealthy = false;
|
|
2850
|
+
}
|
|
2851
|
+
})
|
|
2852
|
+
);
|
|
2853
|
+
dbg({ checks });
|
|
2854
|
+
await send([
|
|
2855
|
+
`---health checks---`,
|
|
2856
|
+
...checks.sort((a, b) => {
|
|
2857
|
+
if (a.priority > b.priority)
|
|
2858
|
+
return -1;
|
|
2859
|
+
if (a.priority < b.priority)
|
|
2860
|
+
return 1;
|
|
2861
|
+
const now2 = /* @__PURE__ */ new Date();
|
|
2862
|
+
const res = +(b.created || now2) - +(a.created || now2);
|
|
2863
|
+
if (res)
|
|
2864
|
+
return res;
|
|
2865
|
+
return a.name.localeCompare(b.name);
|
|
2866
|
+
}).map(({ name, isHealthy, emoji, mem, ago }) => {
|
|
2867
|
+
const isInstance = !!mem;
|
|
2868
|
+
if (isInstance) {
|
|
2869
|
+
return `${isHealthy ? ":white_check_mark:" : ":face_vomiting: "} \`${name.padStart(30)} ${(mem || "").padStart(10)} ${(ago || "").padStart(20)}\``;
|
|
2870
|
+
} else {
|
|
2871
|
+
return `${isHealthy ? ":white_check_mark:" : ":face_vomiting: "} ${name}`;
|
|
2872
|
+
}
|
|
2873
|
+
})
|
|
2874
|
+
]);
|
|
2875
|
+
};
|
|
2876
|
+
|
|
2877
|
+
// src/cli/commands/HealthCommand/index.ts
|
|
2878
|
+
var HealthCommand = () => {
|
|
2879
|
+
const cmd = new Command5(`health`).description(`Perform a health check on the PocketHost system`).action(async (options) => {
|
|
2880
|
+
const logger = LoggerService().create(`HealthCommand`);
|
|
2881
|
+
const { dbg, error, info, warn } = logger;
|
|
2882
|
+
info(`Starting`);
|
|
2883
|
+
await checkHealth();
|
|
2884
|
+
});
|
|
2885
|
+
return cmd;
|
|
2886
|
+
};
|
|
2887
|
+
|
|
2888
|
+
// src/cli/commands/MothershipCommand/index.ts
|
|
2889
|
+
import { Command as Command6 } from "commander";
|
|
2890
|
+
var MothershipCommand = () => {
|
|
2891
|
+
const cmd = new Command6(`mothership`).description(`Run the PocketHost mothership`).action(async (options) => {
|
|
2892
|
+
const logger = LoggerService().create(`Mothership`);
|
|
2893
|
+
const { dbg, error, info, warn } = logger;
|
|
2894
|
+
info(`Starting`);
|
|
2895
|
+
const udService = await PocketbaseReleaseVersionService({
|
|
2896
|
+
cachePath: PH_BIN_CACHE(),
|
|
2897
|
+
checkIntervalMs: 1e3 * 5 * 60
|
|
2898
|
+
});
|
|
2899
|
+
await PortService({});
|
|
2900
|
+
const pbService = await PocketbaseService({});
|
|
2901
|
+
info(`Serving`);
|
|
2902
|
+
const { url, exitCode } = await pbService.spawn({
|
|
2903
|
+
version: MOTHERSHIP_SEMVER(),
|
|
2904
|
+
subdomain: MOTHERSHIP_NAME(),
|
|
2905
|
+
instanceId: MOTHERSHIP_NAME(),
|
|
2906
|
+
port: MOTHERSHIP_PORT(),
|
|
2907
|
+
dev: DEBUG(),
|
|
2908
|
+
env: {
|
|
2909
|
+
DATA_ROOT: mkContainerHomePath(`data`),
|
|
2910
|
+
LS_WEBHOOK_SECRET: LS_WEBHOOK_SECRET()
|
|
2911
|
+
},
|
|
2912
|
+
extraBinds: [
|
|
2913
|
+
`${DATA_ROOT()}:${mkContainerHomePath(`data`)}`,
|
|
2914
|
+
`${MOTHERSHIP_HOOKS_DIR()}:${mkContainerHomePath(`pb_hooks`)}`,
|
|
2915
|
+
`${PH_VERSIONS()}:${mkContainerHomePath(`pb_hooks`, `versions.js`)}`,
|
|
2916
|
+
`${MOTHERSHIP_MIGRATIONS_DIR()}:${mkContainerHomePath(
|
|
2917
|
+
`pb_migrations`
|
|
2918
|
+
)}`,
|
|
2919
|
+
`${MOTHERSHIP_APP_DIR()}:${mkContainerHomePath(`ph_app`)}`
|
|
2920
|
+
]
|
|
2921
|
+
});
|
|
2922
|
+
info(`Mothership URL for this session is ${url}`);
|
|
2923
|
+
exitCode.then((c) => {
|
|
2924
|
+
gracefulExit(c);
|
|
2925
|
+
});
|
|
2926
|
+
});
|
|
2927
|
+
return cmd;
|
|
2928
|
+
};
|
|
2929
|
+
|
|
2930
|
+
// src/cli/commands/ProxyCommand/index.ts
|
|
2931
|
+
import { Command as Command7 } from "commander";
|
|
2932
|
+
|
|
2933
|
+
// src/cli/commands/ProxyCommand/proxy/server.ts
|
|
2934
|
+
import { forEach as forEach3 } from "@s-libs/micro-dash";
|
|
2935
|
+
import cors2 from "cors";
|
|
2936
|
+
import express2 from "express";
|
|
2937
|
+
import "express-async-errors";
|
|
2938
|
+
import fs3 from "fs";
|
|
2939
|
+
import http from "http";
|
|
2940
|
+
import https from "https";
|
|
2941
|
+
import { createProxyMiddleware as createProxyMiddleware2 } from "http-proxy-middleware";
|
|
2942
|
+
|
|
2943
|
+
// src/cli/commands/ProxyCommand/proxy/cidr.ts
|
|
2944
|
+
import IPCIDR from "ip-cidr";
|
|
2945
|
+
var createIpWhitelistMiddleware = (blockedCIDRs) => {
|
|
2946
|
+
const blockedCIDRObjects = blockedCIDRs.map((cidr) => new IPCIDR(cidr));
|
|
2947
|
+
return (req, res, next) => {
|
|
2948
|
+
const ip = req.ip;
|
|
2949
|
+
if (blockedCIDRObjects.some((cidr) => cidr.contains(ip))) {
|
|
2950
|
+
next();
|
|
2951
|
+
} else {
|
|
2952
|
+
res.status(403).send("Nope");
|
|
2953
|
+
}
|
|
2954
|
+
};
|
|
2955
|
+
};
|
|
2956
|
+
|
|
2957
|
+
// src/cli/commands/ProxyCommand/proxy/createVhostProxyMiddleware.ts
|
|
2958
|
+
import { createProxyMiddleware } from "http-proxy-middleware";
|
|
2959
|
+
import vhost from "vhost";
|
|
2960
|
+
function createVhostProxyMiddleware(host, target, ws = false) {
|
|
2961
|
+
console.log(`Creating ${host}->${target}`);
|
|
2962
|
+
const handler = createProxyMiddleware({ target, ws, changeOrigin: ws });
|
|
2963
|
+
return vhost(host, (_req, res, next) => {
|
|
2964
|
+
const req = _req;
|
|
2965
|
+
const method = req.method;
|
|
2966
|
+
const fullUrl = req.protocol + "://" + req.get("host") + req.originalUrl;
|
|
2967
|
+
console.log(`${method} ${fullUrl} -> ${target}`);
|
|
2968
|
+
return handler(req, res, next);
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
// src/cli/commands/ProxyCommand/proxy/server.ts
|
|
2973
|
+
var proxy = async () => {
|
|
2974
|
+
const PROD_ROUTES = {
|
|
2975
|
+
"pockethost-central.pockethost.io": `http://localhost:${MOTHERSHIP_PORT()}`
|
|
2976
|
+
};
|
|
2977
|
+
const DEV_ROUTES = {
|
|
2978
|
+
"mail.pockethost.lvh.me": `http://localhost:${1080}`,
|
|
2979
|
+
"pockethost-central.pockethost.lvh.me": `http://localhost:${MOTHERSHIP_PORT()}`,
|
|
2980
|
+
"app.pockethost.lvh.me": `http://localhost:${5174}`,
|
|
2981
|
+
"superadmin.pockethost.lvh.me": `http://localhost:${5175}`,
|
|
2982
|
+
"pockethost.lvh.me": `http://localhost:${8080}`
|
|
2983
|
+
};
|
|
2984
|
+
const hostnameRoutes = IS_DEV() ? DEV_ROUTES : PROD_ROUTES;
|
|
2985
|
+
const app = express2();
|
|
2986
|
+
app.use(cors2());
|
|
2987
|
+
app.use(createIpWhitelistMiddleware(IPCIDR_LIST()));
|
|
2988
|
+
forEach3(hostnameRoutes, (target, host) => {
|
|
2989
|
+
app.use(createVhostProxyMiddleware(host, target, IS_DEV()));
|
|
2990
|
+
});
|
|
2991
|
+
app.get(`/_api/health`, (req, res, next) => {
|
|
2992
|
+
res.json({ status: "ok" });
|
|
2993
|
+
res.end();
|
|
2994
|
+
});
|
|
2995
|
+
const handler = createProxyMiddleware2({
|
|
2996
|
+
target: `http://localhost:${DAEMON_PORT()}`
|
|
2997
|
+
});
|
|
2998
|
+
app.all(`*`, (req, res, next) => {
|
|
2999
|
+
const method = req.method;
|
|
3000
|
+
const fullUrl = req.protocol + "://" + req.get("host") + req.originalUrl;
|
|
3001
|
+
console.log(
|
|
3002
|
+
`${method} ${fullUrl} -> ${`http://localhost:${DAEMON_PORT()}`}`
|
|
3003
|
+
);
|
|
3004
|
+
handler(req, res, next);
|
|
3005
|
+
});
|
|
3006
|
+
const errorHandler = (err, req, res, next) => {
|
|
3007
|
+
res.status(500).send(err.toString());
|
|
3008
|
+
};
|
|
3009
|
+
app.use(errorHandler);
|
|
3010
|
+
if (IS_DEV()) {
|
|
3011
|
+
http.createServer(app).listen(80, () => {
|
|
3012
|
+
console.log("HTTP server running on port 80");
|
|
3013
|
+
});
|
|
3014
|
+
} else {
|
|
3015
|
+
const httpsOptions = {
|
|
3016
|
+
key: fs3.readFileSync(
|
|
3017
|
+
"/home/pockethost/pockethost/ssl/cloudflare-privkey.pem"
|
|
3018
|
+
),
|
|
3019
|
+
cert: fs3.readFileSync(
|
|
3020
|
+
"/home/pockethost/pockethost/ssl/cloudflare-origin.pem"
|
|
3021
|
+
)
|
|
3022
|
+
};
|
|
3023
|
+
https.createServer(httpsOptions, app).listen(443, () => {
|
|
3024
|
+
console.log("HTTPS server running on port 443");
|
|
3025
|
+
});
|
|
3026
|
+
}
|
|
3027
|
+
};
|
|
3028
|
+
|
|
3029
|
+
// src/cli/commands/ProxyCommand/index.ts
|
|
3030
|
+
var ProxyCommand = () => {
|
|
3031
|
+
const cmd = new Command7(`proxy`).description(`Run the root PocketHost proxy`).action(async (options) => {
|
|
3032
|
+
const logger = LoggerService().create(`ProxyCommand`);
|
|
3033
|
+
const { dbg, error, info, warn } = logger;
|
|
3034
|
+
info(`Starting`);
|
|
3035
|
+
await proxy();
|
|
3036
|
+
});
|
|
3037
|
+
return cmd;
|
|
3038
|
+
};
|
|
3039
|
+
|
|
3040
|
+
// src/cli/commands/SendMailCommand/index.ts
|
|
3041
|
+
import { Command as Command8 } from "commander";
|
|
3042
|
+
|
|
3043
|
+
// src/cli/commands/SendMailCommand/sendmail.ts
|
|
3044
|
+
import { map as map4 } from "@s-libs/micro-dash";
|
|
3045
|
+
import Bottleneck5 from "bottleneck";
|
|
3046
|
+
import { InvalidArgumentError, program } from "commander";
|
|
3047
|
+
import PocketBase2 from "pocketbase";
|
|
3048
|
+
var TBL_SENT_MESSAGES = `sent_messages`;
|
|
3049
|
+
var sendMail = async () => {
|
|
3050
|
+
const { dbg, info } = LoggerService().create(`mail.ts`);
|
|
3051
|
+
function myParseInt(value) {
|
|
3052
|
+
const parsedValue = parseInt(value, 10);
|
|
3053
|
+
if (isNaN(parsedValue)) {
|
|
3054
|
+
throw new InvalidArgumentError("Not a number.");
|
|
3055
|
+
}
|
|
3056
|
+
return parsedValue;
|
|
3057
|
+
}
|
|
3058
|
+
function interpolateString(template, dict) {
|
|
3059
|
+
return template.replace(/\{\$(\w+)\}/g, (match, key) => {
|
|
3060
|
+
dbg({ match, key });
|
|
3061
|
+
const lowerKey = key.toLowerCase();
|
|
3062
|
+
return dict.hasOwnProperty(lowerKey) ? dict[lowerKey] : match;
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
program.argument(`<messageId>`, `ID of the message to send`).option("--limit <number>", `Max messages to send`, myParseInt, 1).option("--confirm", `Really send messages`, false).action(async (messageId, { limit, confirm }) => {
|
|
3066
|
+
dbg({ messageId, confirm, limit });
|
|
3067
|
+
const { getDatabase } = SqliteService({});
|
|
3068
|
+
const db = await getDatabase(MOTHERSHIP_DATA_DB());
|
|
3069
|
+
info(MOTHERSHIP_URL());
|
|
3070
|
+
const client = new PocketBase2(MOTHERSHIP_INTERNAL_URL());
|
|
3071
|
+
await client.admins.authWithPassword(
|
|
3072
|
+
MOTHERSHIP_ADMIN_USERNAME(),
|
|
3073
|
+
MOTHERSHIP_ADMIN_PASSWORD()
|
|
3074
|
+
);
|
|
3075
|
+
const message = await client.collection(`campaign_messages`).getOne(messageId, { expand: "campaign" });
|
|
3076
|
+
const { campaign } = message.expand || {};
|
|
3077
|
+
dbg({ messageId, limit, message, campaign });
|
|
3078
|
+
const vars = {
|
|
3079
|
+
messageId
|
|
3080
|
+
};
|
|
3081
|
+
await Promise.all(
|
|
3082
|
+
map4(campaign.vars, async (sql2, k) => {
|
|
3083
|
+
const res = await db.raw(sql2);
|
|
3084
|
+
const [{ value }] = res;
|
|
3085
|
+
vars[k.toLocaleLowerCase()] = value;
|
|
3086
|
+
})
|
|
3087
|
+
);
|
|
3088
|
+
dbg({ vars });
|
|
3089
|
+
const subject = interpolateString(message.subject, vars);
|
|
3090
|
+
const body = interpolateString(message.body, vars);
|
|
3091
|
+
const sql = `SELECT u.*
|
|
3092
|
+
FROM (${campaign.usersQuery}) u
|
|
3093
|
+
LEFT JOIN sent_messages sm ON u.id = sm.user AND sm.campaign_message = '${messageId}'
|
|
3094
|
+
WHERE sm.id IS NULL;
|
|
3095
|
+
`;
|
|
3096
|
+
dbg(sql);
|
|
3097
|
+
const users = (await db.raw(sql)).slice(0, limit);
|
|
3098
|
+
const limiter = new Bottleneck5({ maxConcurrent: 1, minTime: 100 });
|
|
3099
|
+
await Promise.all(
|
|
3100
|
+
users.map((user) => {
|
|
3101
|
+
return limiter.schedule(async () => {
|
|
3102
|
+
if (!confirm) {
|
|
3103
|
+
const old = user.email;
|
|
3104
|
+
user.email = TEST_EMAIL();
|
|
3105
|
+
info(`Sending to ${user.email} masking ${old}`);
|
|
3106
|
+
} else {
|
|
3107
|
+
info(`Sending to ${user.email}`);
|
|
3108
|
+
}
|
|
3109
|
+
await client.send(`/api/mail`, {
|
|
3110
|
+
method: "POST",
|
|
3111
|
+
body: {
|
|
3112
|
+
to: user.email,
|
|
3113
|
+
subject,
|
|
3114
|
+
body: `${body}<hr/>I only send PocketHost annoucements. But I get it. <a href="https://pockethost-central.pockethost.io/api/unsubscribe?e=${user.id}">[[unsub]]</a>`
|
|
3115
|
+
}
|
|
3116
|
+
});
|
|
3117
|
+
info(`Sent`);
|
|
3118
|
+
if (confirm) {
|
|
3119
|
+
await client.collection(TBL_SENT_MESSAGES).create({
|
|
3120
|
+
user: user.id,
|
|
3121
|
+
message: messageId,
|
|
3122
|
+
campaign_message: messageId
|
|
3123
|
+
});
|
|
3124
|
+
}
|
|
3125
|
+
});
|
|
3126
|
+
})
|
|
3127
|
+
);
|
|
3128
|
+
SqliteService().shutdown();
|
|
3129
|
+
});
|
|
3130
|
+
program.parseAsync();
|
|
3131
|
+
};
|
|
3132
|
+
|
|
3133
|
+
// src/cli/commands/SendMailCommand/index.ts
|
|
3134
|
+
var SendMailCommand = () => {
|
|
3135
|
+
const cmd = new Command8(`mail:send`).description(`Send a PocketHost bulk mail`).action(async (options) => {
|
|
3136
|
+
const logger = LoggerService().create(`SendMailCommand`);
|
|
3137
|
+
const { dbg, error, info, warn } = logger;
|
|
3138
|
+
info(`Starting`);
|
|
3139
|
+
await sendMail();
|
|
3140
|
+
});
|
|
3141
|
+
return cmd;
|
|
3142
|
+
};
|
|
3143
|
+
|
|
3144
|
+
// src/cli/index.ts
|
|
3145
|
+
var { version } = package_default;
|
|
3146
|
+
DefaultSettingsService(SETTINGS);
|
|
3147
|
+
LoggerService({});
|
|
3148
|
+
var main = async () => {
|
|
3149
|
+
program2.name("pockethost").description("Multitenant PocketBase hosting").version(version);
|
|
3150
|
+
program2.addCommand(EdgeFtpServeCommand()).addCommand(MothershipCommand()).addCommand(EdgeSyslogServeCommand()).addCommand(EdgeDaemonServeCommand()).addCommand(DownloadCommand()).addCommand(HealthCommand()).addCommand(ProxyCommand()).addCommand(SendMailCommand());
|
|
3151
|
+
await program2.parseAsync();
|
|
3152
|
+
};
|
|
3153
|
+
main();
|
|
3154
|
+
export {
|
|
3155
|
+
main
|
|
3156
|
+
};
|