humanish 0.16.0 → 0.17.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 +43 -3
- package/dist/observer-auth.d.ts +21 -0
- package/dist/observer-auth.js +92 -0
- package/dist/observer-auth.js.map +1 -0
- package/dist/observer-library.d.ts +19 -0
- package/dist/observer-library.js +189 -0
- package/dist/observer-library.js.map +1 -0
- package/dist/observer-serve.d.ts +111 -0
- package/dist/observer-serve.js +369 -0
- package/dist/observer-serve.js.map +1 -0
- package/dist/observer.d.ts +31 -0
- package/dist/observer.js +10 -5
- package/dist/observer.js.map +1 -1
- package/dist/program.js +242 -0
- package/dist/program.js.map +1 -1
- package/dist/serve-tunnel.d.ts +16 -0
- package/dist/serve-tunnel.js +104 -0
- package/dist/serve-tunnel.js.map +1 -0
- package/docs/architecture/observer.md +7 -0
- package/docs/architecture/serve.md +140 -0
- package/docs/contracts/schemas.md +23 -1
- package/docs/goals/current.md +2 -1
- package/docs/principles/invariants-and-defaults.md +1 -0
- package/docs/ramp/README.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,10 +9,13 @@ It creates committed simulation source under `humanish/`, ignored run evidence
|
|
|
9
9
|
under `.humanish/`, a watchable Observer UI, verification gates, and public-safe
|
|
10
10
|
feedback drafts.
|
|
11
11
|
|
|
12
|
-

|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
A live four-persona study of [drawDB](https://github.com/drawdb-io/drawdb), a
|
|
15
|
+
public open-source database diagram editor, driven against a commit-pinned
|
|
16
|
+
local checkout. Every lane is a real computer-use session on a hosted desktop;
|
|
17
|
+
the captions are each persona's own final report. drawDB is the application
|
|
18
|
+
studied; it is not a Humanish adopter or endorser.
|
|
16
19
|
|
|
17
20
|
## Install
|
|
18
21
|
|
|
@@ -112,6 +115,41 @@ The CI-safe equivalent is:
|
|
|
112
115
|
npx humanish watch --json --no-open
|
|
113
116
|
```
|
|
114
117
|
|
|
118
|
+
## Serve the Library
|
|
119
|
+
|
|
120
|
+
`humanish watch` follows one attached run; `humanish serve` serves the whole
|
|
121
|
+
local run library under `.humanish/runs/` — a library index plus every run's
|
|
122
|
+
Observer page:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npx humanish serve
|
|
126
|
+
npx humanish serve --expose --tunnel ngrok --tunnel-domain observer.example.dev
|
|
127
|
+
npx humanish serve --safe --expose --auth none --tunnel ngrok
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The first serves the library on loopback only. The second is the phone path:
|
|
131
|
+
it prints a secret capability link once; tapping the link mints a session
|
|
132
|
+
cookie, and every request without a valid session is refused. The third is a
|
|
133
|
+
secretless safe observer: no link required, but only runs whose `humanish
|
|
134
|
+
verify` shareSafety is `share_ready` exist at all — everything else is absent
|
|
135
|
+
and 404s.
|
|
136
|
+
|
|
137
|
+
In every mode the server binds `127.0.0.1`; exposure only ever happens through
|
|
138
|
+
a tunnel forwarding to the loopback port. The capability link is minted fresh
|
|
139
|
+
per process: Ctrl-C revokes the link and all sessions, and restarting mints a
|
|
140
|
+
new link.
|
|
141
|
+
|
|
142
|
+
Send the capability link by AirDrop or manual entry rather than a chat app:
|
|
143
|
+
link unfurlers prefetch URLs server-side, and the link is deliberately not
|
|
144
|
+
single-use for exactly that reason — a preview fetch must not burn the link
|
|
145
|
+
before you tap it. On iOS, the first tap can land in an in-app webview whose
|
|
146
|
+
partitioned cookies do not carry over to the real browser; re-tapping the link
|
|
147
|
+
in the real browser mints a fresh session.
|
|
148
|
+
|
|
149
|
+
Live desktop stream URLs are never served in any mode; remote viewers see
|
|
150
|
+
persisted evidence (screenshots, events, terminal tails) only. See
|
|
151
|
+
[Serve architecture](docs/architecture/serve.md).
|
|
152
|
+
|
|
115
153
|
## Commands
|
|
116
154
|
|
|
117
155
|
| Command | Purpose |
|
|
@@ -121,6 +159,7 @@ npx humanish watch --json --no-open
|
|
|
121
159
|
| `humanish run --dry-run` | Generate a synthetic run bundle without browser, keys, or provider spend. |
|
|
122
160
|
| `humanish run --app-url http://127.0.0.1:<port>` | Capture live desktop/mobile browser evidence against a running local app. |
|
|
123
161
|
| `humanish watch [lab]` | Run sims or a named lab, open Observer, and keep watching. |
|
|
162
|
+
| `humanish serve` | Serve the local run library over loopback; optional capability-link or share-safe exposure. |
|
|
124
163
|
| `humanish lab list` | List committed and ignored lab manifests. |
|
|
125
164
|
| `humanish lab inspect <lab>` | Show the source manifest for a lab without running it. |
|
|
126
165
|
| `humanish lab preflight <lab>` | Check lab routing and optional target reachability before actor/model spend. |
|
|
@@ -430,6 +469,7 @@ preserve historical mechanisms and carry explicit amendments near the top.
|
|
|
430
469
|
- [Ramp for future contributors and agents](docs/ramp/README.md)
|
|
431
470
|
- [Project layout](docs/architecture/project-layout.md)
|
|
432
471
|
- [Observer architecture](docs/architecture/observer.md)
|
|
472
|
+
- [Serve: the run library surface](docs/architecture/serve.md)
|
|
433
473
|
- [Actor contract (first-party registry and extension direction)](docs/architecture/actor-contract.md)
|
|
434
474
|
- [State-driven executor (drive a local app, no E2B/vision)](docs/architecture/state-driven-executor.md)
|
|
435
475
|
- [OSS lab design record (historical; see its current safety amendment)](docs/architecture/oss-lab-poc.md)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const SERVE_COOKIE_NAME = "humanish_serve";
|
|
2
|
+
export interface ServeSessionStore {
|
|
3
|
+
mint(): {
|
|
4
|
+
cookieValue: string;
|
|
5
|
+
};
|
|
6
|
+
validate(cookieHeader: string | undefined): boolean;
|
|
7
|
+
revokeAll(): void;
|
|
8
|
+
sessionCount(): number;
|
|
9
|
+
}
|
|
10
|
+
export declare function mintServeToken(): string;
|
|
11
|
+
export declare function sha256Digest(value: string): Buffer;
|
|
12
|
+
export declare function verifyTokenDigest(candidate: string, digest: Buffer): boolean;
|
|
13
|
+
export declare function createServeSessionStore(options: {
|
|
14
|
+
ttlMs: number;
|
|
15
|
+
now?: () => number;
|
|
16
|
+
}): ServeSessionStore;
|
|
17
|
+
export declare function parseCookies(header: string | undefined): Record<string, string>;
|
|
18
|
+
export declare function buildSessionCookie(cookieValue: string, options: {
|
|
19
|
+
ttlSeconds: number;
|
|
20
|
+
secure: boolean;
|
|
21
|
+
}): string;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
export const SERVE_COOKIE_NAME = "humanish_serve";
|
|
3
|
+
export function mintServeToken() {
|
|
4
|
+
return randomBytes(32).toString("base64url");
|
|
5
|
+
}
|
|
6
|
+
export function sha256Digest(value) {
|
|
7
|
+
return createHash("sha256").update(value, "utf8").digest();
|
|
8
|
+
}
|
|
9
|
+
// Hash-first comparison: both sides become fixed-length digests before
|
|
10
|
+
// timingSafeEqual, so an attacker-controlled candidate can neither throw on
|
|
11
|
+
// length mismatch nor learn the token length.
|
|
12
|
+
export function verifyTokenDigest(candidate, digest) {
|
|
13
|
+
return timingSafeEqual(sha256Digest(candidate), digest);
|
|
14
|
+
}
|
|
15
|
+
export function createServeSessionStore(options) {
|
|
16
|
+
const now = options.now ?? (() => Date.now());
|
|
17
|
+
const sessions = new Map();
|
|
18
|
+
return {
|
|
19
|
+
mint() {
|
|
20
|
+
// Sweep expired records on each mint so the map cannot grow without bound
|
|
21
|
+
// when many links are tapped: without this, a record is only pruned when
|
|
22
|
+
// its own cookie is presented again, which an abandoned session never is.
|
|
23
|
+
const currentTime = now();
|
|
24
|
+
for (const [digest, record] of sessions) {
|
|
25
|
+
if (record.expiresAt <= currentTime) {
|
|
26
|
+
sessions.delete(digest);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const cookieValue = randomBytes(32).toString("base64url");
|
|
30
|
+
sessions.set(sha256Digest(cookieValue).toString("hex"), {
|
|
31
|
+
expiresAt: currentTime + options.ttlMs,
|
|
32
|
+
scope: "viewer"
|
|
33
|
+
});
|
|
34
|
+
return { cookieValue };
|
|
35
|
+
},
|
|
36
|
+
validate(cookieHeader) {
|
|
37
|
+
const cookieValue = parseCookies(cookieHeader)[SERVE_COOKIE_NAME];
|
|
38
|
+
if (!cookieValue) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const record = sessions.get(sha256Digest(cookieValue).toString("hex"));
|
|
42
|
+
if (!record || record.scope !== "viewer") {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (record.expiresAt <= now()) {
|
|
46
|
+
sessions.delete(sha256Digest(cookieValue).toString("hex"));
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
},
|
|
51
|
+
revokeAll() {
|
|
52
|
+
sessions.clear();
|
|
53
|
+
},
|
|
54
|
+
sessionCount() {
|
|
55
|
+
return sessions.size;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function parseCookies(header) {
|
|
60
|
+
if (!header) {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
const cookies = {};
|
|
64
|
+
for (const part of header.split(";")) {
|
|
65
|
+
const separator = part.indexOf("=");
|
|
66
|
+
if (separator <= 0) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const name = part.slice(0, separator).trim();
|
|
70
|
+
const value = part.slice(separator + 1).trim();
|
|
71
|
+
if (name) {
|
|
72
|
+
cookies[name] = value;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return cookies;
|
|
76
|
+
}
|
|
77
|
+
export function buildSessionCookie(cookieValue, options) {
|
|
78
|
+
const attributes = [
|
|
79
|
+
`${SERVE_COOKIE_NAME}=${cookieValue}`,
|
|
80
|
+
"Path=/",
|
|
81
|
+
"HttpOnly",
|
|
82
|
+
"SameSite=Lax",
|
|
83
|
+
`Max-Age=${options.ttlSeconds}`
|
|
84
|
+
];
|
|
85
|
+
if (options.secure) {
|
|
86
|
+
attributes.push("Secure");
|
|
87
|
+
}
|
|
88
|
+
// Deliberately no Domain attribute: a host-only cookie on the public origin
|
|
89
|
+
// can never leak to sibling subdomains of a shared tunnel domain.
|
|
90
|
+
return attributes.join("; ");
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=observer-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observer-auth.js","sourceRoot":"","sources":["../src/observer-auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface LibraryHistory {
|
|
2
|
+
latestRunId: string | null;
|
|
3
|
+
runs: Array<{
|
|
4
|
+
runId: string;
|
|
5
|
+
createdAt: string | null;
|
|
6
|
+
mode: string | null;
|
|
7
|
+
href: string;
|
|
8
|
+
status: string;
|
|
9
|
+
streamCount: number;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export interface LibraryRenderOptions {
|
|
13
|
+
mode: "loopback" | "capability-link" | "share-safe-open";
|
|
14
|
+
safe: boolean;
|
|
15
|
+
capabilities: {
|
|
16
|
+
actions: boolean;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export declare function renderLibraryHtml(history: LibraryHistory, opts: LibraryRenderOptions): string;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
const STATUS_TONES = {
|
|
2
|
+
passed: "#3fb970",
|
|
3
|
+
running: "#4f8ff7",
|
|
4
|
+
failed: "#e5534b",
|
|
5
|
+
blocked: "#d29922",
|
|
6
|
+
timed_out: "#d29922"
|
|
7
|
+
};
|
|
8
|
+
export function renderLibraryHtml(history, opts) {
|
|
9
|
+
const emptyState = opts.safe
|
|
10
|
+
? "No share_ready runs yet — run `humanish verify` to see why."
|
|
11
|
+
: "No runs yet — run `humanish watch` to create one.";
|
|
12
|
+
return `<!doctype html>
|
|
13
|
+
<html lang="en" data-theme="dark">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8">
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
17
|
+
<title>Humanish Library</title>
|
|
18
|
+
<style>${libraryCss()}</style>
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<header class="bar">
|
|
22
|
+
<span class="mark">humanish</span>
|
|
23
|
+
<span class="title">Run Library</span>
|
|
24
|
+
<span class="mode">${escapeHtml(modeLabel(opts))}</span>
|
|
25
|
+
</header>
|
|
26
|
+
<main>
|
|
27
|
+
<p class="note" id="note" hidden></p>
|
|
28
|
+
<ul class="runs" id="runs"></ul>
|
|
29
|
+
<p class="empty" id="empty" hidden>${escapeHtml(emptyState)}</p>
|
|
30
|
+
</main>
|
|
31
|
+
<script type="application/json" id="serve-capabilities">${escapeJsonScript(opts.capabilities)}</script>
|
|
32
|
+
<script type="application/json" id="library-data">${escapeJsonScript(history)}</script>
|
|
33
|
+
<script>${libraryClientJs()}</script>
|
|
34
|
+
</body>
|
|
35
|
+
</html>
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
function modeLabel(opts) {
|
|
39
|
+
const auth = opts.mode === "capability-link" ? "capability link" : opts.mode === "share-safe-open" ? "open" : "loopback";
|
|
40
|
+
return opts.safe ? `${auth} · share_ready only` : auth;
|
|
41
|
+
}
|
|
42
|
+
function libraryCss() {
|
|
43
|
+
return `
|
|
44
|
+
:root { color-scheme: dark; }
|
|
45
|
+
* { box-sizing: border-box; margin: 0; }
|
|
46
|
+
body {
|
|
47
|
+
background: #0c0e12; color: #e6e8ec; min-height: 100vh;
|
|
48
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
49
|
+
}
|
|
50
|
+
.bar {
|
|
51
|
+
display: flex; align-items: baseline; gap: 0.6rem;
|
|
52
|
+
padding: 1rem 1.1rem; border-bottom: 1px solid #1e222b;
|
|
53
|
+
position: sticky; top: 0; background: rgba(12,14,18,0.95); backdrop-filter: blur(6px);
|
|
54
|
+
}
|
|
55
|
+
.mark { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-weight: 600; color: #9aa3b2; }
|
|
56
|
+
.title { font-weight: 600; }
|
|
57
|
+
.mode { margin-left: auto; font-size: 0.75rem; color: #9aa3b2; font-family: ui-monospace, "SF Mono", Menlo, monospace; }
|
|
58
|
+
main { padding: 1rem 1.1rem 3rem; max-width: 44rem; margin: 0 auto; }
|
|
59
|
+
.note { font-size: 0.8rem; color: #d29922; padding: 0.5rem 0; }
|
|
60
|
+
.runs { list-style: none; padding: 0; display: grid; gap: 0.6rem; }
|
|
61
|
+
.runs a {
|
|
62
|
+
display: block; text-decoration: none; color: inherit;
|
|
63
|
+
border: 1px solid #1e222b; border-radius: 10px; padding: 0.8rem 0.9rem; background: #11141a;
|
|
64
|
+
}
|
|
65
|
+
.runs a:active { background: #171b23; }
|
|
66
|
+
.run-top { display: flex; align-items: center; gap: 0.5rem; }
|
|
67
|
+
.pip { width: 9px; height: 9px; border-radius: 50%; flex: none; }
|
|
68
|
+
.pip.running { animation: pulse 1.6s ease-in-out infinite; }
|
|
69
|
+
@keyframes pulse { 50% { opacity: 0.35; } }
|
|
70
|
+
.run-id {
|
|
71
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 0.8rem;
|
|
72
|
+
overflow-wrap: anywhere;
|
|
73
|
+
}
|
|
74
|
+
.badge {
|
|
75
|
+
margin-left: auto; flex: none; font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.06em;
|
|
76
|
+
border: 1px solid #2c5f8a; color: #7cb8ec; border-radius: 999px; padding: 0.1rem 0.5rem;
|
|
77
|
+
}
|
|
78
|
+
.badge.latest { border-color: #3fb970; color: #7ee2a8; }
|
|
79
|
+
.run-meta { margin-top: 0.35rem; font-size: 0.75rem; color: #9aa3b2; }
|
|
80
|
+
.empty { color: #9aa3b2; font-size: 0.85rem; padding: 2rem 0; text-align: center; }
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
function libraryClientJs() {
|
|
84
|
+
const tones = JSON.stringify(STATUS_TONES);
|
|
85
|
+
return `
|
|
86
|
+
(function () {
|
|
87
|
+
var TONES = ${tones};
|
|
88
|
+
var POLL_MS = 15000;
|
|
89
|
+
var stopped = false;
|
|
90
|
+
|
|
91
|
+
function render(history) {
|
|
92
|
+
var list = document.getElementById("runs");
|
|
93
|
+
var empty = document.getElementById("empty");
|
|
94
|
+
list.textContent = "";
|
|
95
|
+
var runs = history.runs || [];
|
|
96
|
+
empty.hidden = runs.length !== 0;
|
|
97
|
+
for (var i = 0; i < runs.length; i += 1) {
|
|
98
|
+
var run = runs[i];
|
|
99
|
+
var item = document.createElement("li");
|
|
100
|
+
var link = document.createElement("a");
|
|
101
|
+
link.href = run.href;
|
|
102
|
+
var top = document.createElement("div");
|
|
103
|
+
top.className = "run-top";
|
|
104
|
+
var pip = document.createElement("span");
|
|
105
|
+
pip.className = run.status === "running" ? "pip running" : "pip";
|
|
106
|
+
pip.style.background = TONES[run.status] || "#57606f";
|
|
107
|
+
var id = document.createElement("span");
|
|
108
|
+
id.className = "run-id";
|
|
109
|
+
id.textContent = run.runId;
|
|
110
|
+
top.appendChild(pip);
|
|
111
|
+
top.appendChild(id);
|
|
112
|
+
if (run.runId === history.latestRunId) {
|
|
113
|
+
var latest = document.createElement("span");
|
|
114
|
+
latest.className = "badge latest";
|
|
115
|
+
latest.textContent = "latest";
|
|
116
|
+
top.appendChild(latest);
|
|
117
|
+
} else if (run.status === "running") {
|
|
118
|
+
var live = document.createElement("span");
|
|
119
|
+
live.className = "badge";
|
|
120
|
+
live.textContent = "live";
|
|
121
|
+
top.appendChild(live);
|
|
122
|
+
}
|
|
123
|
+
var meta = document.createElement("div");
|
|
124
|
+
meta.className = "run-meta";
|
|
125
|
+
meta.textContent = [run.mode || "unknown", run.streamCount + " lanes", run.createdAt || ""].filter(Boolean).join(" \\u00b7 ");
|
|
126
|
+
link.appendChild(top);
|
|
127
|
+
link.appendChild(meta);
|
|
128
|
+
item.appendChild(link);
|
|
129
|
+
list.appendChild(item);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function note(text) {
|
|
134
|
+
var element = document.getElementById("note");
|
|
135
|
+
element.hidden = !text;
|
|
136
|
+
element.textContent = text || "";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function poll() {
|
|
140
|
+
if (stopped) return;
|
|
141
|
+
fetch("/_humanish/history.json", { cache: "no-store" }).then(function (response) {
|
|
142
|
+
if (response.status === 401) {
|
|
143
|
+
stopped = true;
|
|
144
|
+
note("session expired \\u2014 tap your capability link again");
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return response.json();
|
|
148
|
+
}).then(function (history) {
|
|
149
|
+
if (history) {
|
|
150
|
+
note("");
|
|
151
|
+
render(history);
|
|
152
|
+
}
|
|
153
|
+
}).catch(function () {
|
|
154
|
+
note("connection lost \\u2014 retrying");
|
|
155
|
+
}).then(function () {
|
|
156
|
+
if (!stopped) setTimeout(poll, POLL_MS);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
render(JSON.parse(document.getElementById("library-data").textContent));
|
|
161
|
+
setTimeout(poll, POLL_MS);
|
|
162
|
+
})();
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
165
|
+
function escapeHtml(value) {
|
|
166
|
+
return value.replace(/[&<>"']/g, (char) => {
|
|
167
|
+
switch (char) {
|
|
168
|
+
case "&":
|
|
169
|
+
return "&";
|
|
170
|
+
case "<":
|
|
171
|
+
return "<";
|
|
172
|
+
case ">":
|
|
173
|
+
return ">";
|
|
174
|
+
case "\"":
|
|
175
|
+
return """;
|
|
176
|
+
default:
|
|
177
|
+
return "'";
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function escapeJsonScript(value) {
|
|
182
|
+
return JSON.stringify(value)
|
|
183
|
+
.replace(/</g, "\\u003c")
|
|
184
|
+
.replace(/>/g, "\\u003e")
|
|
185
|
+
.replace(/&/g, "\\u0026")
|
|
186
|
+
.replace(/\u2028/g, "\\u2028")
|
|
187
|
+
.replace(/\u2029/g, "\\u2029");
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=observer-library.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observer-library.js","sourceRoot":"","sources":["../src/observer-library.ts"],"names":[],"mappings":"AAkBA,MAAM,YAAY,GAA2B;IAC3C,MAAM,EAAE,SAAS;IACjB,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,SAAS;IACjB,OAAO,EAAE,SAAS;IAClB,SAAS,EAAE,SAAS;CACrB,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,OAAuB,EAAE,IAA0B;IACnF,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI;QAC1B,CAAC,CAAC,6DAA6D;QAC/D,CAAC,CAAC,mDAAmD,CAAC;IAExD,OAAO;;;;;;SAMA,UAAU,EAAE;;;;;;uBAME,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;;;;;uCAKX,UAAU,CAAC,UAAU,CAAC;;0DAEH,gBAAgB,CAAC,IAAI,CAAC,YAAY,CAAC;oDACzC,gBAAgB,CAAC,OAAO,CAAC;UACnE,eAAe,EAAE;;;CAG1B,CAAC;AACF,CAAC;AAED,SAAS,SAAS,CAAC,IAA0B;IAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,iBAAiB,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,iBAAiB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC;IACzH,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC;AACzD,CAAC;AAED,SAAS,UAAU;IACjB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCR,CAAC;AACF,CAAC;AAED,SAAS,eAAe;IACtB,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;IAC3C,OAAO;;gBAEO,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4EpB,CAAC;AACF,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE;QACxC,QAAQ,IAAI,EAAE,CAAC;YACb,KAAK,GAAG;gBACN,OAAO,OAAO,CAAC;YACjB,KAAK,GAAG;gBACN,OAAO,MAAM,CAAC;YAChB,KAAK,GAAG;gBACN,OAAO,MAAM,CAAC;YAChB,KAAK,IAAI;gBACP,OAAO,QAAQ,CAAC;YAClB;gBACE,OAAO,OAAO,CAAC;QACnB,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAc;IACtC,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;SACzB,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC;SACxB,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC;SACxB,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC;SACxB,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC;SAC7B,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { PinnedDirectory } from "./observer.js";
|
|
3
|
+
import type { ServeSessionStore } from "./observer-auth.js";
|
|
4
|
+
import type { LibraryHistory } from "./observer-library.js";
|
|
5
|
+
import { verifyRun } from "./run.js";
|
|
6
|
+
export declare const SERVE_SCHEMA = "humanish.serve-result.v1";
|
|
7
|
+
export type ServeMode = "loopback" | "capability-link" | "share-safe-open";
|
|
8
|
+
export type ServeErrorCode = "HUMANISH_INVALID_PORT" | "HUMANISH_SERVE_INVALID_TTL" | "HUMANISH_SERVE_OPTION_CONFLICT" | "HUMANISH_SERVE_TUNNEL_REQUIRES_EXPOSE" | "HUMANISH_SERVE_EXPOSE_REQUIRES_ORIGIN" | "HUMANISH_SERVE_OPEN_REQUIRES_SAFE" | "HUMANISH_SERVE_TUNNEL_NOT_FOUND" | "HUMANISH_SERVE_TUNNEL_START_FAILED" | "HUMANISH_RUN_NOT_FOUND" | "HUMANISH_SERVE_RUN_NOT_SHAREABLE";
|
|
9
|
+
export interface ServeResult {
|
|
10
|
+
schema: typeof SERVE_SCHEMA;
|
|
11
|
+
ok: boolean;
|
|
12
|
+
cwd: string;
|
|
13
|
+
mode: ServeMode;
|
|
14
|
+
safe: boolean;
|
|
15
|
+
host: "127.0.0.1";
|
|
16
|
+
port?: number;
|
|
17
|
+
url?: string;
|
|
18
|
+
capabilityUrl?: string;
|
|
19
|
+
publicUrl?: string;
|
|
20
|
+
publicCapabilityUrl?: string;
|
|
21
|
+
tunnel?: {
|
|
22
|
+
provider: "ngrok";
|
|
23
|
+
url: string;
|
|
24
|
+
};
|
|
25
|
+
ttlMinutes?: number;
|
|
26
|
+
runsListed: number;
|
|
27
|
+
shareReadyCount?: number;
|
|
28
|
+
entryRunId?: string;
|
|
29
|
+
opened?: boolean;
|
|
30
|
+
openCommand?: string;
|
|
31
|
+
warnings: string[];
|
|
32
|
+
error?: {
|
|
33
|
+
code: ServeErrorCode;
|
|
34
|
+
message: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export interface ServeControlPlane {
|
|
38
|
+
startRun?(request: {
|
|
39
|
+
labId: string;
|
|
40
|
+
dryRun: boolean;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
accepted: boolean;
|
|
43
|
+
runId?: string;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
export interface ShareSafetyAdmission {
|
|
47
|
+
admit(runId: string): Promise<boolean>;
|
|
48
|
+
}
|
|
49
|
+
export declare function createShareSafetyAdmission(cwd: string, options?: {
|
|
50
|
+
verifyImpl?: typeof verifyRun;
|
|
51
|
+
ttlMs?: number;
|
|
52
|
+
now?: () => number;
|
|
53
|
+
}): ShareSafetyAdmission;
|
|
54
|
+
export declare function buildServeSecurityHeaders(): Record<string, string>;
|
|
55
|
+
export declare function hostAllowed(hostHeader: string | undefined, allowlist: ReadonlySet<string>): boolean;
|
|
56
|
+
export declare function parsePublicOrigin(value: string): {
|
|
57
|
+
origin: string;
|
|
58
|
+
host: string;
|
|
59
|
+
scheme: "http" | "https";
|
|
60
|
+
} | null;
|
|
61
|
+
export interface ServeAuthContext {
|
|
62
|
+
tokenDigest: Buffer;
|
|
63
|
+
sessions: ServeSessionStore;
|
|
64
|
+
ttlSeconds: number;
|
|
65
|
+
publicScheme?: "http" | "https";
|
|
66
|
+
}
|
|
67
|
+
export interface ServeRequestHandlerOptions {
|
|
68
|
+
proofRoot: PinnedDirectory;
|
|
69
|
+
safe: boolean;
|
|
70
|
+
admit: (runId: string) => Promise<boolean>;
|
|
71
|
+
auth?: ServeAuthContext | null;
|
|
72
|
+
hostAllowlist: ReadonlySet<string>;
|
|
73
|
+
entryRunId?: string;
|
|
74
|
+
renderLibrary: (history: LibraryHistory) => string;
|
|
75
|
+
controlPlane?: ServeControlPlane;
|
|
76
|
+
}
|
|
77
|
+
export declare function createServeRequestHandler(options: ServeRequestHandlerOptions): (request: IncomingMessage, response: ServerResponse) => Promise<void>;
|
|
78
|
+
export interface ServeLibraryOptions {
|
|
79
|
+
port: number;
|
|
80
|
+
safe: boolean;
|
|
81
|
+
expose: boolean;
|
|
82
|
+
authMode: "link" | "none";
|
|
83
|
+
ttlMinutes: number;
|
|
84
|
+
publicOrigin?: string;
|
|
85
|
+
entryRunId?: string;
|
|
86
|
+
controlPlane?: ServeControlPlane;
|
|
87
|
+
verifyImpl?: typeof verifyRun;
|
|
88
|
+
now?: () => number;
|
|
89
|
+
}
|
|
90
|
+
export interface ServeLibraryServer {
|
|
91
|
+
url: string;
|
|
92
|
+
port: number;
|
|
93
|
+
mode: ServeMode;
|
|
94
|
+
capabilityToken?: string;
|
|
95
|
+
runsListed: number;
|
|
96
|
+
shareReadyCount?: number;
|
|
97
|
+
entryRunId?: string;
|
|
98
|
+
addPublicOrigin(origin: string): void;
|
|
99
|
+
close(): Promise<void>;
|
|
100
|
+
}
|
|
101
|
+
export type ServeLibraryStart = {
|
|
102
|
+
ok: true;
|
|
103
|
+
server: ServeLibraryServer;
|
|
104
|
+
} | {
|
|
105
|
+
ok: false;
|
|
106
|
+
error: {
|
|
107
|
+
code: ServeErrorCode;
|
|
108
|
+
message: string;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
export declare function serveObserverLibrary(cwdInput: string, options: ServeLibraryOptions): Promise<ServeLibraryStart>;
|