@oharness/server 0.1.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/dist/analytics.d.ts +47 -0
- package/dist/analytics.js +323 -0
- package/dist/analytics.js.map +1 -0
- package/dist/artifacts.d.ts +53 -0
- package/dist/artifacts.js +129 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/canvas.d.ts +28 -0
- package/dist/canvas.js +646 -0
- package/dist/canvas.js.map +1 -0
- package/dist/changes.d.ts +54 -0
- package/dist/changes.js +154 -0
- package/dist/changes.js.map +1 -0
- package/dist/files.d.ts +48 -0
- package/dist/files.js +168 -0
- package/dist/files.js.map +1 -0
- package/dist/i18n.d.ts +15 -0
- package/dist/i18n.js +625 -0
- package/dist/i18n.js.map +1 -0
- package/dist/icons.d.ts +9 -0
- package/dist/icons.js +197 -0
- package/dist/icons.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +148 -0
- package/dist/main.js.map +1 -0
- package/dist/markdown.d.ts +10 -0
- package/dist/markdown.js +253 -0
- package/dist/markdown.js.map +1 -0
- package/dist/page.d.ts +1 -0
- package/dist/page.js +8591 -0
- package/dist/page.js.map +1 -0
- package/dist/pick.d.ts +62 -0
- package/dist/pick.js +215 -0
- package/dist/pick.js.map +1 -0
- package/dist/prefs.d.ts +59 -0
- package/dist/prefs.js +66 -0
- package/dist/prefs.js.map +1 -0
- package/dist/projects.d.ts +34 -0
- package/dist/projects.js +129 -0
- package/dist/projects.js.map +1 -0
- package/dist/server.d.ts +50 -0
- package/dist/server.js +2789 -0
- package/dist/server.js.map +1 -0
- package/package.json +36 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface AnalyticsState {
|
|
2
|
+
/** True only when this build carries a vendor and the user has opted in. */
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Whether this build carries either vendor at all — the difference between
|
|
6
|
+
* "off" and "absent". One switch covers both: a person consenting to usage
|
|
7
|
+
* analytics is not being asked to arbitrate between two vendors they did not
|
|
8
|
+
* choose, and a build that carries only one is the normal case.
|
|
9
|
+
*/
|
|
10
|
+
available: boolean;
|
|
11
|
+
/** PostHog, present only when enabled and this build carries a key. */
|
|
12
|
+
key?: string;
|
|
13
|
+
host?: string;
|
|
14
|
+
options?: Record<string, unknown>;
|
|
15
|
+
/** DataFast, present only when enabled and this build carries an id. */
|
|
16
|
+
datafast?: {
|
|
17
|
+
id: string;
|
|
18
|
+
domain: string;
|
|
19
|
+
src: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* What `GET /analytics` answers.
|
|
24
|
+
*
|
|
25
|
+
* `available` is reported separately so Settings can say "this build carries
|
|
26
|
+
* no analytics" rather than showing a switch that would do nothing — a control
|
|
27
|
+
* that silently does nothing is worse than no control.
|
|
28
|
+
*/
|
|
29
|
+
export declare function analyticsState(): Promise<AnalyticsState>;
|
|
30
|
+
/**
|
|
31
|
+
* The queue, for the document head.
|
|
32
|
+
*
|
|
33
|
+
* Boot fires events before anything could have decided whether to send them,
|
|
34
|
+
* so they wait here. Nothing in this stub touches the network: it exists only
|
|
35
|
+
* so that `track()` reads the same whether analytics end up on or off, and so
|
|
36
|
+
* that no caller has to remember which branch it is writing in.
|
|
37
|
+
*/
|
|
38
|
+
export declare const ANALYTICS_STUB = "<script>\nwindow.__ANALYTICS_QUEUE = [];\nwindow.__TRACK = function (event, props) {\n // Bounded: a queue nothing will ever drain is a leak in a page left open all\n // day, and the events worth having are the early ones anyway.\n if (window.__ANALYTICS_QUEUE.length < 100) window.__ANALYTICS_QUEUE.push([event, props]);\n};\n</script>";
|
|
39
|
+
/**
|
|
40
|
+
* The loader, appended to the client script where the token already exists.
|
|
41
|
+
*
|
|
42
|
+
* One request to `/analytics` answers for both vendors, and one consent
|
|
43
|
+
* decides both — so a person who has not opted in has neither vendor's key in
|
|
44
|
+
* their page source, which is the property that made the key server-served in
|
|
45
|
+
* the first place.
|
|
46
|
+
*/
|
|
47
|
+
export declare const ANALYTICS_CLIENT: string;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { readPrefs } from "./prefs.js";
|
|
2
|
+
/**
|
|
3
|
+
* Usage analytics for the browser frontend. Off until the person using it says
|
|
4
|
+
* otherwise, and absent entirely from a build nobody published.
|
|
5
|
+
*
|
|
6
|
+
* This is not the website. The page this feeds runs on the user's own machine,
|
|
7
|
+
* in front of an agent that has a shell there, and its address carries the
|
|
8
|
+
* token that grants that shell. So it is gated twice, and the two gates answer
|
|
9
|
+
* different questions:
|
|
10
|
+
*
|
|
11
|
+
* A key, which only an official build carries. It is injected at packaging
|
|
12
|
+
* time and is not in this repository — deliberately, because a default
|
|
13
|
+
* compiled into the source would mean every fork and every self-hosted
|
|
14
|
+
* install reporting into the publisher's dashboard. Someone who builds this
|
|
15
|
+
* themselves gets a build that cannot report anywhere, and gets it without
|
|
16
|
+
* having to configure anything.
|
|
17
|
+
*
|
|
18
|
+
* Consent, which only the user grants. Stored in prefs, absent by default,
|
|
19
|
+
* and revocable from Settings.
|
|
20
|
+
*
|
|
21
|
+
* Neither implies the other, and the page loads nothing until both are true —
|
|
22
|
+
* which is why the key is served from `/analytics` rather than inlined into
|
|
23
|
+
* the HTML. A user who never opts in never has it in their page source at all.
|
|
24
|
+
*
|
|
25
|
+
* What it sends: named events for actions taken — a panel opened, a prompt
|
|
26
|
+
* submitted, a permission answered, a model switched — and nothing about their
|
|
27
|
+
* content. No prompts, no file paths, no diffs, no transcript. Autocapture is
|
|
28
|
+
* off, so a click is never recorded by the text on the button; session replay
|
|
29
|
+
* is off; and URL properties are stripped, because the URL is where the token
|
|
30
|
+
* is. See `track()` in page.ts for the calls themselves.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* The published build's key, or nothing.
|
|
34
|
+
*
|
|
35
|
+
* Read from the environment so that the desktop shell can set it from the file
|
|
36
|
+
* packaging injects, and so that `oharness-web` run from a checkout can be
|
|
37
|
+
* pointed at a project of its own for a day without editing anything.
|
|
38
|
+
*/
|
|
39
|
+
const KEY = process.env["OHARNESS_POSTHOG_KEY"] ?? "";
|
|
40
|
+
/** US cloud unless told otherwise. EU is https://eu.i.posthog.com. */
|
|
41
|
+
const HOST = process.env["OHARNESS_POSTHOG_HOST"] ?? "https://us.i.posthog.com";
|
|
42
|
+
/**
|
|
43
|
+
* DataFast, alongside PostHog and under the same two gates.
|
|
44
|
+
*
|
|
45
|
+
* The site runs both for the reason written in the website's own
|
|
46
|
+
* `lib/analytics.ts`: PostHog answers product questions, DataFast answers "how
|
|
47
|
+
* many, from where" without anyone learning a query language. Here it answers
|
|
48
|
+
* one question PostHog also answers — how many launches, from which country —
|
|
49
|
+
* and it is here so that the desktop app and the site show up on the same
|
|
50
|
+
* dashboard rather than in two places that have to be added up by hand.
|
|
51
|
+
*
|
|
52
|
+
* Both shells, and gated on the one thing that actually matters: whether this
|
|
53
|
+
* page's address carries the token.
|
|
54
|
+
*
|
|
55
|
+
* DataFast's script reads `window.location.href` unconditionally and has no
|
|
56
|
+
* hook to filter what it sends — its entire configuration surface is six
|
|
57
|
+
* `data-*` attributes, none of which redacts anything. PostHog has
|
|
58
|
+
* `property_denylist` and `before_send`; DataFast has nothing, so the URL has
|
|
59
|
+
* to be clean before the script is allowed to exist.
|
|
60
|
+
*
|
|
61
|
+
* Today that means the desktop shell and not `oharness-web`, which is where
|
|
62
|
+
* this landed anyway — but by measuring the thing itself rather than by asking
|
|
63
|
+
* which shell it is. `IS_DESKTOP` would be a proxy for the real question and
|
|
64
|
+
* would silently become wrong the day a third shell appears, or the day the
|
|
65
|
+
* desktop window is handed a URL with a query string on it.
|
|
66
|
+
*
|
|
67
|
+
* Taking the token out of the browser's address bar was tried and does not
|
|
68
|
+
* work: the document request on a reload is the one the page cannot influence,
|
|
69
|
+
* so a scrubbed URL means F5 returns 401. The desktop shell gets around that
|
|
70
|
+
* with `onBeforeSendHeaders`; a browser tab has no equivalent. Anyone
|
|
71
|
+
* revisiting this should read that as the constraint it is — browser-mode
|
|
72
|
+
* DataFast needs the credential to stop travelling in the URL, and that is an
|
|
73
|
+
* authentication change, not an analytics one.
|
|
74
|
+
*/
|
|
75
|
+
const DATAFAST_ID = process.env["OHARNESS_DATAFAST_ID"] ?? "";
|
|
76
|
+
/**
|
|
77
|
+
* The site this files under. `oharness.ai`, not the page's own hostname —
|
|
78
|
+
* the page's hostname is `127.0.0.1`, which is nobody's site.
|
|
79
|
+
*/
|
|
80
|
+
const DATAFAST_DOMAIN = process.env["OHARNESS_DATAFAST_DOMAIN"] ?? "oharness.ai";
|
|
81
|
+
/** Served from DataFast's own origin. There is nothing here to proxy it through. */
|
|
82
|
+
const DATAFAST_SRC = "https://datafa.st/js/script.js";
|
|
83
|
+
/**
|
|
84
|
+
* Every property PostHog would otherwise attach that carries the address of
|
|
85
|
+
* this page.
|
|
86
|
+
*
|
|
87
|
+
* The address is `http://127.0.0.1:<port>/?token=<uuid>`, and that token is
|
|
88
|
+
* the whole of the server's authentication. It must not travel, and "the
|
|
89
|
+
* analytics vendor is trustworthy" is not the argument to rely on — a property
|
|
90
|
+
* that is never collected cannot be leaked by anyone's later mistake.
|
|
91
|
+
*
|
|
92
|
+
* The `$session_entry_` family is easy to miss and was: it is filled in once
|
|
93
|
+
* when a session starts and then rides along on every event afterwards, so
|
|
94
|
+
* denying `$current_url` alone still sent the URL — with the token in it — on
|
|
95
|
+
* all of them. Anything added to this list wants checking against a real
|
|
96
|
+
* payload rather than against the documentation.
|
|
97
|
+
*/
|
|
98
|
+
const URL_PROPERTIES = [
|
|
99
|
+
"$current_url",
|
|
100
|
+
"$pathname",
|
|
101
|
+
"$host",
|
|
102
|
+
"$referrer",
|
|
103
|
+
"$referring_domain",
|
|
104
|
+
"$initial_current_url",
|
|
105
|
+
"$initial_pathname",
|
|
106
|
+
"$initial_host",
|
|
107
|
+
"$initial_referrer",
|
|
108
|
+
"$initial_referring_domain",
|
|
109
|
+
"$session_entry_url",
|
|
110
|
+
"$session_entry_host",
|
|
111
|
+
"$session_entry_pathname",
|
|
112
|
+
"$session_entry_referrer",
|
|
113
|
+
"$session_entry_referring_domain",
|
|
114
|
+
];
|
|
115
|
+
const OPTIONS = {
|
|
116
|
+
api_host: HOST,
|
|
117
|
+
// Autocapture records clicks by the element they landed on, including the
|
|
118
|
+
// text of the button. In this page that text is session titles, file names
|
|
119
|
+
// and model ids the user typed. Named events only.
|
|
120
|
+
autocapture: false,
|
|
121
|
+
capture_pageview: false,
|
|
122
|
+
capture_pageleave: false,
|
|
123
|
+
rageclick: false,
|
|
124
|
+
disable_session_recording: true,
|
|
125
|
+
disable_surveys: true,
|
|
126
|
+
// Nothing here reads a flag or shows a survey, so PostHog never needs to
|
|
127
|
+
// fetch a second script — and with this set it cannot. A page that can be
|
|
128
|
+
// reached with the token is not a page to leave an open script channel in.
|
|
129
|
+
disable_external_dependency_loading: true,
|
|
130
|
+
advanced_disable_feature_flags: true,
|
|
131
|
+
// Counting installs, not people: no profile is created, and the identifier
|
|
132
|
+
// is a random id in this browser's local storage that is never joined to
|
|
133
|
+
// anything. Nothing in the app calls `identify`.
|
|
134
|
+
person_profiles: "never",
|
|
135
|
+
persistence: "localStorage",
|
|
136
|
+
property_denylist: URL_PROPERTIES,
|
|
137
|
+
// Someone who has asked their browser not to be tracked has answered this
|
|
138
|
+
// question already.
|
|
139
|
+
respect_dnt: true,
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* What `GET /analytics` answers.
|
|
143
|
+
*
|
|
144
|
+
* `available` is reported separately so Settings can say "this build carries
|
|
145
|
+
* no analytics" rather than showing a switch that would do nothing — a control
|
|
146
|
+
* that silently does nothing is worse than no control.
|
|
147
|
+
*/
|
|
148
|
+
export async function analyticsState() {
|
|
149
|
+
const available = KEY !== "" || DATAFAST_ID !== "";
|
|
150
|
+
const consented = (await readPrefs()).analytics === true;
|
|
151
|
+
if (!available || !consented)
|
|
152
|
+
return { enabled: false, available };
|
|
153
|
+
// Each vendor is reported only if this build carries it. A build with one
|
|
154
|
+
// and not the other is not a broken build — it is what a fork gets, and what
|
|
155
|
+
// an official build gets if only one id made it into the packaging step.
|
|
156
|
+
const state = { enabled: true, available };
|
|
157
|
+
if (KEY !== "") {
|
|
158
|
+
state.key = KEY;
|
|
159
|
+
state.host = HOST;
|
|
160
|
+
state.options = OPTIONS;
|
|
161
|
+
}
|
|
162
|
+
if (DATAFAST_ID !== "") {
|
|
163
|
+
state.datafast = { id: DATAFAST_ID, domain: DATAFAST_DOMAIN, src: DATAFAST_SRC };
|
|
164
|
+
}
|
|
165
|
+
return state;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The queue, for the document head.
|
|
169
|
+
*
|
|
170
|
+
* Boot fires events before anything could have decided whether to send them,
|
|
171
|
+
* so they wait here. Nothing in this stub touches the network: it exists only
|
|
172
|
+
* so that `track()` reads the same whether analytics end up on or off, and so
|
|
173
|
+
* that no caller has to remember which branch it is writing in.
|
|
174
|
+
*/
|
|
175
|
+
export const ANALYTICS_STUB = `<script>
|
|
176
|
+
window.__ANALYTICS_QUEUE = [];
|
|
177
|
+
window.__TRACK = function (event, props) {
|
|
178
|
+
// Bounded: a queue nothing will ever drain is a leak in a page left open all
|
|
179
|
+
// day, and the events worth having are the early ones anyway.
|
|
180
|
+
if (window.__ANALYTICS_QUEUE.length < 100) window.__ANALYTICS_QUEUE.push([event, props]);
|
|
181
|
+
};
|
|
182
|
+
</script>`;
|
|
183
|
+
/**
|
|
184
|
+
* What a build carrying neither vendor ships instead of the loader.
|
|
185
|
+
*
|
|
186
|
+
* Not the loader with a disabled flag: a page that cannot report anywhere
|
|
187
|
+
* should not carry the code for reporting, and `PAGE` should not so much as
|
|
188
|
+
* contain either vendor's name. The suite asserts exactly that, which is how a
|
|
189
|
+
* later change that quietly starts shipping the loader to everyone gets
|
|
190
|
+
* caught. It still drains the queue, because `track()` calls are unconditional
|
|
191
|
+
* and a queue nobody empties grows all day.
|
|
192
|
+
*/
|
|
193
|
+
const NO_ANALYTICS_CLIENT = String.raw `
|
|
194
|
+
function startAnalytics() {
|
|
195
|
+
window.__TRACK = function () {};
|
|
196
|
+
window.__ANALYTICS_QUEUE = [];
|
|
197
|
+
}
|
|
198
|
+
`;
|
|
199
|
+
/**
|
|
200
|
+
* The DataFast tag, or a no-op where this build carries no id.
|
|
201
|
+
*
|
|
202
|
+
* Two conditions, and the second is the load-bearing one. `config.datafast`
|
|
203
|
+
* says this build carries an id and the user has consented; the `href` check
|
|
204
|
+
* says this page's address has no token in it. See the comment on
|
|
205
|
+
* `DATAFAST_ID` for why the second cannot be relaxed — the script reads
|
|
206
|
+
* `location.href` and offers nothing to filter it with.
|
|
207
|
+
*
|
|
208
|
+
* Checked against the live `href` rather than against which shell this is, so
|
|
209
|
+
* that a regression in the token scrub turns this off rather than turning it
|
|
210
|
+
* into a leak. It is the last gate before a third party sees the address, and
|
|
211
|
+
* the only one that cannot be wrong about what the address contains.
|
|
212
|
+
*
|
|
213
|
+
* `data-allow-localhost` because the page is served from 127.0.0.1 and the
|
|
214
|
+
* script drops every event from a loopback host unless told not to. That flag
|
|
215
|
+
* is the only reason this reports at all, and it is also the reason the check
|
|
216
|
+
* above has to hold.
|
|
217
|
+
*/
|
|
218
|
+
const DATAFAST_CLIENT = DATAFAST_ID === "" ? String.raw `
|
|
219
|
+
function startDataFast() {}
|
|
220
|
+
` : String.raw `
|
|
221
|
+
function startDataFast(config) {
|
|
222
|
+
if (!config.datafast) return;
|
|
223
|
+
if (location.href.indexOf("token=") !== -1) return;
|
|
224
|
+
try {
|
|
225
|
+
const script = document.createElement("script");
|
|
226
|
+
script.src = config.datafast.src;
|
|
227
|
+
script.defer = true;
|
|
228
|
+
script.setAttribute("data-website-id", config.datafast.id);
|
|
229
|
+
script.setAttribute("data-domain", config.datafast.domain);
|
|
230
|
+
script.setAttribute("data-allow-localhost", "true");
|
|
231
|
+
document.head.appendChild(script);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
// Analytics that fail must never be something a person notices while
|
|
234
|
+
// trying to work. There is nothing to retry and nothing to report.
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
`;
|
|
238
|
+
/**
|
|
239
|
+
* PostHog, or a no-op where this build carries no key.
|
|
240
|
+
*
|
|
241
|
+
* `array.js` loaded on its own rather than PostHog's inlined snippet: the
|
|
242
|
+
* snippet is a minified blob whose job is to queue calls made before the
|
|
243
|
+
* library arrives, and this does that job in code that can be read — which is
|
|
244
|
+
* the bar for anything that runs in this page.
|
|
245
|
+
*
|
|
246
|
+
* This is the half that drains the queue in every branch, because `track()` is
|
|
247
|
+
* PostHog's alone — DataFast counts launches and has no named events to send.
|
|
248
|
+
*
|
|
249
|
+
* A failure at any point is silence. Analytics that cannot reach their server
|
|
250
|
+
* must never be something a person notices while trying to work.
|
|
251
|
+
*/
|
|
252
|
+
const POSTHOG_CLIENT = KEY === "" ? String.raw `
|
|
253
|
+
function startPostHog() {
|
|
254
|
+
window.__TRACK = function () {};
|
|
255
|
+
window.__ANALYTICS_QUEUE = [];
|
|
256
|
+
}
|
|
257
|
+
` : String.raw `
|
|
258
|
+
function startPostHog(config) {
|
|
259
|
+
if (!config.key) {
|
|
260
|
+
window.__TRACK = function () {};
|
|
261
|
+
window.__ANALYTICS_QUEUE = [];
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const script = document.createElement("script");
|
|
266
|
+
script.src = config.host + "/static/array.js";
|
|
267
|
+
script.async = true;
|
|
268
|
+
script.onload = function () {
|
|
269
|
+
try {
|
|
270
|
+
window.posthog.init(config.key, Object.assign({}, config.options, {
|
|
271
|
+
// The denylist names the properties known to carry this page's address
|
|
272
|
+
// today. This catches the ones a future version of the library adds:
|
|
273
|
+
// whatever the property is called, if its value contains the token
|
|
274
|
+
// then it is the URL, and it does not leave.
|
|
275
|
+
before_send: function (event) {
|
|
276
|
+
const props = event && event.properties;
|
|
277
|
+
for (const key in props) {
|
|
278
|
+
if (typeof props[key] === "string" && props[key].indexOf("token=") !== -1) {
|
|
279
|
+
delete props[key];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return event;
|
|
283
|
+
},
|
|
284
|
+
}));
|
|
285
|
+
window.__TRACK = function (event, props) { window.posthog.capture(event, props); };
|
|
286
|
+
for (const queued of window.__ANALYTICS_QUEUE) window.__TRACK(queued[0], queued[1]);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
window.__TRACK = function () {};
|
|
289
|
+
}
|
|
290
|
+
window.__ANALYTICS_QUEUE = [];
|
|
291
|
+
};
|
|
292
|
+
script.onerror = function () {
|
|
293
|
+
window.__TRACK = function () {};
|
|
294
|
+
window.__ANALYTICS_QUEUE = [];
|
|
295
|
+
};
|
|
296
|
+
document.head.appendChild(script);
|
|
297
|
+
}
|
|
298
|
+
`;
|
|
299
|
+
/**
|
|
300
|
+
* The loader, appended to the client script where the token already exists.
|
|
301
|
+
*
|
|
302
|
+
* One request to `/analytics` answers for both vendors, and one consent
|
|
303
|
+
* decides both — so a person who has not opted in has neither vendor's key in
|
|
304
|
+
* their page source, which is the property that made the key server-served in
|
|
305
|
+
* the first place.
|
|
306
|
+
*/
|
|
307
|
+
export const ANALYTICS_CLIENT = KEY === "" && DATAFAST_ID === "" ? NO_ANALYTICS_CLIENT : String.raw `
|
|
308
|
+
${DATAFAST_CLIENT}
|
|
309
|
+
${POSTHOG_CLIENT}
|
|
310
|
+
async function startAnalytics() {
|
|
311
|
+
const config = await getJson("/analytics");
|
|
312
|
+
if (!config || config.error || !config.enabled) {
|
|
313
|
+
// Off, or this build carries nothing. Drop the queue rather than hold it:
|
|
314
|
+
// the answer will not change without a reload.
|
|
315
|
+
window.__TRACK = function () {};
|
|
316
|
+
window.__ANALYTICS_QUEUE = [];
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
startDataFast(config);
|
|
320
|
+
startPostHog(config);
|
|
321
|
+
}
|
|
322
|
+
`;
|
|
323
|
+
//# sourceMappingURL=analytics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics.js","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
import type { Artifact, ArtifactVersion } from "@oharness/engine/artifacts/store.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Serving an artifact's body to a browser that is going to execute it.
|
|
5
|
+
*
|
|
6
|
+
* The rest of this server renders untrusted text by building DOM nodes, never
|
|
7
|
+
* by handing markup to the browser — see markdown.ts, where that is the whole
|
|
8
|
+
* design. An HTML artifact is the one case that cannot work that way: its
|
|
9
|
+
* value *is* the browser executing it. So instead of loosening the renderer,
|
|
10
|
+
* this puts the execution somewhere it can do no harm, and the isolation is
|
|
11
|
+
* built from three independent layers because any one of them alone has a
|
|
12
|
+
* known hole:
|
|
13
|
+
*
|
|
14
|
+
* 1. `sandbox="allow-scripts"` on the iframe, without `allow-same-origin`.
|
|
15
|
+
* Those two together are famously equivalent to no sandbox at all, since
|
|
16
|
+
* the frame can then reach up and remove its own sandbox attribute.
|
|
17
|
+
* Without it the document gets an opaque origin and cannot touch the
|
|
18
|
+
* page, its storage, or its cookies.
|
|
19
|
+
* 2. The CSP below, delivered as a header. `connect-src 'none'` is the load
|
|
20
|
+
* bearing one — an opaque origin still stops nothing from *sending* a
|
|
21
|
+
* request, and this server is a loopback endpoint that drives an agent
|
|
22
|
+
* which runs shell commands. A `fetch(..., {mode:'no-cors'})` reads
|
|
23
|
+
* nothing but arrives all the same.
|
|
24
|
+
* 3. A separate credential (see `artifactToken` in server.ts). The frame can
|
|
25
|
+
* read its own URL, so whatever authorises this route is readable by the
|
|
26
|
+
* content it serves. Making that a different secret from the session
|
|
27
|
+
* token means the worst case is an artifact being readable, not an agent
|
|
28
|
+
* being drivable.
|
|
29
|
+
*/
|
|
30
|
+
declare const CSP: string;
|
|
31
|
+
/**
|
|
32
|
+
* The document an iframe loads.
|
|
33
|
+
*
|
|
34
|
+
* `page` artifacts are served as they were written — the model authored a
|
|
35
|
+
* complete document and wrapping it would break any `<!doctype>` or `<head>`
|
|
36
|
+
* it set up. Everything else is wrapped in a minimal shell, because a bare
|
|
37
|
+
* SVG or a bare text file rendered as a top-level document inherits the
|
|
38
|
+
* browser's defaults and looks broken next to the rest of the UI.
|
|
39
|
+
*/
|
|
40
|
+
export declare function artifactDocument(artifact: Artifact, revision: ArtifactVersion): {
|
|
41
|
+
body: string;
|
|
42
|
+
contentType: string;
|
|
43
|
+
};
|
|
44
|
+
export declare function sendArtifact(res: ServerResponse, artifact: Artifact, revision: ArtifactVersion): void;
|
|
45
|
+
/**
|
|
46
|
+
* Escaping for the wrapper above.
|
|
47
|
+
*
|
|
48
|
+
* Only ever applied to the non-executable kinds. The `page` kind is
|
|
49
|
+
* deliberately *not* escaped — escaping it would defeat its purpose — which is
|
|
50
|
+
* exactly why that kind is the one the sandbox and the CSP exist for.
|
|
51
|
+
*/
|
|
52
|
+
export declare function escapeHtml(text: string): string;
|
|
53
|
+
export { CSP as ARTIFACT_CSP };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { isCanvas } from "@oharness/engine/artifacts/store.ts";
|
|
2
|
+
import { canvasDocument } from "./canvas.js";
|
|
3
|
+
/**
|
|
4
|
+
* Serving an artifact's body to a browser that is going to execute it.
|
|
5
|
+
*
|
|
6
|
+
* The rest of this server renders untrusted text by building DOM nodes, never
|
|
7
|
+
* by handing markup to the browser — see markdown.ts, where that is the whole
|
|
8
|
+
* design. An HTML artifact is the one case that cannot work that way: its
|
|
9
|
+
* value *is* the browser executing it. So instead of loosening the renderer,
|
|
10
|
+
* this puts the execution somewhere it can do no harm, and the isolation is
|
|
11
|
+
* built from three independent layers because any one of them alone has a
|
|
12
|
+
* known hole:
|
|
13
|
+
*
|
|
14
|
+
* 1. `sandbox="allow-scripts"` on the iframe, without `allow-same-origin`.
|
|
15
|
+
* Those two together are famously equivalent to no sandbox at all, since
|
|
16
|
+
* the frame can then reach up and remove its own sandbox attribute.
|
|
17
|
+
* Without it the document gets an opaque origin and cannot touch the
|
|
18
|
+
* page, its storage, or its cookies.
|
|
19
|
+
* 2. The CSP below, delivered as a header. `connect-src 'none'` is the load
|
|
20
|
+
* bearing one — an opaque origin still stops nothing from *sending* a
|
|
21
|
+
* request, and this server is a loopback endpoint that drives an agent
|
|
22
|
+
* which runs shell commands. A `fetch(..., {mode:'no-cors'})` reads
|
|
23
|
+
* nothing but arrives all the same.
|
|
24
|
+
* 3. A separate credential (see `artifactToken` in server.ts). The frame can
|
|
25
|
+
* read its own URL, so whatever authorises this route is readable by the
|
|
26
|
+
* content it serves. Making that a different secret from the session
|
|
27
|
+
* token means the worst case is an artifact being readable, not an agent
|
|
28
|
+
* being drivable.
|
|
29
|
+
*/
|
|
30
|
+
const CSP = [
|
|
31
|
+
// Nothing loads unless a directive below says otherwise.
|
|
32
|
+
"default-src 'none'",
|
|
33
|
+
// Inline only: an artifact is one self-contained document by construction,
|
|
34
|
+
// and allowing an origin here would be allowing exfiltration through it.
|
|
35
|
+
"script-src 'unsafe-inline'",
|
|
36
|
+
"style-src 'unsafe-inline'",
|
|
37
|
+
// Data URIs only. A remote image URL is a GET with an attacker-chosen path,
|
|
38
|
+
// which is all exfiltration needs.
|
|
39
|
+
"img-src data:",
|
|
40
|
+
"font-src data:",
|
|
41
|
+
// The three egress channels that are not covered by the source directives.
|
|
42
|
+
"connect-src 'none'",
|
|
43
|
+
"form-action 'none'",
|
|
44
|
+
"base-uri 'none'",
|
|
45
|
+
// An artifact has no business nesting frames of its own, and a frame it
|
|
46
|
+
// controls is a place to render something that looks like the app.
|
|
47
|
+
"frame-src 'none'",
|
|
48
|
+
"child-src 'none'",
|
|
49
|
+
// Only this server's own page may frame an artifact.
|
|
50
|
+
"frame-ancestors 'self'",
|
|
51
|
+
].join("; ");
|
|
52
|
+
/** Headers for any artifact body, whether it executes or not. */
|
|
53
|
+
function isolationHeaders(contentType) {
|
|
54
|
+
return {
|
|
55
|
+
"content-type": contentType,
|
|
56
|
+
"content-security-policy": CSP,
|
|
57
|
+
// Belt and braces for browsers that honour the older header, and for the
|
|
58
|
+
// case where a proxy strips CSP.
|
|
59
|
+
"x-content-type-options": "nosniff",
|
|
60
|
+
"referrer-policy": "no-referrer",
|
|
61
|
+
// This body is a session scratch surface; caching it would outlive both
|
|
62
|
+
// the session and the credential that authorised reading it.
|
|
63
|
+
"cache-control": "no-store",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The document an iframe loads.
|
|
68
|
+
*
|
|
69
|
+
* `page` artifacts are served as they were written — the model authored a
|
|
70
|
+
* complete document and wrapping it would break any `<!doctype>` or `<head>`
|
|
71
|
+
* it set up. Everything else is wrapped in a minimal shell, because a bare
|
|
72
|
+
* SVG or a bare text file rendered as a top-level document inherits the
|
|
73
|
+
* browser's defaults and looks broken next to the rest of the UI.
|
|
74
|
+
*/
|
|
75
|
+
export function artifactDocument(artifact, revision) {
|
|
76
|
+
// Checked before `page`, which a canvas also is: the difference is that the
|
|
77
|
+
// stored content is a fragment of artboards, not a document, and serving it
|
|
78
|
+
// as one would render unstyled boxes with no editor and no explanation.
|
|
79
|
+
if (isCanvas(artifact.mime)) {
|
|
80
|
+
return { body: canvasDocument(artifact, revision), contentType: "text/html; charset=utf-8" };
|
|
81
|
+
}
|
|
82
|
+
if (artifact.kind === "page") {
|
|
83
|
+
return { body: revision.content, contentType: "text/html; charset=utf-8" };
|
|
84
|
+
}
|
|
85
|
+
if (artifact.kind === "vector") {
|
|
86
|
+
// Served as its own content type rather than embedded in HTML: an SVG
|
|
87
|
+
// document scales to the frame on its own, and embedding it would mean
|
|
88
|
+
// deciding on a size here that the artifact panel is better placed to pick.
|
|
89
|
+
return { body: revision.content, contentType: "image/svg+xml; charset=utf-8" };
|
|
90
|
+
}
|
|
91
|
+
// text, markdown, code, image — shown as source in a readable shell. The
|
|
92
|
+
// panel renders these itself in the common case; this route exists so
|
|
93
|
+
// "open in a new tab" works for every kind, not just the executable ones.
|
|
94
|
+
return {
|
|
95
|
+
body: [
|
|
96
|
+
"<!doctype html><meta charset=utf-8>",
|
|
97
|
+
`<title>${escapeHtml(artifact.title)}</title>`,
|
|
98
|
+
"<style>",
|
|
99
|
+
"html{color-scheme:light dark}",
|
|
100
|
+
"body{margin:0;padding:16px;font:13px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;",
|
|
101
|
+
"white-space:pre-wrap;word-break:break-word}",
|
|
102
|
+
"</style>",
|
|
103
|
+
`<body>${escapeHtml(revision.content)}</body>`,
|
|
104
|
+
].join(""),
|
|
105
|
+
contentType: "text/html; charset=utf-8",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function sendArtifact(res, artifact, revision) {
|
|
109
|
+
const { body, contentType } = artifactDocument(artifact, revision);
|
|
110
|
+
res.writeHead(200, isolationHeaders(contentType));
|
|
111
|
+
res.end(body);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Escaping for the wrapper above.
|
|
115
|
+
*
|
|
116
|
+
* Only ever applied to the non-executable kinds. The `page` kind is
|
|
117
|
+
* deliberately *not* escaped — escaping it would defeat its purpose — which is
|
|
118
|
+
* exactly why that kind is the one the sandbox and the CSP exist for.
|
|
119
|
+
*/
|
|
120
|
+
export function escapeHtml(text) {
|
|
121
|
+
return text
|
|
122
|
+
.replaceAll("&", "&")
|
|
123
|
+
.replaceAll("<", "<")
|
|
124
|
+
.replaceAll(">", ">")
|
|
125
|
+
.replaceAll('"', """)
|
|
126
|
+
.replaceAll("'", "'");
|
|
127
|
+
}
|
|
128
|
+
export { CSP as ARTIFACT_CSP };
|
|
129
|
+
//# sourceMappingURL=artifacts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"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"}
|
package/dist/canvas.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Artifact, ArtifactVersion } from "@oharness/engine/artifacts/store.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The document a canvas artifact is served as.
|
|
4
|
+
*
|
|
5
|
+
* This is the half of a canvas the model does not write. It authors artboards;
|
|
6
|
+
* everything that makes them a canvas — the pan/zoom stage, selection, the
|
|
7
|
+
* properties panel, undo, and the save protocol — is authored here, once.
|
|
8
|
+
*
|
|
9
|
+
* That split is the whole reason a canvas is worth a mime of its own rather
|
|
10
|
+
* than being "a `page` the model happened to put an editor in":
|
|
11
|
+
*
|
|
12
|
+
* - Size. An editor written into every artifact would be tens of kilobytes
|
|
13
|
+
* of model output on every revision, re-derived and subtly different each
|
|
14
|
+
* time, and counted against MAX_ARTIFACT_BYTES.
|
|
15
|
+
* - Round-tripping. Saving means reading the artboards back out of the DOM.
|
|
16
|
+
* That only works if something knows exactly which markup it put in, which
|
|
17
|
+
* a model improvising an editor per artifact cannot promise.
|
|
18
|
+
* - Trust. The save path writes a new revision of a stored artifact. What
|
|
19
|
+
* decides the bytes that get sent should not itself be model output.
|
|
20
|
+
*
|
|
21
|
+
* The document stays inside the same CSP as any other artifact: everything
|
|
22
|
+
* here is inline, nothing is fetched, and the only channel out is postMessage
|
|
23
|
+
* to the parent frame — which no CSP directive covers, and which carries no
|
|
24
|
+
* credential. A canvas opened with no parent that speaks the protocol (a new
|
|
25
|
+
* tab, a published copy) settles into a read-only board rather than showing a
|
|
26
|
+
* Save button that silently does nothing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function canvasDocument(artifact: Artifact, revision: ArtifactVersion): string;
|