@proagentstore/cli 0.4.31 → 0.4.33
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/dist/browser-runner/coding/engine-auth.js +55 -0
- package/dist/browser-runner/coding/engine-usage.js +79 -0
- package/dist/browser-runner/coding/headless.js +68 -1
- package/dist/browser-runner/coding/runtime.js +24 -3
- package/dist/browser-runner/index.js +6 -1
- package/dist/browser-runner/server.js +27 -2
- package/package.json +1 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// What credential the engine process ACTUALLY got — read off the merged env, on the machine
|
|
2
|
+
// where the merge happens (#248).
|
|
3
|
+
//
|
|
4
|
+
// The platform decides an engine's sign-in MODE (auto / machine / subscription / api-key) in the
|
|
5
|
+
// cloud, but the mode is only half an answer: the runner spawns the CLI with
|
|
6
|
+
// `{...process.env, ...overlay}`, so the outcome also depends on what the user's shell exports —
|
|
7
|
+
// which the cloud cannot see. "Which credential am I actually billing?" was therefore
|
|
8
|
+
// unanswerable from any surface, and the one time it went wrong it went wrong SILENTLY: a shell
|
|
9
|
+
// `ANTHROPIC_API_KEY` beat the injected subscription token, so picking "subscription" billed per
|
|
10
|
+
// token anyway (see the note on `mergeEnv`).
|
|
11
|
+
//
|
|
12
|
+
// This module derives the answer from the same env the process is spawned with, so the report is
|
|
13
|
+
// an observation, never a restatement of the setting.
|
|
14
|
+
//
|
|
15
|
+
// **Presence only, never values.** Nothing here returns, logs or echoes a key or token — the
|
|
16
|
+
// caller gets an enum. This is a transparency feature, not a credential channel.
|
|
17
|
+
/**
|
|
18
|
+
* The env var each engine reads a PER-TOKEN API key from. Deliberately duplicated from the
|
|
19
|
+
* cloud's `ENGINE_API_KEYS` (workers/api/src/lib/coding-engines.ts) rather than imported: the
|
|
20
|
+
* runner ships inside the published CLI and does not depend on the API worker — vendoring is the
|
|
21
|
+
* house rule for shared constants across packages.
|
|
22
|
+
*/
|
|
23
|
+
const API_KEY_ENV = {
|
|
24
|
+
claude: "ANTHROPIC_API_KEY",
|
|
25
|
+
gemini: "GEMINI_API_KEY",
|
|
26
|
+
codex: "OPENAI_API_KEY",
|
|
27
|
+
grok: "XAI_API_KEY",
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The env var holding a SUBSCRIPTION token. Only Claude Code has one — for every other engine
|
|
31
|
+
* "subscription" and "machine" both mean the machine's own login, so there is nothing to detect.
|
|
32
|
+
*/
|
|
33
|
+
const SUBSCRIPTION_ENV = {
|
|
34
|
+
claude: "CLAUDE_CODE_OAUTH_TOKEN",
|
|
35
|
+
};
|
|
36
|
+
/** A var counts as set only when it holds something. `mergeEnv` deletes on empty, but an
|
|
37
|
+
* inherited `FOO=` from the shell is present-and-empty and means "no credential", not one. */
|
|
38
|
+
function has(env, name) {
|
|
39
|
+
return !!name && !!env[name] && env[name].trim() !== "";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Derive what the engine actually authenticates with from its merged spawn env.
|
|
43
|
+
*
|
|
44
|
+
* API key wins over the subscription token because that is what the CLI itself does — Claude Code
|
|
45
|
+
* prefers `ANTHROPIC_API_KEY` over `CLAUDE_CODE_OAUTH_TOKEN`. Reporting "subscription" whenever a
|
|
46
|
+
* token happened to be present would reproduce the exact illusion this exists to dispel: the
|
|
47
|
+
* setting said subscription, the bill said per-token.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveEngineAuth(clientType, env) {
|
|
50
|
+
if (has(env, API_KEY_ENV[clientType]))
|
|
51
|
+
return "api-key";
|
|
52
|
+
if (has(env, SUBSCRIPTION_ENV[clientType]))
|
|
53
|
+
return "subscription";
|
|
54
|
+
return "machine-login";
|
|
55
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine spend, taken from the CLI's OWN report (#267).
|
|
3
|
+
*
|
|
4
|
+
* The usage ledger is written at three cloud-side choke points (`runUserWorkersAi`,
|
|
5
|
+
* `recordVoiceUsage`, `recordPlatformUsage`). The coding Engine passes through none of them: it
|
|
6
|
+
* is a child process on the user's machine talking straight to Anthropic. So the single largest
|
|
7
|
+
* line item for anyone using Coder in earnest was recorded nowhere, and the Usage page showed a
|
|
8
|
+
* total with no hint that it was incomplete.
|
|
9
|
+
*
|
|
10
|
+
* Claude Code already emits everything needed on the `result` event that ends each turn — token
|
|
11
|
+
* counts AND `total_cost_usd`, which it computes itself. That number is the one figure in the
|
|
12
|
+
* whole ledger that is NOT an estimate, so it is carried through as reported rather than being
|
|
13
|
+
* re-derived from `ai-pricing.ts` list prices.
|
|
14
|
+
*
|
|
15
|
+
* Raw (non-Claude) engines never reach this module: `HeadlessSession` only parses JSON in
|
|
16
|
+
* stream-json mode, so a Codex/Grok session produces no records at all. That is deliberate —
|
|
17
|
+
* a zero row would read as "this engine is free", which is a worse lie than a gap.
|
|
18
|
+
*/
|
|
19
|
+
const num = (v) => {
|
|
20
|
+
const n = Number(v);
|
|
21
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Pick the model id out of Claude Code's `modelUsage` map, which is keyed by model.
|
|
25
|
+
*
|
|
26
|
+
* A single turn can legitimately span more than one model (a subagent on a cheaper one), and the
|
|
27
|
+
* ledger has one model column per row. The most expensive one is the honest label for the row:
|
|
28
|
+
* attributing the turn to the cheap helper model would make an expensive session look cheap in
|
|
29
|
+
* the by-model breakdown, which is the exact question that breakdown exists to answer.
|
|
30
|
+
*/
|
|
31
|
+
function pickModel(modelUsage) {
|
|
32
|
+
if (!modelUsage || typeof modelUsage !== "object")
|
|
33
|
+
return "unknown";
|
|
34
|
+
let best = "";
|
|
35
|
+
let bestCost = -1;
|
|
36
|
+
for (const [model, entry] of Object.entries(modelUsage)) {
|
|
37
|
+
const cost = num(entry?.costUSD);
|
|
38
|
+
if (cost > bestCost) {
|
|
39
|
+
bestCost = cost;
|
|
40
|
+
best = model;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return best || "unknown";
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Turn a stream-json `result` event into a usage record, or null when it carries no measurement.
|
|
47
|
+
*
|
|
48
|
+
* `fallbackId` is used only when the event has no `uuid` of its own (older Claude Code builds);
|
|
49
|
+
* the caller supplies something unique per turn per process.
|
|
50
|
+
*
|
|
51
|
+
* Returns null — never a zeroed record — when there is nothing to report, for the same reason
|
|
52
|
+
* raw engines record nothing: an all-zero row is indistinguishable from "this was free".
|
|
53
|
+
*/
|
|
54
|
+
export function parseEngineUsage(ev, fallbackId) {
|
|
55
|
+
if (!ev || typeof ev !== "object")
|
|
56
|
+
return null;
|
|
57
|
+
const e = ev;
|
|
58
|
+
if (e.type !== "result")
|
|
59
|
+
return null;
|
|
60
|
+
const usage = (e.usage && typeof e.usage === "object" ? e.usage : {});
|
|
61
|
+
const inputTokens = num(usage.input_tokens);
|
|
62
|
+
const outputTokens = num(usage.output_tokens);
|
|
63
|
+
const cacheReadTokens = num(usage.cache_read_input_tokens);
|
|
64
|
+
const cacheWriteTokens = num(usage.cache_creation_input_tokens);
|
|
65
|
+
const costUsd = num(e.total_cost_usd);
|
|
66
|
+
if (!inputTokens && !outputTokens && !cacheReadTokens && !cacheWriteTokens && !costUsd)
|
|
67
|
+
return null;
|
|
68
|
+
const uuid = typeof e.uuid === "string" && e.uuid.trim() ? e.uuid.trim() : "";
|
|
69
|
+
return {
|
|
70
|
+
id: uuid || fallbackId,
|
|
71
|
+
model: pickModel(e.modelUsage),
|
|
72
|
+
inputTokens,
|
|
73
|
+
outputTokens,
|
|
74
|
+
cacheReadTokens,
|
|
75
|
+
cacheWriteTokens,
|
|
76
|
+
costUsd,
|
|
77
|
+
at: new Date().toISOString(),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { parseEngineUsage } from "./engine-usage.js";
|
|
2
3
|
/**
|
|
3
4
|
* Merge the platform's resolved engine env over the machine's, where an EMPTY value means
|
|
4
5
|
* REMOVE rather than "set to empty".
|
|
@@ -22,6 +23,16 @@ export function mergeEnv(base, overlay) {
|
|
|
22
23
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
23
24
|
import { dirname, join } from "node:path";
|
|
24
25
|
import { handlerFor } from "./handlers.js";
|
|
26
|
+
import { resolveEngineAuth } from "./engine-auth.js";
|
|
27
|
+
/**
|
|
28
|
+
* How many un-drained usage records a session holds (#267).
|
|
29
|
+
*
|
|
30
|
+
* Records are drained by the cloud on capture, which polls every 3s while a session is watched,
|
|
31
|
+
* so this only fills when nobody is looking. Dropping the OLDEST past the cap is the right
|
|
32
|
+
* direction: an unbounded queue on a long-running runner is a leak, and the alternative
|
|
33
|
+
* (refusing new records) would lose the turns that just happened rather than ancient ones.
|
|
34
|
+
*/
|
|
35
|
+
const MAX_PENDING_USAGE = 200;
|
|
25
36
|
export class HeadlessSession {
|
|
26
37
|
config;
|
|
27
38
|
/**
|
|
@@ -53,6 +64,19 @@ export class HeadlessSession {
|
|
|
53
64
|
turnStartedAt = 0;
|
|
54
65
|
/** Set by stop() — the only thing that ends a one-shot session (see `alive`). */
|
|
55
66
|
stopped = false;
|
|
67
|
+
/** Measured engine spend not yet handed to the cloud (#267). Drained by {@link takeUsage}. */
|
|
68
|
+
pendingUsage = [];
|
|
69
|
+
/** Turn counter — only used to build a fallback id when the CLI's event has no `uuid`. */
|
|
70
|
+
usageSeq = 0;
|
|
71
|
+
/**
|
|
72
|
+
* Per-PROCESS salt for that fallback id.
|
|
73
|
+
*
|
|
74
|
+
* Without it, a runner restart resets `usageSeq` and the new session's first turn reuses the
|
|
75
|
+
* id of a turn from before the restart — which the cloud's conflict-ignoring insert would
|
|
76
|
+
* silently drop, undercounting exactly the long-lived sessions this feature exists to measure.
|
|
77
|
+
* The queue is in memory and dies with the process, so a salt can never cause a double-count.
|
|
78
|
+
*/
|
|
79
|
+
usageRunId = Math.random().toString(36).slice(2, 10);
|
|
56
80
|
/**
|
|
57
81
|
* The engine binary could not be spawned (ENOENT, not executable).
|
|
58
82
|
*
|
|
@@ -62,6 +86,24 @@ export class HeadlessSession {
|
|
|
62
86
|
* all 40 BYOK decisions re-spawning a binary that isn't there.
|
|
63
87
|
*/
|
|
64
88
|
spawnFailed = false;
|
|
89
|
+
/**
|
|
90
|
+
* What this engine IS, so the question is answerable instead of implied (#248, #247).
|
|
91
|
+
*
|
|
92
|
+
* Sessions have not used tmux since the move to stream-json, but the surface kept saying
|
|
93
|
+
* `tmuxSession`/`pagsTmuxTotal` long enough that a user's reasonable next move
|
|
94
|
+
* (`tmux attach -t …`) failed and looked like a broken engine. Stating the runtime beats
|
|
95
|
+
* removing the wrong word and leaving nothing in its place.
|
|
96
|
+
*/
|
|
97
|
+
engineRuntime = "child-process";
|
|
98
|
+
/**
|
|
99
|
+
* The credential this engine actually runs on — computed from the SAME expression `spawn`
|
|
100
|
+
* uses, so it reports what happened rather than what was configured (#248). A getter, not a
|
|
101
|
+
* cached field, because it must never drift from the env the next turn is spawned with.
|
|
102
|
+
* Presence only: no key or token value leaves this class.
|
|
103
|
+
*/
|
|
104
|
+
get authResolved() {
|
|
105
|
+
return resolveEngineAuth(this.config.clientType, mergeEnv(process.env, this.config.env));
|
|
106
|
+
}
|
|
65
107
|
constructor(config) {
|
|
66
108
|
this.config = config;
|
|
67
109
|
this.engineLabel = `${config.clientType}:${config.id}`;
|
|
@@ -423,11 +465,21 @@ export class HeadlessSession {
|
|
|
423
465
|
this.push(` ↳ ${toolResult(block.content)}`); // ↳
|
|
424
466
|
}
|
|
425
467
|
break;
|
|
426
|
-
case "result":
|
|
468
|
+
case "result": {
|
|
427
469
|
if (ev.is_error)
|
|
428
470
|
this.push(`[error] ${ev.result ?? ev.subtype ?? "failed"}`);
|
|
471
|
+
// The same event that ends the turn also reports what the turn COST (#267). It was
|
|
472
|
+
// parsed and thrown away, which is why Engine spend was absent from the ledger.
|
|
473
|
+
// An errored turn still burned tokens, so this is recorded regardless of is_error.
|
|
474
|
+
const usage = parseEngineUsage(ev, `${this.config.id}:${this.usageRunId}:${this.usageSeq++}`);
|
|
475
|
+
if (usage) {
|
|
476
|
+
this.pendingUsage.push(usage);
|
|
477
|
+
if (this.pendingUsage.length > MAX_PENDING_USAGE)
|
|
478
|
+
this.pendingUsage.shift();
|
|
479
|
+
}
|
|
429
480
|
this.run = "idle"; // the turn is OVER — a fact, not a guess
|
|
430
481
|
break;
|
|
482
|
+
}
|
|
431
483
|
default:
|
|
432
484
|
break;
|
|
433
485
|
}
|
|
@@ -438,6 +490,21 @@ export class HeadlessSession {
|
|
|
438
490
|
push(line) {
|
|
439
491
|
this.transcript.push(line);
|
|
440
492
|
}
|
|
493
|
+
/**
|
|
494
|
+
* Hand over the measured spend since the last drain, and forget it (#267).
|
|
495
|
+
*
|
|
496
|
+
* Draining rather than re-reporting keeps a 3s capture poll from re-sending the same rows
|
|
497
|
+
* forever; the cloud's insert is keyed on {@link EngineUsageRecord.id} and ignores conflicts,
|
|
498
|
+
* so the genuine race — two callers draining at once, or a retry — still cannot double-count.
|
|
499
|
+
*
|
|
500
|
+
* A raw engine returns an empty array here, always: nothing parses its stdout for usage, so
|
|
501
|
+
* there is nothing to hand over and the cloud writes no row for it.
|
|
502
|
+
*/
|
|
503
|
+
takeUsage() {
|
|
504
|
+
const out = this.pendingUsage;
|
|
505
|
+
this.pendingUsage = [];
|
|
506
|
+
return out;
|
|
507
|
+
}
|
|
441
508
|
}
|
|
442
509
|
/** Local wall-clock "HH:MM:SS" for transcript timestamps (runner is a Node process). */
|
|
443
510
|
function stamp() {
|
|
@@ -86,8 +86,13 @@ export class CodingRuntime {
|
|
|
86
86
|
session.start();
|
|
87
87
|
return this.snapshot(input.sessionId);
|
|
88
88
|
}
|
|
89
|
-
/**
|
|
90
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The pane the brain reasons over + the inferred run state.
|
|
91
|
+
*
|
|
92
|
+
* `drainUsage` is opt-in because draining is destructive: nine cloud call sites hit
|
|
93
|
+
* `/coding/capture` and only the two that actually write the ledger may consume the records.
|
|
94
|
+
*/
|
|
95
|
+
snapshot(sessionId, opts = {}) {
|
|
91
96
|
const session = this.require(sessionId);
|
|
92
97
|
const alive = session.alive;
|
|
93
98
|
// ALWAYS return the transcript — it holds the produced output AND the
|
|
@@ -101,6 +106,11 @@ export class CodingRuntime {
|
|
|
101
106
|
alive,
|
|
102
107
|
ready: alive ? session.ready : false,
|
|
103
108
|
runState: alive ? session.runState() : "idle",
|
|
109
|
+
// Reported even when the process is not alive: "what would this session bill?" is
|
|
110
|
+
// exactly the question asked about a session that just stopped.
|
|
111
|
+
authResolved: session.authResolved,
|
|
112
|
+
engineRuntime: session.engineRuntime,
|
|
113
|
+
...(opts.drainUsage ? { usage: session.takeUsage() } : {}),
|
|
104
114
|
};
|
|
105
115
|
}
|
|
106
116
|
/** Perform one action, then return the fresh snapshot (non-blocking, like browser act). */
|
|
@@ -122,14 +132,23 @@ export class CodingRuntime {
|
|
|
122
132
|
return this.snapshot(sessionId);
|
|
123
133
|
}
|
|
124
134
|
/** Tear down a session. */
|
|
135
|
+
/**
|
|
136
|
+
* Stop and forget a session.
|
|
137
|
+
*
|
|
138
|
+
* Returns any un-drained spend (#267) rather than discarding it with the session: the last
|
|
139
|
+
* turn of a session very often runs after the final capture poll, and ending is where that
|
|
140
|
+
* record would otherwise be lost — silently, and only for the turns at the end of every
|
|
141
|
+
* session, which is a bias rather than noise.
|
|
142
|
+
*/
|
|
125
143
|
end(sessionId) {
|
|
126
144
|
const session = this.sessions.get(sessionId);
|
|
145
|
+
const usage = session ? session.takeUsage() : [];
|
|
127
146
|
if (session) {
|
|
128
147
|
session.stop();
|
|
129
148
|
this.sessions.delete(sessionId);
|
|
130
149
|
}
|
|
131
150
|
this.takeovers.delete(sessionId);
|
|
132
|
-
return { ok: true };
|
|
151
|
+
return { ok: true, usage };
|
|
133
152
|
}
|
|
134
153
|
list() {
|
|
135
154
|
return [...this.sessions.entries()].map(([sessionId, s]) => ({
|
|
@@ -150,6 +169,8 @@ export class CodingRuntime {
|
|
|
150
169
|
clientType: s.config.clientType,
|
|
151
170
|
workDir: s.config.workDir,
|
|
152
171
|
takeover: this.takeovers.has(sessionId),
|
|
172
|
+
authResolved: s.authResolved,
|
|
173
|
+
engineRuntime: s.engineRuntime,
|
|
153
174
|
}));
|
|
154
175
|
}
|
|
155
176
|
// ── Human takeover (the "stuck" handoff) ────────────────────────────────
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { startRunnerServer } from "./server.js";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
5
6
|
// Resilience: a stray error in any runtime must NOT take the whole runner down —
|
|
6
7
|
// that drops the tunnel and forces the user to restart `pags up` (and lose their
|
|
7
8
|
// session). Log it and keep serving. Per-component handlers catch most things;
|
|
@@ -29,7 +30,11 @@ function configFromArgs() {
|
|
|
29
30
|
host: arg("--host", process.env.PAGS_RUNNER_HOST || "127.0.0.1") || "127.0.0.1",
|
|
30
31
|
port: Number(arg("--port", process.env.PAGS_RUNNER_PORT || "49171")),
|
|
31
32
|
dataDir,
|
|
32
|
-
|
|
33
|
+
// Never start unauthenticated (#245). This surface drives a coding CLI with permissions
|
|
34
|
+
// skipped, and `authorize` now fails closed — so a missing token would make the runner
|
|
35
|
+
// answer nothing rather than answer everyone. Generate one and print it, mirroring what
|
|
36
|
+
// `pags runner connect` has always done.
|
|
37
|
+
token: arg("--token", process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${randomUUID()}`,
|
|
33
38
|
instanceId: arg("--instance-id", process.env.PAGS_INSTANCE_ID),
|
|
34
39
|
headless: flag("--headless") || process.env.PAGS_RUNNER_HEADLESS === "1",
|
|
35
40
|
};
|
|
@@ -147,8 +147,11 @@ async function route(runner, req, res) {
|
|
|
147
147
|
return json(res, 200, runner.coding.start(b));
|
|
148
148
|
}
|
|
149
149
|
if (req.method === "POST" && path === "/coding/capture") {
|
|
150
|
+
// `drainUsage` opts this caller in to consuming the session's measured engine spend (#267).
|
|
151
|
+
// Only the cloud paths that write the ledger send it; an older cloud omits it and simply
|
|
152
|
+
// gets the snapshot it always got.
|
|
150
153
|
const b = await readJson(req);
|
|
151
|
-
return json(res, 200, runner.coding.snapshot(b.sessionId));
|
|
154
|
+
return json(res, 200, runner.coding.snapshot(b.sessionId, { drainUsage: b.drainUsage === true }));
|
|
152
155
|
}
|
|
153
156
|
if (req.method === "POST" && path === "/coding/act") {
|
|
154
157
|
const b = await readJson(req);
|
|
@@ -391,13 +394,35 @@ async function route(runner, req, res) {
|
|
|
391
394
|
}
|
|
392
395
|
return json(res, 404, { error: "Not found" });
|
|
393
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Authorize a request to the local runner (#245).
|
|
399
|
+
*
|
|
400
|
+
* This surface drives a coding CLI that `pags up` launches with
|
|
401
|
+
* `--dangerously-skip-permissions` / `--sandbox danger-full-access`, so "who may POST here" is
|
|
402
|
+
* the whole security boundary. Two things were the wrong way round:
|
|
403
|
+
*
|
|
404
|
+
* 1. **No token used to mean ALLOW.** `pags up` always generates one, so that path was safe —
|
|
405
|
+
* but `pags-browser-runner` run directly (its own --help documents this) passes
|
|
406
|
+
* `token: undefined`, and served the entire surface unauthenticated. Now it fails CLOSED;
|
|
407
|
+
* the standalone entrypoint generates a token instead of starting open.
|
|
408
|
+
*
|
|
409
|
+
* 2. **A browser could reach it.** Binding to loopback is not isolation: a page the user is
|
|
410
|
+
* visiting cannot READ a cross-origin response, but it can still SEND the POST, and the
|
|
411
|
+
* server sets no CORS headers and did no Origin check. The token already made that
|
|
412
|
+
* unguessable — but nothing legitimate that calls this runner is a browser (the cloud
|
|
413
|
+
* dispatches over the relay; the CLI calls it directly), and neither sends `Origin`. So the
|
|
414
|
+
* presence of that header is by itself proof the caller is a web page, and is refused before
|
|
415
|
+
* the token is even considered. Also closes DNS-rebinding, which loopback does not.
|
|
416
|
+
*/
|
|
394
417
|
function authorize(req, config) {
|
|
418
|
+
if (req.headers.origin)
|
|
419
|
+
return false;
|
|
395
420
|
const token = config.token;
|
|
396
421
|
if (config.instanceId && req.headers["x-pags-instance-id"] !== config.instanceId) {
|
|
397
422
|
return false;
|
|
398
423
|
}
|
|
399
424
|
if (!token)
|
|
400
|
-
return
|
|
425
|
+
return false;
|
|
401
426
|
const auth = req.headers.authorization || "";
|
|
402
427
|
const headerToken = req.headers["x-pags-runner-token"];
|
|
403
428
|
return auth === `Bearer ${token}` || headerToken === token;
|