@antelopejs/dms-frontend 0.1.5 → 0.1.7

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.
@@ -9,6 +9,7 @@ exports.setupWorkspace = setupWorkspace;
9
9
  const node_child_process_1 = require("node:child_process");
10
10
  const node_crypto_1 = require("node:crypto");
11
11
  const node_fs_1 = require("node:fs");
12
+ const node_os_1 = require("node:os");
12
13
  const node_path_1 = require("node:path");
13
14
  const layers_1 = require("./layers");
14
15
  const manifest_1 = require("./manifest");
@@ -50,15 +51,77 @@ function installDeps(workspaceDir) {
50
51
  stdio: "inherit",
51
52
  });
52
53
  }
54
+ const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
55
+ const CHILD_SHUTDOWN_TIMEOUT_MS = 5000;
56
+ function exitCodeForSignal(signal) {
57
+ const number = node_os_1.constants.signals[signal];
58
+ return 128 + (typeof number === "number" ? number : 15);
59
+ }
53
60
  function runCommand(command, args, options) {
61
+ const isWindows = process.platform === "win32";
54
62
  return new Promise((resolve, reject) => {
55
63
  const child = (0, node_child_process_1.spawn)(command, args, {
56
64
  stdio: "inherit",
57
- shell: true,
65
+ shell: isWindows,
58
66
  ...options,
59
67
  });
60
- child.on("error", (error) => reject(error));
61
- child.on("exit", (code) => resolve(code || 0));
68
+ let escalation;
69
+ let forwarded;
70
+ const alive = () => child.exitCode === null && child.signalCode === null;
71
+ const stopChild = (signal) => {
72
+ if (!alive())
73
+ return;
74
+ try {
75
+ child.kill(isWindows ? "SIGTERM" : signal);
76
+ }
77
+ catch {
78
+ }
79
+ if (escalation || isWindows)
80
+ return;
81
+ escalation = setTimeout(() => {
82
+ if (alive()) {
83
+ try {
84
+ child.kill("SIGKILL");
85
+ }
86
+ catch {
87
+ }
88
+ }
89
+ }, CHILD_SHUTDOWN_TIMEOUT_MS);
90
+ escalation.unref();
91
+ };
92
+ const onSignal = (signal) => {
93
+ forwarded = signal;
94
+ stopChild(signal);
95
+ };
96
+ const onParentExit = () => {
97
+ if (alive()) {
98
+ try {
99
+ child.kill("SIGKILL");
100
+ }
101
+ catch {
102
+ }
103
+ }
104
+ };
105
+ for (const signal of FORWARDED_SIGNALS)
106
+ process.on(signal, onSignal);
107
+ process.on("exit", onParentExit);
108
+ const cleanup = () => {
109
+ if (escalation)
110
+ clearTimeout(escalation);
111
+ for (const signal of FORWARDED_SIGNALS)
112
+ process.off(signal, onSignal);
113
+ process.off("exit", onParentExit);
114
+ };
115
+ child.on("error", (error) => {
116
+ cleanup();
117
+ reject(error);
118
+ });
119
+ child.on("exit", (code, signal) => {
120
+ cleanup();
121
+ if (code !== null)
122
+ return resolve(code);
123
+ resolve(exitCodeForSignal(signal ?? forwarded ?? "SIGTERM"));
124
+ });
62
125
  });
63
126
  }
64
127
  async function setupWorkspace(opts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-frontend",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Frontend-agnostic loader for AntelopeJS DMS, shipping the Vue 3 renderer (Vite, Inertia, SSR)",
5
5
  "keywords": [
6
6
  "antelope",
@@ -82,7 +82,7 @@
82
82
  "vue-i18n": "^11.2.7"
83
83
  },
84
84
  "peerDependencies": {
85
- "@antelopejs/core": ">=1.6.0 <3"
85
+ "@antelopejs/core": ">=1.7.0 <2"
86
86
  },
87
87
  "peerDependenciesMeta": {
88
88
  "@antelopejs/core": {
@@ -50,3 +50,17 @@ export function clientIp(request) {
50
50
  const chain = [...forwarded, socketIp];
51
51
  return chain[Math.max(0, chain.length - hops - 1)] ?? socketIp;
52
52
  }
53
+
54
+ /**
55
+ * Body answered with the 403 of the CSRF origin check.
56
+ *
57
+ * The check itself is unchanged: a state-changing request must carry an
58
+ * `Origin` matching the frontend's own. The `reason` only makes the refusal
59
+ * diagnosable, since a bare `{"error":"Forbidden"}` on `POST /auth/login`
60
+ * looks exactly like bad credentials to an API client that simply forgot
61
+ * the header (curl, a server-side integration, a test harness).
62
+ */
63
+ export const CROSS_ORIGIN_ERROR = Object.freeze({
64
+ error: "Forbidden",
65
+ reason: "missing or untrusted Origin header",
66
+ });
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { backend, body, json, UpstreamError } from "./backend.mjs";
3
- import { isSameOrigin } from "./client-ip.mjs";
3
+ import { CROSS_ORIGIN_ERROR, isSameOrigin } from "./client-ip.mjs";
4
4
  import {
5
5
  oauthCallback,
6
6
  oauthHandoff,
@@ -288,8 +288,7 @@ export async function handleAuth(request, response, url) {
288
288
  response.setHeader("allow", "GET, POST, DELETE");
289
289
  return json(response, 405, { error: "Method Not Allowed" });
290
290
  }
291
- if (!isSameOrigin(request))
292
- return json(response, 403, { error: "Forbidden" });
291
+ if (!isSameOrigin(request)) return json(response, 403, CROSS_ORIGIN_ERROR);
293
292
  return request.method === "DELETE"
294
293
  ? logout(request, response)
295
294
  : refresh(request, response);
@@ -301,8 +300,9 @@ export async function handleAuth(request, response, url) {
301
300
  return match[2] === "start"
302
301
  ? oauthStart(request, response, match[1], url)
303
302
  : oauthCallback(request, response, match[1], url);
304
- if (request.method !== "POST" || !isSameOrigin(request))
305
- return json(response, 403, { error: "Forbidden" });
303
+ if (request.method !== "POST")
304
+ return json(response, 403, { error: "Forbidden", reason: "POST required" });
305
+ if (!isSameOrigin(request)) return json(response, 403, CROSS_ORIGIN_ERROR);
306
306
  if (url.pathname === "/auth/oauth/handoff")
307
307
  return oauthHandoff(request, response);
308
308
  const endpoint = PASSTHROUGH.get(url.pathname);
@@ -1,5 +1,5 @@
1
1
  import { body, json, RequestBodyError } from "./auth/backend.mjs";
2
- import { isSameOrigin } from "./auth/client-ip.mjs";
2
+ import { CROSS_ORIGIN_ERROR, isSameOrigin } from "./auth/client-ip.mjs";
3
3
  import { readSession } from "./auth/session.mjs";
4
4
 
5
5
  const RESPONSE_LIMIT = 64 * 1024;
@@ -206,8 +206,7 @@ export async function handleTester(request, response) {
206
206
  response.setHeader("allow", "POST");
207
207
  return json(response, 405, { error: "Method Not Allowed" });
208
208
  }
209
- if (!isSameOrigin(request))
210
- return json(response, 403, { error: "Forbidden" });
209
+ if (!isSameOrigin(request)) return json(response, 403, CROSS_ORIGIN_ERROR);
211
210
  try {
212
211
  const session = readSession(request);
213
212
  if (!session?.accessToken)
@@ -11,7 +11,7 @@ import {
11
11
  } from "node:zlib";
12
12
  import { ofetch } from "ofetch";
13
13
  import { RequestBodyError, UpstreamError } from "./server/auth/backend.mjs";
14
- import { isSameOrigin } from "./server/auth/client-ip.mjs";
14
+ import { CROSS_ORIGIN_ERROR, isSameOrigin } from "./server/auth/client-ip.mjs";
15
15
  import {
16
16
  handleAuth,
17
17
  publicSession,
@@ -242,7 +242,7 @@ export function requestOwnership(method, pathname) {
242
242
  async function proxy(request, response, fallbackOnNotFound = false) {
243
243
  if (!SAFE_METHODS.has(request.method) && !isSameOrigin(request)) {
244
244
  response.writeHead(403, { "content-type": JSON_TYPE });
245
- response.end(JSON.stringify({ error: "Forbidden" }));
245
+ response.end(JSON.stringify(CROSS_ORIGIN_ERROR));
246
246
  return true;
247
247
  }
248
248
  const target = new URL(request.url, process.env.DMS_API_BASE_URL);