@kernhq/module-quire 0.15.0 → 0.16.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/server/export/markdown.d.ts.map +1 -1
- package/dist/server/export/markdown.js +44 -0
- package/dist/server/export/markdown.js.map +1 -1
- package/dist/server/render.d.ts +33 -2
- package/dist/server/render.d.ts.map +1 -1
- package/dist/server/render.js +173 -0
- package/dist/server/render.js.map +1 -1
- package/dist/server/services/objects.d.ts +61 -0
- package/dist/server/services/objects.d.ts.map +1 -0
- package/dist/server/services/objects.js +110 -0
- package/dist/server/services/objects.js.map +1 -0
- package/dist/server/services/unfurl.d.ts +154 -0
- package/dist/server/services/unfurl.d.ts.map +1 -0
- package/dist/server/services/unfurl.js +593 -0
- package/dist/server/services/unfurl.js.map +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { PAGE_OBJECT_REF } from '@kernhq/ui/editor/page-doc';
|
|
2
|
+
/** How many distinct objects one page may name. A document decides how much work a render does. */
|
|
3
|
+
const MAX_OBJECTS = 40;
|
|
4
|
+
/**
|
|
5
|
+
* `quire:page:0192…` in three parts, or nothing.
|
|
6
|
+
*
|
|
7
|
+
* The same pattern the editor narrows with, checked again here for the reason every attribute in
|
|
8
|
+
* this module is: what arrives is JSON out of a CRDT, which is to say a string a client picked.
|
|
9
|
+
*/
|
|
10
|
+
export function parseRef(value) {
|
|
11
|
+
if (typeof value !== 'string' || !PAGE_OBJECT_REF.test(value))
|
|
12
|
+
return null;
|
|
13
|
+
const [module, type, id] = value.split(':');
|
|
14
|
+
return { ref: value, module, type, id };
|
|
15
|
+
}
|
|
16
|
+
export function quireObjects(kernel, access) {
|
|
17
|
+
/**
|
|
18
|
+
* The module hosting a type, but only if this process hosts it.
|
|
19
|
+
*
|
|
20
|
+
* `kernel.registry` is local, which is the honest limit and the safe one: a module hosted by
|
|
21
|
+
* another service answers nothing here, so the embed draws an empty frame. Reaching across with
|
|
22
|
+
* `kernel.call` would need a procedure every module agreed to expose, and inventing one is a
|
|
23
|
+
* change to the module contract rather than to this module.
|
|
24
|
+
*/
|
|
25
|
+
function hostOf(module, type) {
|
|
26
|
+
const mod = kernel.registry.get(module);
|
|
27
|
+
if (!mod)
|
|
28
|
+
return null;
|
|
29
|
+
const resolver = mod.resolvers?.find((r) => r.type === type);
|
|
30
|
+
if (!resolver)
|
|
31
|
+
return null;
|
|
32
|
+
const declared = mod.definition.objectTypes?.find((o) => o.type === type);
|
|
33
|
+
return { resolver, label: declared?.label ?? type, icon: declared?.icon ?? null };
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a set of references for one reader, in one pass.
|
|
38
|
+
*
|
|
39
|
+
* Grouped by module and type so a page naming twelve issues is one call into tracker rather
|
|
40
|
+
* than twelve — the same arrangement `macrosIn` and `referencesIn` exist for, and for the same
|
|
41
|
+
* reason: a document must not decide how many round trips a render makes.
|
|
42
|
+
*
|
|
43
|
+
* Anything that cannot be answered is simply absent from the map, and an absent answer is the
|
|
44
|
+
* empty frame. There is no path here that returns an object nobody was asked about.
|
|
45
|
+
*/
|
|
46
|
+
async resolve(tx, workspaceId, refs, principal, href) {
|
|
47
|
+
const answers = new Map();
|
|
48
|
+
const parsed = refs
|
|
49
|
+
.map(parseRef)
|
|
50
|
+
.filter((r) => r !== null)
|
|
51
|
+
.slice(0, MAX_OBJECTS);
|
|
52
|
+
if (parsed.length === 0)
|
|
53
|
+
return answers;
|
|
54
|
+
const groups = new Map();
|
|
55
|
+
for (const ref of parsed) {
|
|
56
|
+
const key = `${ref.module}:${ref.type}`;
|
|
57
|
+
const bucket = groups.get(key);
|
|
58
|
+
if (bucket)
|
|
59
|
+
bucket.push(ref);
|
|
60
|
+
else
|
|
61
|
+
groups.set(key, [ref]);
|
|
62
|
+
}
|
|
63
|
+
for (const [key, bucket] of groups) {
|
|
64
|
+
const [module, type] = key.split(':');
|
|
65
|
+
const host = hostOf(module, type);
|
|
66
|
+
if (!host)
|
|
67
|
+
continue;
|
|
68
|
+
/*
|
|
69
|
+
* A workspace that has switched a module off has switched off everything it named. Asked
|
|
70
|
+
* here rather than left to the resolver, because a resolver reads its own tables and knows
|
|
71
|
+
* nothing about whether the customer still has the feature.
|
|
72
|
+
*/
|
|
73
|
+
const enabled = await kernel.isModuleEnabled(workspaceId, module).catch(() => false);
|
|
74
|
+
if (!enabled)
|
|
75
|
+
continue;
|
|
76
|
+
const rows = await host.resolver
|
|
77
|
+
.resolve(workspaceId, bucket.map((r) => r.id), principal, kernel)
|
|
78
|
+
.catch(() => null);
|
|
79
|
+
if (!rows)
|
|
80
|
+
continue;
|
|
81
|
+
for (const [index, row] of rows.entries()) {
|
|
82
|
+
const ref = bucket[index];
|
|
83
|
+
if (!row || !ref)
|
|
84
|
+
continue;
|
|
85
|
+
// This module's own pages get this module's own rule; see the note at the top.
|
|
86
|
+
if (module === 'quire' &&
|
|
87
|
+
type === 'page' &&
|
|
88
|
+
!(await visiblePage(tx, workspaceId, principal, row.id)))
|
|
89
|
+
continue;
|
|
90
|
+
answers.set(ref.ref, {
|
|
91
|
+
label: host.label,
|
|
92
|
+
title: row.title,
|
|
93
|
+
icon: row.icon ?? host.icon,
|
|
94
|
+
href: href?.(ref, row.url) ?? row.url,
|
|
95
|
+
subtitle: row.subtitle ?? null,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return answers;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
/** `quire.page.view` at page scope, with the ancestor chain a restriction on a section needs. */
|
|
103
|
+
async function visiblePage(tx, workspaceId, principal, pageId) {
|
|
104
|
+
const scope = await access.scopeOf(tx, workspaceId, pageId).catch(() => null);
|
|
105
|
+
if (!scope)
|
|
106
|
+
return false;
|
|
107
|
+
return access.canPage(principal, 'quire.page.view', workspaceId, scope);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=objects.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"objects.js","sourceRoot":"","sources":["../../../src/server/services/objects.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server fetching a URL somebody typed into a page.
|
|
3
|
+
*
|
|
4
|
+
* This is the most dangerous thing in the module, and it is worth saying why before anything else.
|
|
5
|
+
* Core listens on :4000 and Postgres on :5432 **on the same host as this process**, and a cloud
|
|
6
|
+
* instance sits behind a metadata service at 169.254.169.254 that hands out credentials to anyone
|
|
7
|
+
* who asks. A fetcher that will retrieve any address a user supplies is a way to make Kern read all
|
|
8
|
+
* of that on the attacker's behalf and hand back what it found — which is what "unfurl this link"
|
|
9
|
+
* would be, written the obvious way.
|
|
10
|
+
*
|
|
11
|
+
* So the defences come first and the feature second. In order, and every one of them is load-bearing:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Scheme.** `http:` and `https:` only. `file:`, `gopher:` and `data:` are not addresses on
|
|
14
|
+
* the web, and a redirect to one is a redirect out of this function's assumptions.
|
|
15
|
+
* 2. **No credentials in the URL.** `http://user:pass@host/` sends those to the host, and a URL
|
|
16
|
+
* with them in is either an accident or a way to reach something that asked for them.
|
|
17
|
+
* 3. **Resolve, then judge.** The hostname is resolved and *every* address it answers with has to
|
|
18
|
+
* be public. `evil.example` resolving to 127.0.0.1 is the ordinary way past a name-based check,
|
|
19
|
+
* and a name-based check is the only kind that can run before DNS.
|
|
20
|
+
* 4. **Then the allow-list**, on the hostname, *after* the address check rather than instead of
|
|
21
|
+
* it. Order matters: an allow-list entry must never be a way *past* the address rule, so a host
|
|
22
|
+
* an administrator allowed which today resolves into private space is still refused.
|
|
23
|
+
* 5. **The connection is pinned to the addresses that were checked.** The guard is installed as
|
|
24
|
+
* the socket's own `lookup`, so there is no second resolution between the check and the
|
|
25
|
+
* connection for a DNS rebind to win — which is why this uses `node:http` rather than `fetch`,
|
|
26
|
+
* whose dispatcher cannot be given one without pulling undici in.
|
|
27
|
+
* 6. **Every redirect is a new request and gets the whole check again.** The first response is
|
|
28
|
+
* public and the second one is the loopback interface: that is the entire trick, and it is why
|
|
29
|
+
* redirects are followed here by hand rather than by the HTTP client.
|
|
30
|
+
* 7. **A ceiling on the body, the time and the number of hops**, because a fetcher with none is a
|
|
31
|
+
* way to hold a worker open on a socket that trickles bytes for ever.
|
|
32
|
+
*
|
|
33
|
+
* **The allow-list is empty by default, and every unfurl is refused until an operator sets one.**
|
|
34
|
+
* That is deliberate rather than unfinished. Fetching arbitrary addresses on a user's say-so is a
|
|
35
|
+
* capability an instance's operator should have to grant on purpose, and a self-hosted Kern sitting
|
|
36
|
+
* inside somebody's private network is exactly the deployment where granting it by default would be
|
|
37
|
+
* worst. `QUIRE_EMBED_HOSTS` is the switch; the procedure says so when it refuses, so an
|
|
38
|
+
* administrator reads a sentence about a setting rather than "something went wrong".
|
|
39
|
+
*
|
|
40
|
+
* What this file is **not** for: Kern's own objects. A page, an issue or a channel is named by
|
|
41
|
+
* reference and resolved through `objectTypes`/`resolvers` against whoever is reading — see
|
|
42
|
+
* `objects.ts`. Unfurling our own URL would point this fetcher at core, and would freeze a
|
|
43
|
+
* permission question into a stored answer.
|
|
44
|
+
*/
|
|
45
|
+
import { type Kernel } from '@kernhq/kernel';
|
|
46
|
+
/**
|
|
47
|
+
* Why an address was not fetched.
|
|
48
|
+
*
|
|
49
|
+
* Named rather than free text, because the caller has to be able to tell an operator's problem
|
|
50
|
+
* ("nobody has allowed any host") from a writer's ("that address is not one of them") from the
|
|
51
|
+
* site's ("it never answered"), and because the tests assert on them: a refusal for the wrong
|
|
52
|
+
* reason is a check that happened to catch something the *next* URL will walk past.
|
|
53
|
+
*/
|
|
54
|
+
export declare const UNFURL_REFUSALS: readonly ["no_allowlist", "not_allowed", "private_address", "unresolvable", "bad_url", "too_many_redirects", "unreachable", "not_readable"];
|
|
55
|
+
export type UnfurlRefusal = (typeof UNFURL_REFUSALS)[number];
|
|
56
|
+
export declare class UnfurlRefused extends Error {
|
|
57
|
+
readonly refusal: UnfurlRefusal;
|
|
58
|
+
constructor(refusal: UnfurlRefusal);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Is this address one the public internet can route to?
|
|
62
|
+
*
|
|
63
|
+
* **Anything this function cannot parse is not public.** That is the direction the default has to
|
|
64
|
+
* fall: an address shape nobody here anticipated is exactly the one an attacker went looking for.
|
|
65
|
+
*/
|
|
66
|
+
export declare function isPublicAddress(ip: string): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* `QUIRE_EMBED_HOSTS`, as a list.
|
|
69
|
+
*
|
|
70
|
+
* `example.com` allows exactly that host. `.example.com` allows it and everything under it — spelled
|
|
71
|
+
* with a leading dot rather than inferred, because "allow github.com" and "allow every host anybody
|
|
72
|
+
* can create under github.io" are different decisions and an operator should have to make the second
|
|
73
|
+
* one on purpose.
|
|
74
|
+
*/
|
|
75
|
+
export declare function parseHostAllowlist(raw: string | null | undefined): string[];
|
|
76
|
+
export declare function hostAllowed(hostname: string, allowlist: readonly string[]): boolean;
|
|
77
|
+
export interface UnfurlPolicy {
|
|
78
|
+
/** Hosts an operator has allowed. Empty means every unfurl is refused. */
|
|
79
|
+
allowHosts: readonly string[];
|
|
80
|
+
/** How many redirects to follow. Each one is checked in full before it is requested. */
|
|
81
|
+
maxRedirects?: number;
|
|
82
|
+
/** The whole thing, redirects included. */
|
|
83
|
+
timeoutMs?: number;
|
|
84
|
+
/** How much of a page is read before the socket is dropped. The metadata is in the first few KB. */
|
|
85
|
+
maxBytes?: number;
|
|
86
|
+
/**
|
|
87
|
+
* How a hostname is resolved. Injectable so a test can prove the guard refuses a *name* that
|
|
88
|
+
* answers with 127.0.0.1 — which is the attack, and which cannot be arranged with real DNS.
|
|
89
|
+
*/
|
|
90
|
+
resolve?: (hostname: string) => Promise<string[]>;
|
|
91
|
+
/** One request, no redirect following. Injectable so the redirect rule can be tested off-network. */
|
|
92
|
+
hop?: (url: URL, addresses: string[], policy: Required<UnfurlPolicy>) => Promise<Hop>;
|
|
93
|
+
}
|
|
94
|
+
export interface Hop {
|
|
95
|
+
status: number;
|
|
96
|
+
location: string | null;
|
|
97
|
+
contentType: string | null;
|
|
98
|
+
body: string;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Everything that has to be true before a request is made, in the order it has to be true in.
|
|
102
|
+
*
|
|
103
|
+
* Returns the addresses, because the connection is then pinned to exactly these — checking an
|
|
104
|
+
* address and then letting the socket resolve the name again is checking a different address from
|
|
105
|
+
* the one that gets connected to.
|
|
106
|
+
*/
|
|
107
|
+
export declare function checkTarget(url: URL, policy: UnfurlPolicy): Promise<string[]>;
|
|
108
|
+
/**
|
|
109
|
+
* One HTTP request that does not follow redirects, capped in bytes and in time.
|
|
110
|
+
*
|
|
111
|
+
* `node:http` rather than `fetch`, and the reason is the `lookup` below: it is the only place a
|
|
112
|
+
* resolution can be pinned to addresses that have already been judged, and Node's `fetch` gives no
|
|
113
|
+
* way to install one without adding undici as a dependency. `agent: false` because a pooled socket
|
|
114
|
+
* is keyed on host and port and *not* on the lookup, so pooling would happily hand back a connection
|
|
115
|
+
* that was opened under someone else's guard.
|
|
116
|
+
*/
|
|
117
|
+
export declare function httpHop(url: URL, addresses: string[], policy: Required<UnfurlPolicy>): Promise<Hop>;
|
|
118
|
+
export interface UnfurlResult {
|
|
119
|
+
/** The address that actually answered, after every redirect this followed. */
|
|
120
|
+
url: string;
|
|
121
|
+
title: string | null;
|
|
122
|
+
description: string | null;
|
|
123
|
+
siteName: string | null;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* What an HTML page says about itself.
|
|
127
|
+
*
|
|
128
|
+
* Open Graph first because it is what a page puts there deliberately, `<title>` and the description
|
|
129
|
+
* meta as the fallback, and the hostname as the site of last resort — a card with an address and
|
|
130
|
+
* nothing else is still a card, and it is what the renderer draws when there is no title at all.
|
|
131
|
+
*/
|
|
132
|
+
export declare function metadataFrom(html: string, url: URL): UnfurlResult;
|
|
133
|
+
/**
|
|
134
|
+
* Fetch a URL and read what it says about itself, or refuse and say which rule refused it.
|
|
135
|
+
*
|
|
136
|
+
* The redirect loop is the part worth reading. Every hop — including the first — goes through
|
|
137
|
+
* `checkTarget` *before* it is requested, so a 302 from a public site to `http://127.0.0.1:4000` is
|
|
138
|
+
* refused at the second check, with the second request never made. That is the only arrangement
|
|
139
|
+
* that works: an HTTP client following redirects on its own has already made the request by the
|
|
140
|
+
* time anything here could look at where it went.
|
|
141
|
+
*/
|
|
142
|
+
export declare function unfurl(rawUrl: string, policy: UnfurlPolicy): Promise<UnfurlResult>;
|
|
143
|
+
export declare function quireUnfurl(kernel: Kernel): {
|
|
144
|
+
allowHosts: () => string[];
|
|
145
|
+
/**
|
|
146
|
+
* Unfurl one address, refusing with a sentence rather than a stack trace.
|
|
147
|
+
*
|
|
148
|
+
* Cached by the URL the writer typed. The cache is per process and small: it exists so that a
|
|
149
|
+
* page whose writer pastes the same link twice is one request, not so that an unfurl is free.
|
|
150
|
+
*/
|
|
151
|
+
get(rawUrl: string): Promise<UnfurlResult>;
|
|
152
|
+
};
|
|
153
|
+
export type QuireUnfurl = ReturnType<typeof quireUnfurl>;
|
|
154
|
+
//# sourceMappingURL=unfurl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unfurl.d.ts","sourceRoot":"","sources":["../../../src/server/services/unfurl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAMH,OAAO,EAAa,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAYvD;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,6IASlB,CAAA;AACV,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;AAE5D,qBAAa,aAAc,SAAQ,KAAK;IAC1B,QAAQ,CAAC,OAAO,EAAE,aAAa;gBAAtB,OAAO,EAAE,aAAa;CAI5C;AAgHD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAOnD;AAMD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,CAK3E;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CASnF;AAMD,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;IAC7B,wFAAwF;IACxF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oGAAoG;IACpG,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IACjD,qGAAqG;IACrG,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,YAAY,CAAC,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;CACtF;AAED,MAAM,WAAW,GAAG;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,IAAI,EAAE,MAAM,CAAA;CACb;AAcD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAmBnF;AAMD;;;;;;;;GAQG;AACH,wBAAsB,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAsFzG;AAmED,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,YAAY,CAgBjE;AAMD;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA0CxF;AAkCD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM;;IAYtC;;;;;OAKG;gBACe,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;EAwBnD;AAED,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAA"}
|