pinomad 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # PiNomad
2
2
 
3
- English · [中文](README.zh-CN.md)
3
+ English · [中文](README.zh-CN.md) · [pinomad.si](https://pinomad.si)
4
4
 
5
5
  A self-hosted coding agent built on [Pi Durable](https://www.npmjs.com/package/@earendil-works/pi-durable). The agent runs on your own machine; the Mac app, the web client, and the iPhone app all attach to the same runs.
6
6
 
@@ -87,8 +87,10 @@ These docs are written in Chinese.
87
87
  | [Usage](docs/usage.md) | Projects, worktrees, tools, MCP configuration |
88
88
  | [Remote access](docs/remote-access.md) | Pairing, tunnels, self-hosted relay (VPS or container platforms) |
89
89
  | [Running as a service](docs/service.md) | `service` and `upgrade` commands, platform notes |
90
+ | [Website](docs/website.md) | The pinomad.si landing page and the hosted app.pinomad.si instance |
91
+ | [Architecture overview](docs/architecture.md) | How the parts and languages fit together, with send/receive diagrams |
90
92
  | [Architecture decisions](docs/adr/README.md) | Why things are the way they are |
91
- | [Roadmap](docs/roadmap.md) | What's done and what's next |
93
+ | [Roadmap](docs/roadmap.md) | Direction and order; items live in GitHub Issues |
92
94
  | [Glossary](GLOSSARY.md) | Domain terms |
93
95
 
94
96
  ## Contributing
@@ -1,6 +1,6 @@
1
1
  // apps/host/src/distribution.ts
2
2
  import { fileURLToPath } from "node:url";
3
- var packagedVersion = "0.4.0";
3
+ var packagedVersion = "0.5.0";
4
4
  var webRoot = fileURLToPath(new URL(packagedVersion === undefined ? "../../web/dist" : "../web/", import.meta.url));
5
5
  var hostEntry = packagedVersion === undefined ? { script: fileURLToPath(new URL("./main.ts", import.meta.url)), args: [] } : { script: fileURLToPath(new URL("./pinomad.js", import.meta.url)), args: ["host"] };
6
6
  var commandHint = (name) => packagedVersion === undefined ? `bun run ${name}` : `pinomad ${name}`;
@@ -1,15 +1,13 @@
1
- import {
2
- relayWsBase
3
- } from "./chunk-knaw1a9g.js";
4
1
  import {
5
2
  RELAY_CLOSE,
6
3
  decodeRelayToHost,
7
4
  encodeRelayFrame,
8
5
  relayHostId,
6
+ relayWsBase,
9
7
  signRelayChallenge,
10
8
  signRelayChallengeV2,
11
9
  toBase64Url
12
- } from "./chunk-7gmkgyaz.js";
10
+ } from "./chunk-kmbh44dt.js";
13
11
 
14
12
  // apps/host/src/cli/server-ops.ts
15
13
  import { WebSocket } from "ws";
@@ -118,7 +116,7 @@ async function runServerOperation(options) {
118
116
  finish(() => reject(new Error(`${origin} ignored the operation — the server is too old for accounts; upgrade it first`)));
119
117
  return;
120
118
  }
121
- if (frame.t === "incoming" || frame.t === "pairing-request")
119
+ if (frame.t === "incoming" || frame.t === "pairing-request" || frame.t === "push-result")
122
120
  return;
123
121
  finish(() => resolve(frame));
124
122
  });
@@ -2,35 +2,33 @@
2
2
  import {
3
3
  serveWebClient
4
4
  } from "./chunk-r6w5aqkw.js";
5
- import {
6
- relayWsBase
7
- } from "./chunk-knaw1a9g.js";
8
- import {
9
- commandHint,
10
- webRoot
11
- } from "./chunk-fgx2sh7d.js";
12
5
  import {
13
6
  RELAY_CLOSE,
14
7
  decodeHostToRelay,
15
8
  encodeRelayFrame,
9
+ relayWsBase,
16
10
  verifyRelayChallenge,
17
11
  verifyRelayChallengeV2
18
- } from "./chunk-7gmkgyaz.js";
12
+ } from "./chunk-kmbh44dt.js";
13
+ import {
14
+ commandHint,
15
+ webRoot
16
+ } from "./chunk-1d9sppj0.js";
19
17
  import"./chunk-df32avw6.js";
20
18
 
21
- // apps/relay/src/main.ts
19
+ // apps/server/src/main.ts
22
20
  import { homedir } from "node:os";
23
21
  import { join as join2, resolve } from "node:path";
24
22
  import { parseArgs } from "node:util";
25
23
 
26
- // apps/relay/src/accounts.ts
24
+ // apps/server/src/accounts.ts
27
25
  import { createHash, randomBytes, scrypt, timingSafeEqual } from "node:crypto";
28
26
  import { mkdir } from "node:fs/promises";
29
27
  import { join } from "node:path";
30
28
  import { DatabaseSync } from "node:sqlite";
31
29
  import { promisify } from "node:util";
32
30
 
33
- // apps/relay/src/lock.ts
31
+ // apps/server/src/lock.ts
34
32
  import lockfile from "proper-lockfile";
35
33
  async function lockDataDir(dir, options) {
36
34
  const stale = Math.max(2000, options?.staleMs ?? 1e4);
@@ -44,6 +42,8 @@ async function lockDataDir(dir, options) {
44
42
  throw error;
45
43
  }
46
44
  }).catch((error) => {
45
+ if (error.code !== "ELOCKED")
46
+ throw error;
47
47
  throw new Error(`Data directory is already in use by a running process: ${dir}`, { cause: error });
48
48
  });
49
49
  return async () => {
@@ -52,7 +52,7 @@ async function lockDataDir(dir, options) {
52
52
  };
53
53
  }
54
54
 
55
- // apps/relay/src/accounts.ts
55
+ // apps/server/src/accounts.ts
56
56
  var DB_FILE = "server.sqlite";
57
57
  var SCHEMA_VERSION = 1;
58
58
  var SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
@@ -394,13 +394,220 @@ async function changePassword(options) {
394
394
  }
395
395
  }
396
396
 
397
- // apps/relay/src/relay.ts
397
+ // apps/server/src/apns.ts
398
+ import { createPrivateKey, sign } from "node:crypto";
399
+ import { connect } from "node:http2";
400
+ var AUTHORITIES = {
401
+ production: "https://api.push.apple.com",
402
+ development: "https://api.sandbox.push.apple.com"
403
+ };
404
+ var PAYLOAD_MAX_BYTES = 4096;
405
+ var EXPIRATION_SECONDS = 3600;
406
+ var TOKEN_REFRESH_MS = 45 * 60 * 1000;
407
+ var REQUEST_TIMEOUT_MS = 8000;
408
+ var SERVER_ERROR_RETRY_MS = 15 * 60 * 1000;
409
+ var base64url = (text) => Buffer.from(text, "utf8").toString("base64url");
410
+ function loadApnsConfig(env = process.env) {
411
+ const teamId = env.PINOMAD_SERVER_APNS_TEAM_ID?.trim();
412
+ const topic = env.PINOMAD_SERVER_APNS_TOPIC?.trim();
413
+ if (teamId === undefined || teamId === "" || topic === undefined || topic === "")
414
+ return;
415
+ const loadEnv = (name) => {
416
+ const keyId = env[`PINOMAD_SERVER_APNS_${name}_KEY_ID`]?.trim();
417
+ const keyPem = env[`PINOMAD_SERVER_APNS_${name}_KEY`]?.replace(/\\n/g, `
418
+ `).trim();
419
+ if (keyId === undefined && keyPem === undefined)
420
+ return;
421
+ if (keyId === undefined || keyId === "" || keyPem === undefined || keyPem === "") {
422
+ console.error(`apns: PINOMAD_SERVER_APNS_${name}_KEY and _KEY_ID must both be set — environment disabled`);
423
+ return;
424
+ }
425
+ return { keyId, keyPem };
426
+ };
427
+ const production = loadEnv("PRODUCTION");
428
+ const development = loadEnv("DEVELOPMENT");
429
+ return {
430
+ teamId,
431
+ topic,
432
+ environments: {
433
+ ...production === undefined ? {} : { production },
434
+ ...development === undefined ? {} : { development }
435
+ }
436
+ };
437
+ }
438
+ function providerToken(key, keyId, teamId, iatSeconds) {
439
+ const signingInput = `${base64url(JSON.stringify({ alg: "ES256", kid: keyId }))}.${base64url(JSON.stringify({ iss: teamId, iat: iatSeconds }))}`;
440
+ const signature = sign("sha256", Buffer.from(signingInput, "utf8"), { key, dsaEncoding: "ieee-p1363" });
441
+ return `${signingInput}.${signature.toString("base64url")}`;
442
+ }
443
+ function openApns(config) {
444
+ const states = new Map;
445
+ for (const environment of ["production", "development"]) {
446
+ const env = config.environments[environment];
447
+ if (env === undefined)
448
+ continue;
449
+ try {
450
+ states.set(environment, {
451
+ key: createPrivateKey(env.keyPem),
452
+ keyId: env.keyId,
453
+ authority: env.authority ?? AUTHORITIES[environment]
454
+ });
455
+ } catch {
456
+ console.error(`apns: the ${environment} key is not a valid EC private key — environment disabled`);
457
+ }
458
+ }
459
+ const environments = [...states.keys()];
460
+ const bearerFor = (state) => {
461
+ const now = Date.now();
462
+ if (state.token !== undefined && now - state.token.issuedAt < TOKEN_REFRESH_MS)
463
+ return state.token.jwt;
464
+ const jwt = providerToken(state.key, state.keyId, config.teamId, Math.floor(now / 1000));
465
+ state.token = { jwt, issuedAt: now };
466
+ return jwt;
467
+ };
468
+ const sessionFor = (environment, state) => {
469
+ const existing = state.session;
470
+ if (existing !== undefined && !existing.destroyed && !existing.closed)
471
+ return existing;
472
+ const session = connect(state.authority);
473
+ const drop = () => {
474
+ if (state.session === session)
475
+ state.session = undefined;
476
+ if (!session.destroyed)
477
+ session.destroy();
478
+ };
479
+ session.once("goaway", drop);
480
+ session.once("error", drop);
481
+ session.once("close", () => {
482
+ if (state.session === session)
483
+ state.session = undefined;
484
+ });
485
+ state.session = session;
486
+ return session;
487
+ };
488
+ const payloadFor = (notification) => JSON.stringify({
489
+ aps: {
490
+ alert: { title: notification.title, body: notification.body },
491
+ sound: "default",
492
+ "thread-id": notification.collapseId
493
+ },
494
+ hostKey: notification.hostKey,
495
+ conversationId: notification.conversationId
496
+ });
497
+ const classify = (environment, status, headers, body, state) => {
498
+ const reason = typeof body?.reason === "string" ? body.reason : `HTTP ${status}`;
499
+ const timestamp = body?.timestamp;
500
+ if (status === 200)
501
+ return { result: "accepted", reason: "ok" };
502
+ if (status === 410) {
503
+ return {
504
+ result: "unregistered",
505
+ reason,
506
+ unregisteredAt: typeof timestamp === "number" ? timestamp : Date.now()
507
+ };
508
+ }
509
+ if (status === 403 && reason === "ExpiredProviderToken") {
510
+ state.token = undefined;
511
+ return { result: "retry", reason };
512
+ }
513
+ if (reason === "InvalidProviderToken" || reason === "TooManyProviderTokenUpdates") {
514
+ console.error(`apns: provider token problem for ${environment}: ${reason}`);
515
+ return { result: "rejected", reason };
516
+ }
517
+ if (status === 429) {
518
+ const retryAfter = Number(headers["retry-after"]);
519
+ return {
520
+ result: "retry",
521
+ reason,
522
+ ...Number.isFinite(retryAfter) && retryAfter > 0 ? { retryAfterMs: retryAfter * 1000 } : {}
523
+ };
524
+ }
525
+ if (status >= 500)
526
+ return { result: "retry", reason, retryAfterMs: SERVER_ERROR_RETRY_MS };
527
+ return { result: "rejected", reason };
528
+ };
529
+ return {
530
+ environments,
531
+ async send(request) {
532
+ const state = states.get(request.environment);
533
+ if (state === undefined)
534
+ return { result: "unavailable", reason: "environment not configured" };
535
+ const body = payloadFor(request.notification);
536
+ if (Buffer.byteLength(body, "utf8") > PAYLOAD_MAX_BYTES) {
537
+ return { result: "rejected", reason: "PayloadTooLarge" };
538
+ }
539
+ return await new Promise((resolve) => {
540
+ let session;
541
+ try {
542
+ session = sessionFor(request.environment, state);
543
+ } catch {
544
+ resolve({ result: "retry", reason: "session unavailable" });
545
+ return;
546
+ }
547
+ let stream;
548
+ try {
549
+ stream = session.request({
550
+ ":method": "POST",
551
+ ":path": `/3/device/${request.token}`,
552
+ authorization: `bearer ${bearerFor(state)}`,
553
+ "apns-push-type": "alert",
554
+ "apns-topic": config.topic,
555
+ "apns-expiration": String(Math.floor(Date.now() / 1000) + EXPIRATION_SECONDS),
556
+ "apns-collapse-id": request.notification.collapseId,
557
+ "content-type": "application/json"
558
+ });
559
+ } catch {
560
+ resolve({ result: "retry", reason: "session unavailable" });
561
+ return;
562
+ }
563
+ let status = 0;
564
+ let responseHeaders = {};
565
+ const chunks = [];
566
+ let settled = false;
567
+ const finish = (verdict) => {
568
+ if (settled)
569
+ return;
570
+ settled = true;
571
+ clearTimeout(timer);
572
+ stream.destroy();
573
+ resolve(verdict);
574
+ };
575
+ const timer = setTimeout(() => finish({ result: "retry", reason: "apns timeout" }), REQUEST_TIMEOUT_MS);
576
+ stream.on("response", (headers) => {
577
+ status = Number(headers[":status"] ?? 0);
578
+ responseHeaders = headers;
579
+ });
580
+ stream.on("data", (chunk) => chunks.push(chunk));
581
+ stream.on("end", () => {
582
+ let body2;
583
+ try {
584
+ body2 = JSON.parse(Buffer.concat(chunks).toString("utf8"));
585
+ } catch {
586
+ body2 = undefined;
587
+ }
588
+ finish(classify(request.environment, status, responseHeaders, body2, state));
589
+ });
590
+ stream.on("error", () => finish({ result: "retry", reason: "connection error" }));
591
+ stream.on("aborted", () => finish({ result: "retry", reason: "connection aborted" }));
592
+ stream.end(body);
593
+ });
594
+ },
595
+ close() {
596
+ for (const state of states.values())
597
+ state.session?.destroy();
598
+ states.clear();
599
+ return Promise.resolve();
600
+ }
601
+ };
602
+ }
603
+
604
+ // apps/server/src/relay.ts
398
605
  import { randomBytes as randomBytes2 } from "node:crypto";
399
606
  import { once } from "node:events";
400
607
  import { createServer } from "node:http";
401
608
  import { WebSocket, WebSocketServer } from "ws";
402
609
 
403
- // apps/relay/src/api.ts
610
+ // apps/server/src/api.ts
404
611
  var SESSION_COOKIE = "__Host-pinomad-session";
405
612
  var MAX_BODY = 8 * 1024;
406
613
  var MUTATING = new Set(["POST", "PUT", "PATCH", "DELETE"]);
@@ -635,7 +842,7 @@ async function handleApiRequest(request, response, ctx) {
635
842
  deny(404, "not_found");
636
843
  }
637
844
 
638
- // apps/relay/src/relay.ts
845
+ // apps/server/src/relay.ts
639
846
  var MAX_PAYLOAD = 128 * 1024;
640
847
  var MAX_BUFFERED = 64 * 1024;
641
848
  var HIGH_WATER = 1024 * 1024;
@@ -647,11 +854,12 @@ var isSendableCloseCode = (code) => code >= 1000 && code <= 1003 || code >= 1007
647
854
  var rawLength = (data) => Array.isArray(data) ? data.reduce((sum, part) => sum + part.length, 0) : data.byteLength;
648
855
  var MAX_PAIRING_PER_HOST = 4;
649
856
  var MAX_PAIRING_PER_SESSION = 2;
857
+ var MAX_PUSH_PER_HOST = 4;
858
+ var MAX_PUSH_TOTAL = 32;
650
859
  async function startRelay(options) {
651
860
  const publicOrigin = options.publicOrigin.replace(/\/+$/, "");
652
861
  if (publicOrigin === "")
653
862
  throw new Error("publicOrigin is required");
654
- const allowedHosts = new Set(options.allowedHosts);
655
863
  const acceptTimeoutMs = options.acceptTimeoutMs ?? 1e4;
656
864
  const registerTimeoutMs = options.registerTimeoutMs ?? 1e4;
657
865
  const pingIntervalMs = options.pingIntervalMs ?? 30000;
@@ -659,6 +867,12 @@ async function startRelay(options) {
659
867
  const accounts = options.accounts;
660
868
  const trustProxy = options.trustProxy ?? false;
661
869
  const pairingTimeoutMs = options.pairingTimeoutMs ?? 1e4;
870
+ const apns = options.push;
871
+ const pushEnvironments = apns?.environments ?? [];
872
+ const pushPerHost = options.pushLimits?.perHost ?? MAX_PUSH_PER_HOST;
873
+ const pushTotal = options.pushLimits?.total ?? MAX_PUSH_TOTAL;
874
+ let pushInflightTotal = 0;
875
+ const pushInflightHosts = new Map;
662
876
  const controls = new Map;
663
877
  const pending = new Map;
664
878
  const pairingRequests = new Map;
@@ -732,6 +946,45 @@ async function startRelay(options) {
732
946
  });
733
947
  }
734
948
  const pairing = { status: pairingStatus, request: requestPairing };
949
+ function handlePush(socket, hostId, frame) {
950
+ const startedAt = Date.now();
951
+ const finish = (verdict) => {
952
+ console.log(JSON.stringify({
953
+ event: "push",
954
+ hostId,
955
+ environment: frame.environment,
956
+ result: verdict.result,
957
+ reason: verdict.reason,
958
+ ms: Date.now() - startedAt
959
+ }));
960
+ if (socket.readyState === WebSocket.OPEN) {
961
+ socket.send(encodeRelayFrame({ t: "push-result", requestId: frame.requestId, ...verdict }));
962
+ }
963
+ };
964
+ if (accounts?.hasHost(hostId) !== true) {
965
+ finish({ result: "rejected", reason: "unauthorized" });
966
+ return;
967
+ }
968
+ if (apns === undefined || !pushEnvironments.includes(frame.environment)) {
969
+ finish({ result: "unavailable", reason: "environment not configured" });
970
+ return;
971
+ }
972
+ const perHost = pushInflightHosts.get(hostId) ?? 0;
973
+ if (perHost >= pushPerHost || pushInflightTotal >= pushTotal) {
974
+ finish({ result: "retry", reason: "rate_limited" });
975
+ return;
976
+ }
977
+ pushInflightTotal += 1;
978
+ pushInflightHosts.set(hostId, perHost + 1);
979
+ apns.send({ token: frame.token, environment: frame.environment, notification: frame.notification }).then(finish, () => finish({ result: "retry", reason: "server error" })).finally(() => {
980
+ pushInflightTotal -= 1;
981
+ const left = (pushInflightHosts.get(hostId) ?? 1) - 1;
982
+ if (left <= 0)
983
+ pushInflightHosts.delete(hostId);
984
+ else
985
+ pushInflightHosts.set(hostId, left);
986
+ });
987
+ }
735
988
  function runOp(socket, frame, noisePublicKey) {
736
989
  const answer = (reply) => socket.send(encodeRelayFrame(reply), () => socket.close(1000));
737
990
  if (accounts === undefined) {
@@ -763,8 +1016,7 @@ async function startRelay(options) {
763
1016
  return;
764
1017
  }
765
1018
  answer({ t: "left" });
766
- if (!allowedHosts.has(frame.hostId))
767
- controls.get(frame.hostId)?.socket.close(RELAY_CLOSE.hostNotAllowed);
1019
+ controls.get(frame.hostId)?.socket.close(RELAY_CLOSE.hostNotAllowed);
768
1020
  }
769
1021
  function onControl(socket) {
770
1022
  track(socket);
@@ -785,6 +1037,10 @@ async function startRelay(options) {
785
1037
  socket.close(1008, "malformed frame");
786
1038
  return;
787
1039
  }
1040
+ if (answer.t === "push") {
1041
+ handlePush(socket, hostId, answer);
1042
+ return;
1043
+ }
788
1044
  if (answer.t === "pairing" || answer.t === "pairing-refused") {
789
1045
  const request = pairingRequests.get(answer.requestId);
790
1046
  if (request === undefined || request.control !== socket)
@@ -826,7 +1082,7 @@ async function startRelay(options) {
826
1082
  runOp(socket, frame, noisePublicKey);
827
1083
  return;
828
1084
  }
829
- if (!allowedHosts.has(frame.hostId) && accounts?.hasHost(frame.hostId) !== true) {
1085
+ if (accounts?.hasHost(frame.hostId) !== true) {
830
1086
  socket.close(RELAY_CLOSE.hostNotAllowed);
831
1087
  return;
832
1088
  }
@@ -841,7 +1097,7 @@ async function startRelay(options) {
841
1097
  ...frame.features === undefined ? {} : { features: frame.features }
842
1098
  });
843
1099
  accounts?.markHostSeen(hostId);
844
- socket.send(encodeRelayFrame({ t: "registered" }));
1100
+ socket.send(encodeRelayFrame({ t: "registered", push: pushEnvironments }));
845
1101
  });
846
1102
  socket.once("close", () => {
847
1103
  clearTimeout(deadline);
@@ -1016,11 +1272,12 @@ async function startRelay(options) {
1016
1272
  await closing;
1017
1273
  await new Promise((resolve) => httpServer.close(() => resolve()));
1018
1274
  await accounts?.close();
1275
+ await apns?.close();
1019
1276
  }
1020
1277
  };
1021
1278
  }
1022
1279
 
1023
- // apps/relay/src/main.ts
1280
+ // apps/server/src/main.ts
1024
1281
  function resolveDataDir(flag) {
1025
1282
  return resolve(flag ?? process.env.PINOMAD_SERVER_DATA_DIR ?? join2(homedir(), ".pinomad-server"));
1026
1283
  }
@@ -1045,7 +1302,7 @@ async function passwdMain(argv) {
1045
1302
  strict: true
1046
1303
  });
1047
1304
  if (process.stdin.isTTY) {
1048
- console.error("pipe the new password on stdin, e.g. `printenv NEW_PINOMAD_SERVER_PASSWORD | " + commandHint("relay") + " passwd`");
1305
+ console.error("pipe the new password on stdin, e.g. `printenv NEW_PINOMAD_SERVER_PASSWORD | " + commandHint("server") + " passwd`");
1049
1306
  process.exit(1);
1050
1307
  }
1051
1308
  const dataDir = resolveDataDir(values["data-dir"]);
@@ -1067,44 +1324,40 @@ async function main(argv) {
1067
1324
  port: { type: "string", default: "7430" },
1068
1325
  listen: { type: "string", default: "127.0.0.1" },
1069
1326
  "public-origin": { type: "string" },
1070
- "allow-host": { type: "string", multiple: true },
1071
1327
  "data-dir": { type: "string" },
1072
1328
  "trust-proxy": { type: "boolean", default: false }
1073
1329
  }
1074
1330
  });
1075
- const allowedHosts = values["allow-host"] ?? [];
1076
- const bootstrapUser = process.env.PINOMAD_SERVER_USER;
1077
- const bootstrapPassword = process.env.PINOMAD_SERVER_PASSWORD;
1078
- const accountMode = allowedHosts.length === 0 || values["data-dir"] !== undefined || process.env.PINOMAD_SERVER_DATA_DIR !== undefined || bootstrapUser !== undefined || bootstrapPassword !== undefined;
1079
1331
  if (values["public-origin"] === undefined) {
1080
- console.error(`usage: ${commandHint("relay")} --public-origin ORIGIN [--allow-host HOST_ID ...] [--data-dir DIR] [--trust-proxy] [--port 7430] [--listen 127.0.0.1]
1081
- ` + ` ${commandHint("relay")} passwd [--data-dir DIR] [--user NAME] (server stopped; new password on stdin)
1332
+ console.error(`usage: ${commandHint("server")} --public-origin ORIGIN [--data-dir DIR] [--trust-proxy] [--port 7430] [--listen 127.0.0.1]
1333
+ ` + ` ${commandHint("server")} passwd [--data-dir DIR] [--user NAME] (server stopped; new password on stdin)
1082
1334
  ` + " env: PINOMAD_SERVER_USER, PINOMAD_SERVER_PASSWORD, PINOMAD_SERVER_DATA_DIR");
1083
1335
  process.exit(1);
1084
1336
  }
1085
- const accounts = accountMode ? await openAccounts({
1337
+ const accounts = await openAccounts({
1086
1338
  dataDir: resolveDataDir(values["data-dir"]),
1087
- bootstrapUser,
1088
- bootstrapPassword
1339
+ bootstrapUser: process.env.PINOMAD_SERVER_USER,
1340
+ bootstrapPassword: process.env.PINOMAD_SERVER_PASSWORD
1089
1341
  }).catch((error) => {
1090
1342
  console.error(error instanceof Error ? error.message : String(error));
1091
1343
  process.exit(1);
1092
- }) : undefined;
1344
+ });
1345
+ const apnsConfig = loadApnsConfig();
1346
+ const apns = apnsConfig === undefined ? undefined : openApns(apnsConfig);
1093
1347
  const relay = await startRelay({
1094
1348
  port: Number(values.port),
1095
1349
  listenHost: values.listen,
1096
1350
  publicOrigin: values["public-origin"],
1097
- allowedHosts,
1098
1351
  webRoot,
1099
1352
  accounts,
1100
- trustProxy: values["trust-proxy"]
1353
+ trustProxy: values["trust-proxy"],
1354
+ ...apns === undefined ? {} : { push: apns }
1101
1355
  });
1102
1356
  console.log(JSON.stringify({
1103
1357
  event: "ready",
1104
1358
  url: relay.url,
1105
1359
  publicOrigin: values["public-origin"],
1106
- allowedHosts: allowedHosts.length,
1107
- accounts: accounts !== undefined
1360
+ push: apns?.environments ?? []
1108
1361
  }));
1109
1362
  let stopping = false;
1110
1363
  const stop = () => {
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  main,
4
4
  restartService
5
- } from "./chunk-akgxwg8f.js";
6
- import"./chunk-fgx2sh7d.js";
7
- import"./chunk-374zqx5h.js";
8
- import"./chunk-myjp910s.js";
5
+ } from "./chunk-aqj4wg33.js";
6
+ import"./chunk-1d9sppj0.js";
7
+ import"./chunk-m4q9wafw.js";
8
+ import"./chunk-hsp12cjt.js";
9
9
  import"./chunk-cjfmfyjb.js";
10
10
  import"./chunk-df32avw6.js";
11
11
  export {
@@ -3,31 +3,28 @@ import {
3
3
  assertSecureServerOrigin,
4
4
  probeServerAccounts,
5
5
  runServerOperation
6
- } from "./chunk-5785kwej.js";
6
+ } from "./chunk-26d04z02.js";
7
7
  import {
8
+ loadHostKey,
9
+ loadRelayKey,
8
10
  writeServerConfig
9
- } from "./chunk-302y6by9.js";
10
- import"./chunk-knaw1a9g.js";
11
+ } from "./chunk-ebfaf3k2.js";
12
+ import {
13
+ relayHostId
14
+ } from "./chunk-kmbh44dt.js";
11
15
  import {
12
16
  argValue,
13
17
  installSpec,
14
18
  restartService,
15
19
  serviceManager,
16
20
  withoutRelayArgs
17
- } from "./chunk-akgxwg8f.js";
21
+ } from "./chunk-aqj4wg33.js";
18
22
  import {
19
23
  commandHint,
20
24
  hostEntry
21
- } from "./chunk-fgx2sh7d.js";
22
- import"./chunk-374zqx5h.js";
23
- import"./chunk-myjp910s.js";
24
- import {
25
- loadHostKey,
26
- loadRelayKey
27
- } from "./chunk-837gh8nr.js";
28
- import {
29
- relayHostId
30
- } from "./chunk-7gmkgyaz.js";
25
+ } from "./chunk-1d9sppj0.js";
26
+ import"./chunk-m4q9wafw.js";
27
+ import"./chunk-hsp12cjt.js";
31
28
  import {
32
29
  DEFAULT_DATA_DIR
33
30
  } from "./chunk-cjfmfyjb.js";
@@ -2,10 +2,10 @@ import {
2
2
  commandHint,
3
3
  hostEntry,
4
4
  packagedVersion
5
- } from "./chunk-fgx2sh7d.js";
5
+ } from "./chunk-1d9sppj0.js";
6
6
  import {
7
7
  connectRemoteDurable
8
- } from "./chunk-374zqx5h.js";
8
+ } from "./chunk-m4q9wafw.js";
9
9
  import {
10
10
  DEFAULT_DATA_DIR,
11
11
  hostAddress
@@ -2,14 +2,14 @@
2
2
  import {
3
3
  restartService,
4
4
  serviceManager
5
- } from "./chunk-akgxwg8f.js";
5
+ } from "./chunk-aqj4wg33.js";
6
6
  import {
7
7
  globalPrefixOf,
8
8
  packageDir,
9
9
  packagedVersion
10
- } from "./chunk-fgx2sh7d.js";
11
- import"./chunk-374zqx5h.js";
12
- import"./chunk-myjp910s.js";
10
+ } from "./chunk-1d9sppj0.js";
11
+ import"./chunk-m4q9wafw.js";
12
+ import"./chunk-hsp12cjt.js";
13
13
  import"./chunk-cjfmfyjb.js";
14
14
  import"./chunk-df32avw6.js";
15
15