@oh-hai/cli 0.1.0-beta.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 +154 -0
- package/dist/auth/file-backend.d.ts +16 -0
- package/dist/auth/file-backend.js +98 -0
- package/dist/auth/file-backend.js.map +1 -0
- package/dist/auth/keychain.d.ts +54 -0
- package/dist/auth/keychain.js +232 -0
- package/dist/auth/keychain.js.map +1 -0
- package/dist/auth/resolve-token.d.ts +34 -0
- package/dist/auth/resolve-token.js +91 -0
- package/dist/auth/resolve-token.js.map +1 -0
- package/dist/auth/secure-write.d.ts +2 -0
- package/dist/auth/secure-write.js +30 -0
- package/dist/auth/secure-write.js.map +1 -0
- package/dist/auth/token-store.d.ts +104 -0
- package/dist/auth/token-store.js +208 -0
- package/dist/auth/token-store.js.map +1 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +238 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/agents.d.ts +2 -0
- package/dist/commands/agents.js +370 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/ask.d.ts +2 -0
- package/dist/commands/ask.js +246 -0
- package/dist/commands/ask.js.map +1 -0
- package/dist/commands/context.d.ts +72 -0
- package/dist/commands/context.js +7 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +237 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/flags.d.ts +25 -0
- package/dist/commands/flags.js +100 -0
- package/dist/commands/flags.js.map +1 -0
- package/dist/commands/handlers.d.ts +2 -0
- package/dist/commands/handlers.js +26 -0
- package/dist/commands/handlers.js.map +1 -0
- package/dist/commands/http.d.ts +8 -0
- package/dist/commands/http.js +19 -0
- package/dist/commands/http.js.map +1 -0
- package/dist/commands/inbox.d.ts +2 -0
- package/dist/commands/inbox.js +111 -0
- package/dist/commands/inbox.js.map +1 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +272 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +35 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/messaging/await.d.ts +43 -0
- package/dist/commands/messaging/await.js +125 -0
- package/dist/commands/messaging/await.js.map +1 -0
- package/dist/commands/messaging/build.d.ts +46 -0
- package/dist/commands/messaging/build.js +66 -0
- package/dist/commands/messaging/build.js.map +1 -0
- package/dist/commands/messaging/http.d.ts +22 -0
- package/dist/commands/messaging/http.js +270 -0
- package/dist/commands/messaging/http.js.map +1 -0
- package/dist/commands/messaging/identity.d.ts +29 -0
- package/dist/commands/messaging/identity.js +63 -0
- package/dist/commands/messaging/identity.js.map +1 -0
- package/dist/commands/messaging/shared.d.ts +53 -0
- package/dist/commands/messaging/shared.js +135 -0
- package/dist/commands/messaging/shared.js.map +1 -0
- package/dist/commands/messaging/state.d.ts +26 -0
- package/dist/commands/messaging/state.js +82 -0
- package/dist/commands/messaging/state.js.map +1 -0
- package/dist/commands/messaging/validate.d.ts +40 -0
- package/dist/commands/messaging/validate.js +193 -0
- package/dist/commands/messaging/validate.js.map +1 -0
- package/dist/commands/messaging/wire.d.ts +133 -0
- package/dist/commands/messaging/wire.js +16 -0
- package/dist/commands/messaging/wire.js.map +1 -0
- package/dist/commands/notify.d.ts +2 -0
- package/dist/commands/notify.js +68 -0
- package/dist/commands/notify.js.map +1 -0
- package/dist/commands/registry.d.ts +14 -0
- package/dist/commands/registry.js +144 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/stub.d.ts +1 -0
- package/dist/commands/stub.js +9 -0
- package/dist/commands/stub.js.map +1 -0
- package/dist/commands/task.d.ts +2 -0
- package/dist/commands/task.js +223 -0
- package/dist/commands/task.js.map +1 -0
- package/dist/commands/whoami.d.ts +6 -0
- package/dist/commands/whoami.js +90 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/config-file.d.ts +38 -0
- package/dist/config-file.js +233 -0
- package/dist/config-file.js.map +1 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +97 -0
- package/dist/config.js.map +1 -0
- package/dist/envelope.d.ts +25 -0
- package/dist/envelope.js +41 -0
- package/dist/envelope.js.map +1 -0
- package/dist/exit-codes.d.ts +51 -0
- package/dist/exit-codes.js +57 -0
- package/dist/exit-codes.js.map +1 -0
- package/dist/help.d.ts +1 -0
- package/dist/help.js +17 -0
- package/dist/help.js.map +1 -0
- package/dist/terminal.d.ts +5 -0
- package/dist/terminal.js +18 -0
- package/dist/terminal.js.map +1 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import type { ParseArgsConfig } from "node:util";
|
|
3
|
+
type OptionsConfig = NonNullable<ParseArgsConfig["options"]>;
|
|
4
|
+
/** The global flags available on every command (§4.2). */
|
|
5
|
+
export declare const GLOBAL_OPTIONS: OptionsConfig;
|
|
6
|
+
/**
|
|
7
|
+
* Coerce a `parseArgs` boolean-flag value to on/off. Under `strict: false`, `--json` yields
|
|
8
|
+
* `true` but `--json=true` yields the STRING `"true"`, so a bare `=== true` check would treat
|
|
9
|
+
* the common `--json=true` / `--version=1` forms as off. Bare presence and truthy `=`-values
|
|
10
|
+
* are on; `--json=false` / `0` / `no` are off. A repeated flag becomes an array — last wins.
|
|
11
|
+
*/
|
|
12
|
+
export declare function flagOn(value: unknown): boolean;
|
|
13
|
+
/** Parse argv for a command with no subcommand operand (the auth commands). */
|
|
14
|
+
export declare function parseCommandArgs(argv: string[], extra?: OptionsConfig): ReturnType<typeof parseArgs>;
|
|
15
|
+
/** Parse argv for a command that takes a SUBCOMMAND operand (`ask`/`task` → submit | await).
|
|
16
|
+
* Like `parseCommandArgs` (global + `extra` options, `strict:false` so the `--flag=value`
|
|
17
|
+
* boolean form isn't rejected, then reject any unknown flag), but `positionals[0]` is the
|
|
18
|
+
* command token and `positionals[1]` MUST be one of `subcommands`. With correct option types
|
|
19
|
+
* no flag value leaks into the positionals, so exactly `[command, subcommand]` is expected;
|
|
20
|
+
* a missing subcommand, an unknown one, or an extra operand is a usage error (exit 2). */
|
|
21
|
+
export declare function parseSubcommandArgs(argv: string[], subcommands: readonly string[], extra?: OptionsConfig): {
|
|
22
|
+
values: ReturnType<typeof parseArgs>["values"];
|
|
23
|
+
subcommand: string;
|
|
24
|
+
};
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Shared flag parsing (docs/specs/cli.md §4.2). The global-flag schema lives here so both the
|
|
2
|
+
// top-level entrypoint (cli.ts) and each per-command handler parse the SAME globals; a handler
|
|
3
|
+
// adds only its own command-specific options on top. `strict: false` mirrors the entrypoint —
|
|
4
|
+
// unknown tokens don't throw, they're tolerated (a handler ignores globals it doesn't consume).
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { CliError } from "../envelope.js";
|
|
7
|
+
/** The global flags available on every command (§4.2). */
|
|
8
|
+
export const GLOBAL_OPTIONS = {
|
|
9
|
+
"base-url": { type: "string" },
|
|
10
|
+
account: { type: "string" },
|
|
11
|
+
agent: { type: "string" },
|
|
12
|
+
json: { type: "boolean" },
|
|
13
|
+
quiet: { type: "boolean", short: "q" },
|
|
14
|
+
verbose: { type: "boolean", short: "v" },
|
|
15
|
+
"no-color": { type: "boolean" },
|
|
16
|
+
timeout: { type: "string" },
|
|
17
|
+
help: { type: "boolean", short: "h" },
|
|
18
|
+
version: { type: "boolean" },
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Coerce a `parseArgs` boolean-flag value to on/off. Under `strict: false`, `--json` yields
|
|
22
|
+
* `true` but `--json=true` yields the STRING `"true"`, so a bare `=== true` check would treat
|
|
23
|
+
* the common `--json=true` / `--version=1` forms as off. Bare presence and truthy `=`-values
|
|
24
|
+
* are on; `--json=false` / `0` / `no` are off. A repeated flag becomes an array — last wins.
|
|
25
|
+
*/
|
|
26
|
+
export function flagOn(value) {
|
|
27
|
+
if (value === true) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === "string") {
|
|
31
|
+
const v = value.trim().toLowerCase();
|
|
32
|
+
return v === "" || v === "true" || v === "1" || v === "yes" || v === "on";
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
return value.length > 0 && flagOn(value[value.length - 1]);
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
/** Parse argv against the global flags plus the command's own `extra` options with `strict: false`
|
|
40
|
+
* (so the `--flag=value` boolean form the CLI supports isn't rejected), then reject any flag NOT
|
|
41
|
+
* in the known set — so a typo like `oh-hai whoami --chek` is a usage error (exit 2), not silently
|
|
42
|
+
* ignored. Shared by the no-subcommand and subcommand parsers below. */
|
|
43
|
+
function parseKnownFlags(argv, extra) {
|
|
44
|
+
const options = { ...GLOBAL_OPTIONS, ...extra };
|
|
45
|
+
// Guard the parseArgs greedy-value footgun: under strict:false a string option either consumes the
|
|
46
|
+
// NEXT token even when it's another flag (`notify --title --dry-run` → title="--dry-run", --dry-run
|
|
47
|
+
// UNSET) or, when it's the last token, is left as boolean `true` (`notify --title Hi --base-url` →
|
|
48
|
+
// base-url silently falls back to default). Both let a malformed invocation perform a LIVE action
|
|
49
|
+
// instead of failing. If a bare `--<string-flag>` has no real value — the next token is another
|
|
50
|
+
// `--…` flag OR the args end — reject as usage (pass `--flag=<value>` for a value starting with `--`).
|
|
51
|
+
for (let i = 0; i < argv.length; i++) {
|
|
52
|
+
const tok = argv[i];
|
|
53
|
+
if (tok.startsWith("--") && tok !== "--" && !tok.includes("=") && options[tok.slice(2)]?.type === "string") {
|
|
54
|
+
const next = argv[i + 1];
|
|
55
|
+
if (next === undefined || next.startsWith("--")) {
|
|
56
|
+
const got = next === undefined ? "reached the end of the arguments" : `got ${next}`;
|
|
57
|
+
throw new CliError("usage", `${tok} expects a value but ${got}; to pass a value that starts with '--', use ${tok}=<value>.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, strict: false, options });
|
|
62
|
+
const allowed = new Set(Object.keys(options));
|
|
63
|
+
for (const key of Object.keys(parsed.values)) {
|
|
64
|
+
if (!allowed.has(key)) {
|
|
65
|
+
throw new CliError("usage", `unknown flag: --${key}. Run 'oh-hai <command> --help'.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
/** Parse argv for a command with no subcommand operand (the auth commands). */
|
|
71
|
+
export function parseCommandArgs(argv, extra = {}) {
|
|
72
|
+
const parsed = parseKnownFlags(argv, extra);
|
|
73
|
+
// positionals[0] is the command token; these auth commands take no positional operands, so a
|
|
74
|
+
// stray one (`oh-hai logout all`, `oh-hai whoami check`) is a typo, not a silently-ignored arg.
|
|
75
|
+
if (parsed.positionals.length > 1) {
|
|
76
|
+
throw new CliError("usage", `unexpected argument: ${parsed.positionals[1]}. Run 'oh-hai <command> --help'.`);
|
|
77
|
+
}
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
80
|
+
/** Parse argv for a command that takes a SUBCOMMAND operand (`ask`/`task` → submit | await).
|
|
81
|
+
* Like `parseCommandArgs` (global + `extra` options, `strict:false` so the `--flag=value`
|
|
82
|
+
* boolean form isn't rejected, then reject any unknown flag), but `positionals[0]` is the
|
|
83
|
+
* command token and `positionals[1]` MUST be one of `subcommands`. With correct option types
|
|
84
|
+
* no flag value leaks into the positionals, so exactly `[command, subcommand]` is expected;
|
|
85
|
+
* a missing subcommand, an unknown one, or an extra operand is a usage error (exit 2). */
|
|
86
|
+
export function parseSubcommandArgs(argv, subcommands, extra = {}) {
|
|
87
|
+
const parsed = parseKnownFlags(argv, extra);
|
|
88
|
+
const subcommand = parsed.positionals[1];
|
|
89
|
+
if (subcommand === undefined) {
|
|
90
|
+
throw new CliError("usage", `missing subcommand: expected one of ${subcommands.join(" | ")}.`);
|
|
91
|
+
}
|
|
92
|
+
if (!subcommands.includes(subcommand)) {
|
|
93
|
+
throw new CliError("usage", `unknown subcommand: ${subcommand} (expected ${subcommands.join(" | ")}).`);
|
|
94
|
+
}
|
|
95
|
+
if (parsed.positionals.length > 2) {
|
|
96
|
+
throw new CliError("usage", `unexpected argument: ${parsed.positionals[2]}.`);
|
|
97
|
+
}
|
|
98
|
+
return { values: parsed.values, subcommand };
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=flags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flags.js","sourceRoot":"","sources":["../../src/commands/flags.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The command-handler registry (docs/specs/cli.md §4). #106 wired the three auth commands; #107
|
|
2
|
+
// wires the messaging commands (notify / ask / task); #209 wires `agents` (account/identity
|
|
3
|
+
// management); #108 wires `doctor` (health). Every registry command now has a handler; the
|
|
4
|
+
// entrypoint (cli.ts) looks a command up here first, and a miss still falls through to
|
|
5
|
+
// `notImplemented()` as a defensive fallback for any future entry added without a handler.
|
|
6
|
+
import { agentsCommand } from "./agents.js";
|
|
7
|
+
import { askCommand } from "./ask.js";
|
|
8
|
+
import { doctorCommand } from "./doctor.js";
|
|
9
|
+
import { inboxCommand } from "./inbox.js";
|
|
10
|
+
import { loginCommand } from "./login.js";
|
|
11
|
+
import { logoutCommand } from "./logout.js";
|
|
12
|
+
import { notifyCommand } from "./notify.js";
|
|
13
|
+
import { taskCommand } from "./task.js";
|
|
14
|
+
import { whoamiCommand } from "./whoami.js";
|
|
15
|
+
export const COMMAND_HANDLERS = {
|
|
16
|
+
login: loginCommand,
|
|
17
|
+
logout: logoutCommand,
|
|
18
|
+
whoami: whoamiCommand,
|
|
19
|
+
notify: notifyCommand,
|
|
20
|
+
ask: askCommand,
|
|
21
|
+
task: taskCommand,
|
|
22
|
+
agents: agentsCommand,
|
|
23
|
+
inbox: inboxCommand,
|
|
24
|
+
doctor: doctorCommand,
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=handlers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handlers.js","sourceRoot":"","sources":["../../src/commands/handlers.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,4FAA4F;AAC5F,2FAA2F;AAC3F,uFAAuF;AACvF,2FAA2F;AAE3F,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,CAAC,MAAM,gBAAgB,GAAmC;IAC9D,KAAK,EAAE,YAAY;IACnB,MAAM,EAAE,aAAa;IACrB,MAAM,EAAE,aAAa;IACrB,MAAM,EAAE,aAAa;IACrB,GAAG,EAAE,UAAU;IACf,IAAI,EAAE,WAAW;IACjB,MAAM,EAAE,aAAa;IACrB,KAAK,EAAE,YAAY;IACnB,MAAM,EAAE,aAAa;CACtB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map a transport-layer failure (the `fetch` itself rejecting, before any HTTP response) to the
|
|
3
|
+
* right §7 exit code: a per-request timeout is exit 7 (`timeout`, §7); any other connection failure
|
|
4
|
+
* (DNS, refused, unreachable) is exit 5 (`network`, §9). A non-`Error` throw is not a timeout, so it
|
|
5
|
+
* takes the network branch. Always throws — the `never` return lets a caller use it as the whole
|
|
6
|
+
* body of a `catch`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function throwTransportError(error: unknown, baseUrl: string): never;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Shared HTTP helper for the network-touching commands (login, whoami). Kept tiny and
|
|
2
|
+
// dependency-free: the one thing worth sharing is the transport-error → §7 exit-code mapping, so a
|
|
3
|
+
// DNS/refused failure and a per-request timeout surface the SAME codes and messages from every
|
|
4
|
+
// command (cli spec §7/§9) instead of each command carrying its own copy.
|
|
5
|
+
import { CliError } from "../envelope.js";
|
|
6
|
+
/**
|
|
7
|
+
* Map a transport-layer failure (the `fetch` itself rejecting, before any HTTP response) to the
|
|
8
|
+
* right §7 exit code: a per-request timeout is exit 7 (`timeout`, §7); any other connection failure
|
|
9
|
+
* (DNS, refused, unreachable) is exit 5 (`network`, §9). A non-`Error` throw is not a timeout, so it
|
|
10
|
+
* takes the network branch. Always throws — the `never` return lets a caller use it as the whole
|
|
11
|
+
* body of a `catch`.
|
|
12
|
+
*/
|
|
13
|
+
export function throwTransportError(error, baseUrl) {
|
|
14
|
+
if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) {
|
|
15
|
+
throw new CliError("timeout", `Hub did not respond within the timeout (${baseUrl}).`);
|
|
16
|
+
}
|
|
17
|
+
throw new CliError("network", `Hub unreachable at ${baseUrl}.`);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/commands/http.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,mGAAmG;AACnG,+FAA+F;AAC/F,0EAA0E;AAE1E,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAc,EAAE,OAAe;IACjE,IAAI,KAAK,YAAY,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,cAAc,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,CAAC,EAAE,CAAC;QAC7F,MAAM,IAAI,QAAQ,CAAC,SAAS,EAAE,2CAA2C,OAAO,IAAI,CAAC,CAAC;IACxF,CAAC;IACD,MAAM,IAAI,QAAQ,CAAC,SAAS,EAAE,sBAAsB,OAAO,GAAG,CAAC,CAAC;AAClE,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// `oh-hai inbox` (docs/specs/cli.md §4.7) — the CLI-owned poll loop for the MA2H v0.4 human→agent
|
|
2
|
+
// leg (spec §13/§14/§15; issue #153). Subcommand `watch`: DRAIN the agent's mailbox, HAND each
|
|
3
|
+
// directive to the agent runtime (a stdout stream), post the single batched consume-ACK, and — since
|
|
4
|
+
// every authenticated drain touches `agent_seen` server-side — emit the PRESENCE heartbeat simply by
|
|
5
|
+
// polling on an interval. Owning this plumbing is the point: "so the agent invents nothing and stays
|
|
6
|
+
// spec-conformant" — a runtime can `oh-hai inbox watch --json | my-runtime` and get a clean directive stream.
|
|
7
|
+
//
|
|
8
|
+
// TRUST / SCOPE (deliberate): the CLI drains over the AUTHENTICATED PULL channel (bearer GET over
|
|
9
|
+
// TLS, straight from the Hub) and trusts it exactly as `await.ts` trusts a pulled Response — it does
|
|
10
|
+
// NOT verify the §9.7 inbound signature, keep a jti-replay cache, or run the §13.4 addressee check.
|
|
11
|
+
// Those are PUSH/webhook-channel obligations (conformance vectors dp-007 cases 5–6), not this pull
|
|
12
|
+
// loop's. The signed delivery `{ directive, signature }` is forwarded VERBATIM so a runtime that
|
|
13
|
+
// wants defense-in-depth can verify it against the Hub key. In-session `id` dedup (§13.4) IS handled
|
|
14
|
+
// here (see the loop). Sibling subcommands (`inbox list` / `inbox ack`) and long-poll are out of scope.
|
|
15
|
+
import { buildOk, serializeEnvelope } from "../envelope.js";
|
|
16
|
+
import { parseSubcommandArgs } from "./flags.js";
|
|
17
|
+
import { ackInbox, drainInbox, redactToken } from "./messaging/http.js";
|
|
18
|
+
import { requireToken } from "./messaging/identity.js";
|
|
19
|
+
import { positiveInt, sanitizeForTerminal, strictBoolFlag, stringFlag } from "./messaging/shared.js";
|
|
20
|
+
const SUBCOMMANDS = ["watch"];
|
|
21
|
+
const INBOX_WATCH_OPTIONS = {
|
|
22
|
+
interval: { type: "string" },
|
|
23
|
+
max: { type: "string" },
|
|
24
|
+
once: { type: "boolean" },
|
|
25
|
+
};
|
|
26
|
+
/** Default poll cadence (ms). Well under the Hub's 90s presence-freshness window so the agent stays
|
|
27
|
+
* `online` between polls, and responsive to arriving directives without hammering the Hub. */
|
|
28
|
+
const DEFAULT_INTERVAL_MS = 5000;
|
|
29
|
+
/** A sane upper bound for `--max` (a drain batch size, not a timer): generous enough to never reject a
|
|
30
|
+
* realistic mailbox drain, small enough to reject an absurd `?max=` — the Hub clamps to its own cap
|
|
31
|
+
* regardless. Distinct from the timer ceiling `--interval` uses. */
|
|
32
|
+
const MAX_DRAIN_BATCH = 10_000;
|
|
33
|
+
export async function inboxCommand(ctx) {
|
|
34
|
+
// `watch` is the only subcommand, so dispatch is unconditional — the parse still validates the
|
|
35
|
+
// operand (an unknown/absent subcommand is a usage error) before we get here.
|
|
36
|
+
const { values } = parseSubcommandArgs(ctx.argv, SUBCOMMANDS, INBOX_WATCH_OPTIONS);
|
|
37
|
+
await inboxWatch(ctx, values);
|
|
38
|
+
}
|
|
39
|
+
async function inboxWatch(ctx, flags) {
|
|
40
|
+
const intervalMs = flags.interval !== undefined ? positiveInt(stringFlag(flags.interval), "--interval") : DEFAULT_INTERVAL_MS;
|
|
41
|
+
const max = flags.max !== undefined ? positiveInt(stringFlag(flags.max), "--max", "a positive integer", MAX_DRAIN_BATCH) : undefined;
|
|
42
|
+
// Strict boolean: a mistyped `--once=treu` is a usage error, not a silent fall-through to the
|
|
43
|
+
// infinite watch loop (it would keep draining + acking every interval instead of a one-shot).
|
|
44
|
+
const once = strictBoolFlag(flags.once, "--once");
|
|
45
|
+
// The mailbox is selected by the bearer alone (the Hub derives the agent from the token), so this
|
|
46
|
+
// needs only a token — like `await`, and unlike a submit, it does not require a resolvable account.
|
|
47
|
+
const token = await requireToken(ctx);
|
|
48
|
+
// Cross-poll dedup (§13.4): once a directive id has been emitted AND its batch acked, a later
|
|
49
|
+
// redelivery of that same id (fresh t/jti, after a crash or the server's visibility timeout) is
|
|
50
|
+
// suppressed from re-emission — but still acked so the Hub consumes it. `seen` commits only AFTER a
|
|
51
|
+
// successful ack (below), so an unacked batch is never suppressed: on ack failure the loop throws
|
|
52
|
+
// and exits, and a fresh process re-emits the redelivery (at-least-once holds). The set is in-memory
|
|
53
|
+
// / per-session — cross-restart at-most-once needs an idempotent side effect or a persisted dedup
|
|
54
|
+
// set (dp-007), the runtime's concern, not this loop's. A long-lived watch grows `seen` with
|
|
55
|
+
// directive volume; acceptable for v1.
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
for (;;) {
|
|
58
|
+
const { directives } = await drainInbox(ctx, token, max);
|
|
59
|
+
// The unique ids in THIS drain — both the ack list and the within-batch emit guard (a Hub could
|
|
60
|
+
// repeat an id inside one batch; emit it once and ack it once, not twice).
|
|
61
|
+
const batchIds = new Set();
|
|
62
|
+
for (const delivery of directives) {
|
|
63
|
+
const id = delivery.directive.id;
|
|
64
|
+
if (!seen.has(id) && !batchIds.has(id))
|
|
65
|
+
emitDirective(ctx, delivery, token);
|
|
66
|
+
batchIds.add(id);
|
|
67
|
+
}
|
|
68
|
+
if (batchIds.size > 0) {
|
|
69
|
+
// Flush stdout BEFORE acking so the emit-then-ack handoff is genuinely at-least-once: `io.log`
|
|
70
|
+
// only queues to stdout, so when `watch` is piped to a runtime under backpressure the bytes may
|
|
71
|
+
// still be buffered in this process. Acking first would let the Hub consume a directive that a
|
|
72
|
+
// crash/EPIPE could still lose before it reaches the consumer. `flushOutput` waits for the
|
|
73
|
+
// queued writes to complete (a write-callback barrier, not just the drain flag); THEN ack.
|
|
74
|
+
await ctx.runtime.flushOutput?.();
|
|
75
|
+
// The single batched consume-ack (§14) — one call per drained batch, deduped ids.
|
|
76
|
+
await ackInbox(ctx, token, [...batchIds]);
|
|
77
|
+
// Commit to the cross-poll dedup set only now that the batch is durably consumed.
|
|
78
|
+
for (const id of batchIds)
|
|
79
|
+
seen.add(id);
|
|
80
|
+
}
|
|
81
|
+
// --once: a single drain→emit→ack pass (cron / scripting / deterministic tests). Otherwise loop;
|
|
82
|
+
// the poll itself IS the presence heartbeat, so keep polling on the interval even on an empty drain.
|
|
83
|
+
if (once)
|
|
84
|
+
return;
|
|
85
|
+
await ctx.runtime.sleep(intervalMs);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Hand one directive to the agent runtime. stdout is the directive STREAM.
|
|
89
|
+
*
|
|
90
|
+
* Under `--json` the delivery is emitted BYTE-VERBATIM — one NDJSON `inbox.watch` envelope carrying
|
|
91
|
+
* the exact `{ directive, signature }`. This is deliberate and must not be redacted: the detached
|
|
92
|
+
* `signature` is computed over the `directive` bytes, so mutating the directive (even to strip an
|
|
93
|
+
* echoed token) would break the signature a defense-in-depth runtime verifies — and we'd then ack a
|
|
94
|
+
* directive the runtime couldn't authenticate. A conformant Hub never puts the bearer in directive
|
|
95
|
+
* content, and the machine stream feeds the agent's OWN runtime (which already holds the token), so
|
|
96
|
+
* verbatim is the right posture; JSON.stringify escapes control chars.
|
|
97
|
+
*
|
|
98
|
+
* The HUMAN summary carries no signature contract, so it stays hardened (§5.4): fields are
|
|
99
|
+
* terminal-sanitized AND the bearer is redacted — a compromised/buggy Hub can't inject ANSI or echo
|
|
100
|
+
* the token into a line a person might read or log. */
|
|
101
|
+
function emitDirective(ctx, delivery, token) {
|
|
102
|
+
if (ctx.json) {
|
|
103
|
+
ctx.io.log(serializeEnvelope(buildOk("inbox.watch", { directive: delivery.directive, signature: delivery.signature })));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const d = delivery.directive;
|
|
107
|
+
const priority = d.priority !== undefined ? ` (${sanitizeForTerminal(d.priority)})` : "";
|
|
108
|
+
const summary = `📨 ${sanitizeForTerminal(d.id)} · ${sanitizeForTerminal(d.from)} · ${sanitizeForTerminal(d.title)}${priority}`;
|
|
109
|
+
ctx.io.log(redactToken(summary, token) ?? summary);
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=inbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbox.js","sourceRoot":"","sources":["../../src/commands/inbox.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// `oh-hai login` (docs/specs/cli.md §4.3). Two paths:
|
|
2
|
+
// - `--token-stdin`: read a token from stdin and store it in the OS keychain (or the 0600
|
|
3
|
+
// file fallback). The token is NEVER printed and NEVER accepted as a flag value (§5.4).
|
|
4
|
+
// - bare `login` (device-code): the PRIMARY flow — request a device code, print a short
|
|
5
|
+
// `user_code` + verification URL to STDERR, and poll the Hub's device-authorization grant
|
|
6
|
+
// (RFC 8628; server routes from #193) until the human approves in a browser. On success the
|
|
7
|
+
// Hub returns the agent bearer, stored under the Hub-scoped key `<origin>|<agent id>` (§5.1).
|
|
8
|
+
// The token is never shown, never typed, never through the LLM (§1/§5.4).
|
|
9
|
+
import { CliError, buildOk, serializeEnvelope } from "../envelope.js";
|
|
10
|
+
import { originOf } from "../auth/resolve-token.js";
|
|
11
|
+
import { sanitizeForTerminal } from "../terminal.js";
|
|
12
|
+
import { flagOn, parseCommandArgs } from "./flags.js";
|
|
13
|
+
import { throwTransportError } from "./http.js";
|
|
14
|
+
/** Per-request bound for each device-grant POST when no `--timeout`/`MA2H_TIMEOUT_MS` is set. It
|
|
15
|
+
* bounds a SINGLE request, never the whole flow — the human's approval window (minutes) is bounded
|
|
16
|
+
* separately by the poll budget below, so a snappy per-request timeout can't abort a slow human. */
|
|
17
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
18
|
+
/** RFC 8628 §3.5: on `slow_down` (and the Hub's HTTP 429 rate-limit), the client backs off +5s. */
|
|
19
|
+
const SLOW_DOWN_INCREMENT_MS = 5_000;
|
|
20
|
+
/** Cap on an honored `Retry-After` (seconds) so a hostile Hub can't stall the poll indefinitely. */
|
|
21
|
+
const MAX_RETRY_AFTER_SECONDS = 300;
|
|
22
|
+
// Poll interval / code-expiry bounds. The server advertises `interval` (5s) and `expires_in` (900s),
|
|
23
|
+
// but they arrive over the wire from a not-yet-authenticated endpoint, so they're clamped into sane
|
|
24
|
+
// ranges: a missing / zero / fractional-floors-to-zero / negative / absurd value must never yield a
|
|
25
|
+
// zero or effectively unbounded poll budget (a `0` interval alone divides `maxPolls` to Infinity and
|
|
26
|
+
// spins the loop with a no-op sleep). Maxima are generous so a legit human window is never cut short.
|
|
27
|
+
const DEFAULT_INTERVAL_SECONDS = 5;
|
|
28
|
+
const MIN_INTERVAL_SECONDS = 1;
|
|
29
|
+
const MAX_INTERVAL_SECONDS = 60;
|
|
30
|
+
const DEFAULT_EXPIRES_IN_SECONDS = 900;
|
|
31
|
+
const MIN_EXPIRES_IN_SECONDS = 1;
|
|
32
|
+
const MAX_EXPIRES_IN_SECONDS = 3600;
|
|
33
|
+
export async function loginCommand(ctx) {
|
|
34
|
+
const { values } = parseCommandArgs(ctx.argv, { "token-stdin": { type: "boolean" } });
|
|
35
|
+
if (flagOn(values["token-stdin"])) {
|
|
36
|
+
await tokenStdinLogin(ctx);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
await deviceCodeLogin(ctx);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** `--token-stdin` (§4.3): read a bearer from stdin (local bootstrap / paste) and store it the same
|
|
43
|
+
* way the device-code path does. Behavior is frozen — #195 only adds the device-code flow. */
|
|
44
|
+
async function tokenStdinLogin(ctx) {
|
|
45
|
+
const account = ctx.config.account;
|
|
46
|
+
if (account === undefined) {
|
|
47
|
+
throw new CliError("usage", "an agent id is required — pass --account/--agent or set MA2H_AGENT_ID.");
|
|
48
|
+
}
|
|
49
|
+
const raw = await ctx.runtime.readStdin();
|
|
50
|
+
const token = raw.trim();
|
|
51
|
+
if (token === "") {
|
|
52
|
+
throw new CliError("usage", "no token on stdin — pipe the token, e.g. `printf %s \"$TOKEN\" | oh-hai login --token-stdin`.");
|
|
53
|
+
}
|
|
54
|
+
await storeAndReport(ctx, account, token);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The bare `oh-hai login` device-code acquisition (§4.3/§1, RFC 8628). Requests a device code,
|
|
58
|
+
* prompts on STDERR (stdout stays reserved for the single `--json` envelope, §8), then polls until
|
|
59
|
+
* the human approves — storing the returned bearer under `<origin>|<agent id>`. The agent id comes
|
|
60
|
+
* FROM the token response (the human chose which agent to bind at approval time), so this path
|
|
61
|
+
* needs no `--account`, unlike `--token-stdin`.
|
|
62
|
+
*/
|
|
63
|
+
async function deviceCodeLogin(ctx) {
|
|
64
|
+
const code = await requestDeviceCode(ctx);
|
|
65
|
+
// Prompt on stderr. `openUrl` prefers the one-click `verification_uri_complete` (embeds the code);
|
|
66
|
+
// the code is shown separately so a human copying the plain URL still knows what to enter. Both are
|
|
67
|
+
// sanitized of control characters first — they're server-supplied and echoed straight to the terminal.
|
|
68
|
+
ctx.io.err(`oh-hai: to authorize this device, open ${sanitizeForTerminal(code.openUrl)}`);
|
|
69
|
+
ctx.io.err(`oh-hai: and enter the code: ${sanitizeForTerminal(code.userCode)}`);
|
|
70
|
+
ctx.io.err("oh-hai: waiting for approval…");
|
|
71
|
+
// Poll budget: the server expires the code and returns `expired_token` at the real deadline; this
|
|
72
|
+
// cap is a pure anti-infinite-loop guard against a server that never returns a terminal state.
|
|
73
|
+
const maxPolls = Math.ceil(code.expiresInSeconds / code.intervalSeconds) + 2;
|
|
74
|
+
// `intervalMs` is the advertised base interval; only `slow_down` raises it permanently (RFC 8628
|
|
75
|
+
// §3.5). `nextDelayMs` is the wait before the NEXT poll — it resets to the base each round, so a
|
|
76
|
+
// one-shot 429 back-off (below) never stretches every later poll and risks missing an approval
|
|
77
|
+
// before the code expires.
|
|
78
|
+
let intervalMs = code.intervalSeconds * 1000;
|
|
79
|
+
let nextDelayMs = intervalMs;
|
|
80
|
+
for (let poll = 0; poll < maxPolls; poll++) {
|
|
81
|
+
await ctx.runtime.sleep(nextDelayMs);
|
|
82
|
+
nextDelayMs = intervalMs; // resume the advertised interval unless this round overrides it below
|
|
83
|
+
const res = await hubPost(ctx, "/auth/device/token", { device_code: code.deviceCode });
|
|
84
|
+
if (res.status === 200) {
|
|
85
|
+
const token = await readAccessToken(res);
|
|
86
|
+
await storeAndReport(ctx, token.agentId, token.accessToken);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// The Hub's per-IP limiter throttles the shared device surface with HTTP 429 `rate_limited`
|
|
90
|
+
// (+ Retry-After); a legit poller (e.g. behind a corporate NAT) can trip it mid-approval, so
|
|
91
|
+
// back off and keep polling rather than aborting. Honor Retry-After (the remaining window) as a
|
|
92
|
+
// ONE-SHOT delay so we don't hammer inside the window, then resume the advertised interval.
|
|
93
|
+
if (res.status === 429) {
|
|
94
|
+
const retryAfterMs = retryAfterMillis(res.headers?.get("retry-after"));
|
|
95
|
+
nextDelayMs = Math.max(intervalMs + SLOW_DOWN_INCREMENT_MS, retryAfterMs);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// Every other non-200 poll response is the grant's HTTP 400 carrying an RFC 8628 `error.code`.
|
|
99
|
+
// A non-400 here is an unexpected transport/server error (5xx, a proxy) — map it and stop, so a
|
|
100
|
+
// stray RFC-looking body on a 5xx isn't mistaken for a live grant state and silently retried.
|
|
101
|
+
const { code: errCode, message } = await readErrorEnvelope(res);
|
|
102
|
+
if (res.status !== 400) {
|
|
103
|
+
throw mapHubError(res.status, message);
|
|
104
|
+
}
|
|
105
|
+
switch (errCode) {
|
|
106
|
+
case "authorization_pending":
|
|
107
|
+
continue; // human hasn't approved yet — keep polling at the current interval
|
|
108
|
+
case "slow_down":
|
|
109
|
+
intervalMs += SLOW_DOWN_INCREMENT_MS; // RFC 8628 §3.5 — permanent interval increase
|
|
110
|
+
nextDelayMs = intervalMs;
|
|
111
|
+
continue;
|
|
112
|
+
case "access_denied":
|
|
113
|
+
throw new CliError("auth", "device authorization was denied.");
|
|
114
|
+
case "expired_token":
|
|
115
|
+
throw new CliError("timeout", "the device code expired before it was approved; run `oh-hai login` again.");
|
|
116
|
+
default:
|
|
117
|
+
// A 400 with an unexpected/absent error code — not one of the four RFC states.
|
|
118
|
+
throw mapHubError(res.status, message);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// The loop exhausted its budget without a terminal response — treat as a timeout (§7).
|
|
122
|
+
throw new CliError("timeout", "device authorization did not complete before the code expired; run `oh-hai login` again.");
|
|
123
|
+
}
|
|
124
|
+
/** Store the bearer Hub-scoped (§5.1) and report success. The token is NEVER in the output (§5.4);
|
|
125
|
+
* an existing token for this identity is overwritten silently (§4.3). Shared by both login paths. */
|
|
126
|
+
async function storeAndReport(ctx, agentId, token) {
|
|
127
|
+
const store = await ctx.runtime.openStore();
|
|
128
|
+
await store.set(originOf(ctx.config.baseUrl), agentId, token);
|
|
129
|
+
if (ctx.json) {
|
|
130
|
+
// No token field, ever (§5.4). `storage` reports which backend actually holds it. The agent id
|
|
131
|
+
// is emitted raw — JSON.stringify escapes any control character, so it can't reach the terminal.
|
|
132
|
+
ctx.io.log(serializeEnvelope(buildOk("login", { agent_id: agentId, base_url: ctx.config.baseUrl, storage: store.backendName })));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// Sanitize the DISPLAYED agent id — on the device-code path it's server-supplied, so a hostile
|
|
136
|
+
// Hub could return control characters. The stored credential key above uses the RAW id so it
|
|
137
|
+
// still matches the Hub's identity for later resolution.
|
|
138
|
+
ctx.io.log(`Logged in as ${sanitizeForTerminal(agentId)} @ ${ctx.config.baseUrl}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** POST /auth/device/code, validate the fields the prompt + poll need, and normalize the poll
|
|
142
|
+
* interval / expiry to sane bounded values. */
|
|
143
|
+
async function requestDeviceCode(ctx) {
|
|
144
|
+
const res = await hubPost(ctx, "/auth/device/code", {});
|
|
145
|
+
if (res.status !== 200) {
|
|
146
|
+
const { message } = await readErrorEnvelope(res);
|
|
147
|
+
throw mapHubError(res.status, message);
|
|
148
|
+
}
|
|
149
|
+
const body = await readJson(res);
|
|
150
|
+
const deviceCode = str(body.device_code);
|
|
151
|
+
const userCode = str(body.user_code);
|
|
152
|
+
const verificationUri = str(body.verification_uri);
|
|
153
|
+
if (deviceCode === undefined || userCode === undefined || verificationUri === undefined) {
|
|
154
|
+
throw new CliError("server", "the Hub returned a malformed device-code response.");
|
|
155
|
+
}
|
|
156
|
+
// Only ever invite the user to open an http(s) URL — a hostile Hub must not get a `file:` /
|
|
157
|
+
// `javascript:` verification URL echoed as "open this". Prefer the complete form when it's also http(s).
|
|
158
|
+
if (!isHttpUrl(verificationUri)) {
|
|
159
|
+
throw new CliError("server", "the Hub returned a non-http verification URL.");
|
|
160
|
+
}
|
|
161
|
+
const complete = str(body.verification_uri_complete);
|
|
162
|
+
return {
|
|
163
|
+
deviceCode,
|
|
164
|
+
userCode,
|
|
165
|
+
openUrl: complete !== undefined && isHttpUrl(complete) ? complete : verificationUri,
|
|
166
|
+
intervalSeconds: boundedInt(body.interval, DEFAULT_INTERVAL_SECONDS, MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS),
|
|
167
|
+
expiresInSeconds: boundedInt(body.expires_in, DEFAULT_EXPIRES_IN_SECONDS, MIN_EXPIRES_IN_SECONDS, MAX_EXPIRES_IN_SECONDS),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** Read + validate the 200 token response (`{ access_token, agent_id }`). */
|
|
171
|
+
async function readAccessToken(res) {
|
|
172
|
+
const body = await readJson(res);
|
|
173
|
+
const accessToken = str(body.access_token);
|
|
174
|
+
const agentId = str(body.agent_id);
|
|
175
|
+
if (accessToken === undefined || agentId === undefined) {
|
|
176
|
+
throw new CliError("server", "the Hub returned a malformed token response.");
|
|
177
|
+
}
|
|
178
|
+
return { accessToken, agentId };
|
|
179
|
+
}
|
|
180
|
+
/** POST a JSON body to `path` on the Hub, mapping transport failures to §7 exit codes. The
|
|
181
|
+
* per-request timeout bounds THIS request only; the poll budget bounds the whole flow. */
|
|
182
|
+
async function hubPost(ctx, path, body) {
|
|
183
|
+
const url = `${ctx.config.baseUrl}${path}`;
|
|
184
|
+
const init = {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: { "Content-Type": "application/json" },
|
|
187
|
+
body: JSON.stringify(body),
|
|
188
|
+
signal: AbortSignal.timeout(ctx.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS),
|
|
189
|
+
};
|
|
190
|
+
try {
|
|
191
|
+
return await ctx.runtime.fetchImpl(url, init);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
throwTransportError(error, ctx.config.baseUrl);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Parse a response body as a JSON object, defensively — a fake without `json()`, a non-JSON body,
|
|
198
|
+
* or a non-object (array / string / null) all collapse to `{}`, so callers read fields off a plain
|
|
199
|
+
* record without unchecked casts. */
|
|
200
|
+
async function readJson(res) {
|
|
201
|
+
if (typeof res.json !== "function")
|
|
202
|
+
return {};
|
|
203
|
+
try {
|
|
204
|
+
const parsed = await res.json();
|
|
205
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
206
|
+
? parsed
|
|
207
|
+
: {};
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** Read the A2H error envelope `{ error: { code, message } }` from a non-200 response. */
|
|
214
|
+
async function readErrorEnvelope(res) {
|
|
215
|
+
const body = await readJson(res);
|
|
216
|
+
const error = body.error;
|
|
217
|
+
const record = error !== null && typeof error === "object" ? error : {};
|
|
218
|
+
return { code: str(record.code), message: str(record.message) };
|
|
219
|
+
}
|
|
220
|
+
/** Map an unexpected Hub error status to the right §7 exit code, preferring the server's message.
|
|
221
|
+
* The server-supplied `message` is stripped of terminal control characters — the top-level catch
|
|
222
|
+
* prints a CliError's message to the terminal, so a hostile Hub could otherwise inject escapes. */
|
|
223
|
+
function mapHubError(status, message) {
|
|
224
|
+
const detail = message !== undefined ? sanitizeForTerminal(message) : `Hub returned ${status}.`;
|
|
225
|
+
if (status === 401 || status === 403)
|
|
226
|
+
return new CliError("auth", detail);
|
|
227
|
+
if (status === 404)
|
|
228
|
+
return new CliError("not_found", detail);
|
|
229
|
+
if (status === 413)
|
|
230
|
+
return new CliError("payload_too_large", detail);
|
|
231
|
+
if (status === 400 || status === 422)
|
|
232
|
+
return new CliError("bad_request", detail);
|
|
233
|
+
if (status >= 500)
|
|
234
|
+
return new CliError("server", detail);
|
|
235
|
+
return new CliError("error", detail);
|
|
236
|
+
}
|
|
237
|
+
/** Parse a `Retry-After` header of the numeric-seconds form the Hub sends into milliseconds, capped
|
|
238
|
+
* so a hostile value can't stall the poll; 0 when the header is absent or unparseable. */
|
|
239
|
+
function retryAfterMillis(headerValue) {
|
|
240
|
+
if (headerValue === null || headerValue === undefined)
|
|
241
|
+
return 0;
|
|
242
|
+
const seconds = Number.parseInt(headerValue.trim(), 10);
|
|
243
|
+
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
244
|
+
return 0;
|
|
245
|
+
return Math.min(seconds, MAX_RETRY_AFTER_SECONDS) * 1000;
|
|
246
|
+
}
|
|
247
|
+
/** A trimmed non-empty string from an unknown field, else undefined (validates response fields). */
|
|
248
|
+
function str(value) {
|
|
249
|
+
if (typeof value !== "string")
|
|
250
|
+
return undefined;
|
|
251
|
+
const trimmed = value.trim();
|
|
252
|
+
return trimmed === "" ? undefined : trimmed;
|
|
253
|
+
}
|
|
254
|
+
/** True when `value` parses as an http(s) URL — the only schemes we invite the user to open (§4.3). */
|
|
255
|
+
function isHttpUrl(value) {
|
|
256
|
+
try {
|
|
257
|
+
const { protocol } = new URL(value);
|
|
258
|
+
return protocol === "http:" || protocol === "https:";
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/** An integer from an unknown response field, clamped to `[min, max]`; `fallback` (already in range)
|
|
265
|
+
* is used when the value is missing or non-finite. Clamping means a missing, zero, fractional
|
|
266
|
+
* (floors to 0), negative, or absurdly large server-supplied `interval` / `expires_in` can never
|
|
267
|
+
* produce a zero/negative or effectively unbounded poll budget — which would defeat the poll cap. */
|
|
268
|
+
function boundedInt(value, fallback, min, max) {
|
|
269
|
+
const n = typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : fallback;
|
|
270
|
+
return Math.min(Math.max(n, min), max);
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=login.js.map
|