@_linked/server 3.12.1 → 3.13.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/CHANGELOG.md +39 -0
- package/lib/esm/shapes/LinkedServer.d.ts +13 -6
- package/lib/esm/shapes/LinkedServer.js +47 -27
- package/lib/esm/shapes/LinkedServer.js.map +1 -1
- package/lib/esm/shapes/quadstores/RemoteDataset.d.ts +17 -14
- package/lib/esm/shapes/quadstores/RemoteDataset.js +57 -20
- package/lib/esm/shapes/quadstores/RemoteDataset.js.map +1 -1
- package/lib/esm/utils/adminChannel.d.ts +7 -6
- package/lib/esm/utils/adminChannel.js +2 -2
- package/lib/esm/utils/adminChannel.js.map +1 -1
- package/lib/esm/utils/graphTargets.d.ts +17 -9
- package/lib/esm/utils/graphTargets.js +34 -20
- package/lib/esm/utils/graphTargets.js.map +1 -1
- package/lib/esm/utils/linkedDependencies.d.ts +53 -7
- package/lib/esm/utils/linkedDependencies.js +130 -12
- package/lib/esm/utils/linkedDependencies.js.map +1 -1
- package/lib/esm/utils/ontologySync.d.ts +19 -12
- package/lib/esm/utils/ontologySync.js +45 -15
- package/lib/esm/utils/ontologySync.js.map +1 -1
- package/lib/esm/utils/syncShapes.d.ts +5 -0
- package/lib/esm/utils/syncShapes.js +17 -5
- package/lib/esm/utils/syncShapes.js.map +1 -1
- package/package.json +4 -4
- package/readme.md +24 -14
|
@@ -25,24 +25,27 @@ export interface RemoteDatasetConfig {
|
|
|
25
25
|
/** Or pass a fully-qualified URI directly. */
|
|
26
26
|
id?: string;
|
|
27
27
|
/**
|
|
28
|
-
* In the url form, send the signed-in user's
|
|
29
|
-
*
|
|
30
|
-
* string `'false'`, as an interpolated config gives it) turns it off
|
|
28
|
+
* In the url form, send the signed-in user's own access token with each request made
|
|
29
|
+
* while their HTTP request is answered (see {@link USER_TOKEN_HEADER}). Default true;
|
|
30
|
+
* `false` (or the string `'false'`, as an interpolated config gives it) turns it off: do so
|
|
31
|
+
* for a data server you do not trust with your users' tokens.
|
|
31
32
|
*/
|
|
32
|
-
|
|
33
|
+
forwardUserToken?: boolean | string;
|
|
33
34
|
}
|
|
34
35
|
/**
|
|
35
|
-
* The header the url form sends
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* The header the url form sends the signed-in user's Solid-OIDC access token in, with a
|
|
37
|
+
* `DPoP` proof for that request beside it. Both come from {@link USER_TOKEN_HEADERS_HOOK};
|
|
38
|
+
* the data server verifies them and acts as that user. The bearer token still decides which
|
|
39
|
+
* data the store reaches.
|
|
38
40
|
*/
|
|
39
|
-
export declare const
|
|
41
|
+
export declare const USER_TOKEN_HEADER = "x-linked-user-token";
|
|
40
42
|
/**
|
|
41
|
-
* The
|
|
42
|
-
* `request
|
|
43
|
-
*
|
|
43
|
+
* The `globalThis` key of the hook that answers the user token headers for a request:
|
|
44
|
+
* `(request, {method, url}) => Promise<{'x-linked-user-token': string; DPoP: string} | undefined>`.
|
|
45
|
+
* `@_linked/auth`'s backend installs it; read here instead of importing auth, so this store
|
|
46
|
+
* stays browser-safe and free of auth.
|
|
44
47
|
*/
|
|
45
|
-
export declare
|
|
48
|
+
export declare const USER_TOKEN_HEADERS_HOOK: unique symbol;
|
|
46
49
|
/**
|
|
47
50
|
* A failed call to the data server at `url`. `status` is the HTTP status (0 when no HTTP
|
|
48
51
|
* answer came back); `code` is the error body's `code` when the server sent one.
|
|
@@ -101,8 +104,8 @@ export declare class RemoteDataset implements IDataset {
|
|
|
101
104
|
/** The data server base url, trailing slashes trimmed; undefined in the no-url form. */
|
|
102
105
|
readonly url?: string;
|
|
103
106
|
private readonly token?;
|
|
104
|
-
/** Whether the url form sends {@link
|
|
105
|
-
readonly
|
|
107
|
+
/** Whether the url form sends {@link USER_TOKEN_HEADER}; see `RemoteDatasetConfig.forwardUserToken`. */
|
|
108
|
+
readonly forwardUserToken: boolean;
|
|
106
109
|
private initialized?;
|
|
107
110
|
private pingInfo?;
|
|
108
111
|
private readonly instanceRegistryKey;
|
|
@@ -6,11 +6,19 @@ import { invalidateGraphRegistry, noteGraphQuery, readGraphRegistry, } from '@_l
|
|
|
6
6
|
import { graphs } from '@_linked/core/ontologies/graphs';
|
|
7
7
|
import { OntologyGraph, ShapesGraph } from '@_linked/core/shapes/NamedGraph';
|
|
8
8
|
/**
|
|
9
|
-
* The header the url form sends
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The header the url form sends the signed-in user's Solid-OIDC access token in, with a
|
|
10
|
+
* `DPoP` proof for that request beside it. Both come from {@link USER_TOKEN_HEADERS_HOOK};
|
|
11
|
+
* the data server verifies them and acts as that user. The bearer token still decides which
|
|
12
|
+
* data the store reaches.
|
|
12
13
|
*/
|
|
13
|
-
export const
|
|
14
|
+
export const USER_TOKEN_HEADER = 'x-linked-user-token';
|
|
15
|
+
/**
|
|
16
|
+
* The `globalThis` key of the hook that answers the user token headers for a request:
|
|
17
|
+
* `(request, {method, url}) => Promise<{'x-linked-user-token': string; DPoP: string} | undefined>`.
|
|
18
|
+
* `@_linked/auth`'s backend installs it; read here instead of importing auth, so this store
|
|
19
|
+
* stays browser-safe and free of auth.
|
|
20
|
+
*/
|
|
21
|
+
export const USER_TOKEN_HEADERS_HOOK = Symbol.for('@_linked/auth:userTokenHeaders');
|
|
14
22
|
// The store `@_linked/server-utils/utils/CallContext` keeps the call context in, shared on
|
|
15
23
|
// `globalThis` under this key. Read here instead of importing that module: it imports
|
|
16
24
|
// `node:async_hooks`, and this store also runs in the browser (the no-url form). In a browser,
|
|
@@ -20,18 +28,43 @@ function currentCallContext() {
|
|
|
20
28
|
const store = globalThis[CALL_CONTEXT_KEY];
|
|
21
29
|
return typeof (store === null || store === void 0 ? void 0 : store.getStore) === 'function' ? store.getStore() : undefined;
|
|
22
30
|
}
|
|
31
|
+
let warnedUserTokenHook = false;
|
|
32
|
+
const warnUserTokenHook = (problem) => {
|
|
33
|
+
if (warnedUserTokenHook)
|
|
34
|
+
return;
|
|
35
|
+
warnedUserTokenHook = true;
|
|
36
|
+
console.warn(`RemoteDataset: the user token hook ${problem}; queries go without the user's token`);
|
|
37
|
+
};
|
|
23
38
|
/**
|
|
24
|
-
* The
|
|
25
|
-
*
|
|
26
|
-
*
|
|
39
|
+
* The user token headers for a request to `url`, made in the current call: the hook's
|
|
40
|
+
* answer for the http context's request. Undefined in a system context (`runAsSystem`),
|
|
41
|
+
* outside any call, without the hook, and when it answers nothing. A hook that throws or
|
|
42
|
+
* answers something else than two non-empty strings sends nothing (one warning per process).
|
|
27
43
|
*/
|
|
28
|
-
|
|
29
|
-
var _a, _b, _c, _d;
|
|
44
|
+
async function userTokenHeadersFor(url) {
|
|
30
45
|
const ctx = currentCallContext();
|
|
31
|
-
if (!ctx || ctx.kind !== 'http')
|
|
46
|
+
if (!ctx || ctx.kind !== 'http' || !ctx.request)
|
|
47
|
+
return undefined;
|
|
48
|
+
const hook = globalThis[USER_TOKEN_HEADERS_HOOK];
|
|
49
|
+
if (typeof hook !== 'function')
|
|
50
|
+
return undefined;
|
|
51
|
+
let answer;
|
|
52
|
+
try {
|
|
53
|
+
answer = await hook(ctx.request, { method: 'POST', url });
|
|
54
|
+
}
|
|
55
|
+
catch (_a) {
|
|
56
|
+
warnUserTokenHook('failed');
|
|
32
57
|
return undefined;
|
|
33
|
-
|
|
34
|
-
|
|
58
|
+
}
|
|
59
|
+
if (answer === undefined || answer === null)
|
|
60
|
+
return undefined;
|
|
61
|
+
const token = answer[USER_TOKEN_HEADER];
|
|
62
|
+
const proof = answer.DPoP;
|
|
63
|
+
if (typeof token !== 'string' || !token || typeof proof !== 'string' || !proof) {
|
|
64
|
+
warnUserTokenHook('answered something else than a token and a proof');
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return { [USER_TOKEN_HEADER]: token, DPoP: proof };
|
|
35
68
|
}
|
|
36
69
|
/**
|
|
37
70
|
* A failed call to the data server at `url`. `status` is the HTTP status (0 when no HTTP
|
|
@@ -125,8 +158,8 @@ const PING_UNREACHABLE_STATUSES = new Set([404, 501, 502, 503, 504]);
|
|
|
125
158
|
*/
|
|
126
159
|
export class RemoteDataset {
|
|
127
160
|
constructor(config) {
|
|
128
|
-
/** Whether the url form sends {@link
|
|
129
|
-
this.
|
|
161
|
+
/** Whether the url form sends {@link USER_TOKEN_HEADER}; see `RemoteDatasetConfig.forwardUserToken`. */
|
|
162
|
+
this.forwardUserToken = true;
|
|
130
163
|
this.instanceRegistryKey = `remote-dataset:${nextRegistryKey++}`;
|
|
131
164
|
this.warnedRegistryRead = false;
|
|
132
165
|
if (!config)
|
|
@@ -141,8 +174,8 @@ export class RemoteDataset {
|
|
|
141
174
|
if (token)
|
|
142
175
|
this.token = token;
|
|
143
176
|
}
|
|
144
|
-
if (config.
|
|
145
|
-
this.
|
|
177
|
+
if (config.forwardUserToken === false || config.forwardUserToken === 'false') {
|
|
178
|
+
this.forwardUserToken = false;
|
|
146
179
|
}
|
|
147
180
|
if (config.id) {
|
|
148
181
|
this.id = config.id;
|
|
@@ -340,10 +373,11 @@ export class RemoteDataset {
|
|
|
340
373
|
};
|
|
341
374
|
if (this.token)
|
|
342
375
|
headers.Authorization = `Bearer ${this.token}`;
|
|
343
|
-
if (this.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
376
|
+
if (this.forwardUserToken) {
|
|
377
|
+
// a fresh proof for every request: never cached (each has its own `jti`)
|
|
378
|
+
const userHeaders = await userTokenHeadersFor(target);
|
|
379
|
+
if (userHeaders)
|
|
380
|
+
Object.assign(headers, userHeaders);
|
|
347
381
|
}
|
|
348
382
|
let response;
|
|
349
383
|
try {
|
|
@@ -351,6 +385,9 @@ export class RemoteDataset {
|
|
|
351
385
|
method: 'POST',
|
|
352
386
|
headers,
|
|
353
387
|
body: JSON.stringify({ args }),
|
|
388
|
+
// A redirect is an answer, never followed: the bearer and the user's token and proof
|
|
389
|
+
// go to the data server at `url` and nowhere else.
|
|
390
|
+
redirect: 'manual',
|
|
354
391
|
});
|
|
355
392
|
}
|
|
356
393
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RemoteDataset.js","sourceRoot":"","sources":["../../../../src/shapes/quadstores/RemoteDataset.ts"],"names":[],"mappings":"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","sourcesContent":["import { packageName } from '../../package.js';\nimport { brandLinkedError } from '@_linked/core/utils/linkedError';\nimport { Server } from '@_linked/server-utils/utils/Server';\nimport type { CallContext } from '@_linked/server-utils/utils/CallContext';\nimport type { IDataset } from '@_linked/core/interfaces/IDataset';\nimport type { SelectQuery } from '@_linked/core/queries/SelectQuery';\nimport type { CountQuery } from '@_linked/core/queries/CountQuery';\nimport type { AskQuery } from '@_linked/core/queries/AskQuery';\nimport type { UpdateQuery } from '@_linked/core/queries/UpdateQuery';\nimport type { CreateQuery } from '@_linked/core/queries/CreateQuery';\nimport type {\n DeleteQuery,\n DeleteResponse,\n} from '@_linked/core/queries/DeleteQuery';\nimport type {\n SelectResult,\n UpdateResult,\n CreateResult,\n GraphQuery,\n GraphResult,\n} from '@_linked/core/queries/IntermediateRepresentation';\nimport {\n GraphQueryError,\n isGraphQueryError,\n unsupportedGraphQuery,\n type GraphQueryErrorCode,\n} from '@_linked/core/queries/graphQuery';\nimport {\n invalidateGraphRegistry,\n noteGraphQuery,\n readGraphRegistry,\n type GraphRegistryRow,\n} from '@_linked/core/utils/graphRegistryCache';\nimport { graphs } from '@_linked/core/ontologies/graphs';\nimport { OntologyGraph, ShapesGraph } from '@_linked/core/shapes/NamedGraph';\n\n/**\n * Constructor argument for `new RemoteDataset(config)`: the `config` object of a\n * `linked.{frontend,backend}.datasets.json` entry, passed verbatim.\n */\nexport interface RemoteDatasetConfig {\n /**\n * Base URL of the data server, e.g. `http://localhost:4040/gateway`; routes are appended\n * to it. Without it, queries go to this process's own backend through `Server.call`.\n * Present but empty is an error, never the no-url form.\n */\n url?: string;\n /** Sent as `Authorization: Bearer <token>` when set and `url` is set. */\n token?: string;\n /** Stable name; becomes the suffix of the store URI: ${DATA_ROOT}/backend-api-store/<name>. */\n name?: string;\n /** Or pass a fully-qualified URI directly. */\n id?: string;\n /**\n * In the url form, send the signed-in user's WebID of the current call as\n * `x-linked-end-user` (see {@link END_USER_HEADER}). Default true; `false` (or the\n * string `'false'`, as an interpolated config gives it) turns it off.\n */\n forwardEndUser?: boolean | string;\n}\n\n/**\n * The header the url form sends with a query made while a signed-in user's HTTP request is\n * being answered: that user's WebID. The data server decides what to do with it; the bearer\n * token still decides which data the store reaches.\n */\nexport const END_USER_HEADER = 'x-linked-end-user';\n\n// The store `@_linked/server-utils/utils/CallContext` keeps the call context in, shared on\n// `globalThis` under this key. Read here instead of importing that module: it imports\n// `node:async_hooks`, and this store also runs in the browser (the no-url form). In a browser,\n// and in a server before any call ran, the key is unset and there is no context.\nconst CALL_CONTEXT_KEY = Symbol.for('@_linked/server-utils:callContext');\n\nfunction currentCallContext(): CallContext | undefined {\n const store = (globalThis as Record<symbol, { getStore?: () => unknown } | undefined>)[CALL_CONTEXT_KEY];\n return typeof store?.getStore === 'function' ? (store.getStore() as CallContext | undefined) : undefined;\n}\n\n/**\n * The WebID of the signed-in user behind the current call: the http context's\n * `request.linkedAuth.userAccount.accountOf.id`. Undefined in a system context\n * (`runAsSystem`), outside any call, and for a request without a session.\n */\nexport function currentEndUserWebId(): string | undefined {\n const ctx = currentCallContext();\n if (!ctx || ctx.kind !== 'http') return undefined;\n const webId = ctx.request?.linkedAuth?.userAccount?.accountOf?.id;\n return typeof webId === 'string' && webId ? webId : undefined;\n}\n\n/**\n * A failed call to the data server at `url`. `status` is the HTTP status (0 when no HTTP\n * answer came back); `code` is the error body's `code` when the server sent one.\n */\nexport class RemoteDatasetError extends Error {\n /** The data server base url ('' when the configured url is empty). */\n readonly url: string;\n /** The HTTP status; 0 = no HTTP answer. */\n readonly status: number;\n /** The error body's `code`, when the server sent one. */\n readonly code?: string;\n\n constructor(message: string, url: string, status: number, code?: string) {\n super(message);\n this.name = new.target.name;\n this.url = url;\n this.status = status;\n if (code !== undefined) this.code = code;\n }\n}\n\n/**\n * Nothing at `url` speaks the query wire: no HTTP answer, an empty url, or (on `ping`) an\n * answer that is not the server's. A boot that gets this should stop.\n */\nexport class RemoteDatasetUnreachableError extends RemoteDatasetError {}\n\n/** The data server answered 401 or 403: it refused this dataset's token. */\nexport class RemoteDatasetUnauthorizedError extends RemoteDatasetError {}\n\n// Branded, so `isLinkedError(err, RemoteDatasetUnreachableError)` from `@_linked/core` matches an\n// error thrown by another copy of this module, where `instanceof` does not. See\n// https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md.\nbrandLinkedError(RemoteDatasetError, '@_linked/server', 'RemoteDatasetError');\nbrandLinkedError(RemoteDatasetUnreachableError, '@_linked/server', 'RemoteDatasetUnreachableError');\nbrandLinkedError(RemoteDatasetUnauthorizedError, '@_linked/server', 'RemoteDatasetUnauthorizedError');\n\n/**\n * What the data server's `ping` answered, as is. The generic `@_linked/server` ping answers\n * `{ok: true}`; a gateway may add fields that say which dataset it serves, such as\n * `{ok, projectId, branchName, dataset, forked}`. A body that is not a JSON object reads as `{}`.\n */\nexport type RemoteDatasetPingInfo = Readonly<Record<string, unknown>>;\n\ntype Method =\n | 'ping'\n | 'selectQuery'\n | 'askQuery'\n | 'updateQuery'\n | 'createQuery'\n | 'deleteQuery'\n | 'graphQuery';\n\n/** The refusal codes a data server sends back for a graph query (`{error, code}`). */\nconst GRAPH_REFUSAL_CODES: ReadonlySet<string> = new Set<GraphQueryErrorCode>([\n 'unsupported-query-kind',\n 'graph-not-registered',\n 'graph-name-read-only',\n 'graph-exists',\n]);\n\n/**\n * A failed graph query as the caller expects it: a {@link GraphQueryError} when the data server\n * refused it with one of the graph refusal codes, or when it has no graph method at all (404 or\n * 501 without any code: an older server or gateway), which reads as `unsupported-query-kind`\n * so the ontology sync falls back to the default graph. Anything else is passed on as is.\n */\nfunction graphQueryFailure(err: unknown, query: GraphQuery): unknown {\n if (isGraphQueryError(err)) return err;\n const code = (err as { code?: unknown } | null)?.code;\n const message = err instanceof Error ? err.message : String(err);\n let refusal: GraphQueryError | undefined;\n if (typeof code === 'string' && GRAPH_REFUSAL_CODES.has(code)) {\n refusal = new GraphQueryError(code as GraphQueryErrorCode, message, query.graph);\n } else if (code === undefined) {\n const status = (err as { status?: unknown } | null)?.status;\n if (status === 404 || status === 501) refusal = unsupportedGraphQuery(query, message);\n }\n if (!refusal) return err;\n (refusal as { cause?: unknown }).cause = err;\n return refusal;\n}\n\n/** The portable names a remote store expands from its own registry cache, with their record class. */\nconst REGISTRY_EXPANDED: ReadonlyMap<string, string> = new Map([\n [graphs.allOntologies, OntologyGraph.targetClass.id],\n [graphs.allShapes, ShapesGraph.targetClass.id],\n]);\n\nlet nextRegistryKey = 0;\n\n/** On `ping`, these statuses mean nothing at that url serves the query wire. */\nconst PING_UNREACHABLE_STATUSES = new Set([404, 501, 502, 503, 504]);\n\n/**\n * An {@link IDataset} whose queries are answered by a linked server.\n *\n * - **Without `url`** the server is this app's own backend: every query goes through\n * `Server.call`'s package form (`/call/@_linked/server/<method>` with a body of `{args}`),\n * and on a backend `Server`'s local shortcut answers it in process. This is the store a\n * browser uses to talk to its backend.\n * - **With `url`** the server is a remote data server: every query posts\n * `{\"args\": [query.toJSON()]}` to `{url}/call/@_linked/server/<method>` with the bearer\n * `token`, through `fetch`, never through `Server` (whose local shortcut would answer the\n * app's own provider) and never retried (a retried write could apply twice). `init()` asks\n * the server's `ping` and throws {@link RemoteDatasetUnreachableError} or\n * {@link RemoteDatasetUnauthorizedError} when it cannot be used.\n *\n * A store is not a Shape: it addresses the backend by package name, so nothing about the\n * store instance crosses the wire. Query routing is decided by the query's shape.\n *\n * `LinkedStorage.setDefaultDataset` calls `init()` without awaiting it, so a boot that must\n * fail loudly awaits `init()` itself first; a successful `init()` is memoized, and resolves\n * the ping body (also readable as `info`), so a boot can log which dataset the server serves.\n */\nexport class RemoteDataset implements IDataset {\n /**\n * Kept so the config form is still accepted and a store can be named; it is not an\n * addressing mechanism, nothing transmits or resolves it.\n */\n readonly id?: string;\n /** The data server base url, trailing slashes trimmed; undefined in the no-url form. */\n readonly url?: string;\n private readonly token?: string;\n /** Whether the url form sends {@link END_USER_HEADER}; see `RemoteDatasetConfig.forwardEndUser`. */\n readonly forwardEndUser: boolean = true;\n private initialized?: Promise<RemoteDatasetPingInfo | undefined>;\n private pingInfo?: RemoteDatasetPingInfo;\n private readonly instanceRegistryKey = `remote-dataset:${nextRegistryKey++}`;\n private warnedRegistryRead = false;\n\n constructor(config?: RemoteDatasetConfig | string) {\n if (!config) return;\n if (typeof config === 'string') {\n this.id = `${process.env.DATA_ROOT}/backend-api-store/${config}`;\n return;\n }\n if (typeof config.url === 'string') {\n this.url = config.url.trim().replace(/\\/+$/, '');\n const token = typeof config.token === 'string' ? config.token.trim() : '';\n if (token) this.token = token;\n }\n if (config.forwardEndUser === false || config.forwardEndUser === 'false') {\n this.forwardEndUser = false;\n }\n if (config.id) {\n this.id = config.id;\n return;\n }\n if (config.name) {\n this.id = `${process.env.DATA_ROOT}/backend-api-store/${config.name}`;\n }\n }\n\n /**\n * The body of the last successful `ping` (see {@link init}); undefined before one, and\n * always in the no-url form.\n */\n get info(): RemoteDatasetPingInfo | undefined {\n return this.pingInfo;\n }\n\n /**\n * In the url form, asks the server's `ping` and resolves its body (also kept as\n * {@link info}); resolves undefined in the no-url form.\n */\n init(): Promise<RemoteDatasetPingInfo | undefined> {\n // No initialization needed when queries are routed to the app's own backend.\n if (this.url === undefined) return Promise.resolve(undefined);\n if (!this.initialized) {\n this.initialized = this.post('ping', []).then(\n (body) => {\n const info: RemoteDatasetPingInfo = Object.freeze(\n body && typeof body === 'object' && !Array.isArray(body)\n ? { ...(body as Record<string, unknown>) }\n : {},\n );\n this.pingInfo = info;\n return info;\n },\n (err) => {\n // Not memoized: the next init() asks again.\n this.initialized = undefined;\n throw err;\n },\n );\n }\n return this.initialized;\n }\n\n // Queries serialize to DSL-JSON for the wire: the live (closed) query can't cross a call\n // as-is, so we ship `toJSON()` and the server's generic provider rehydrates it with\n // `fromJSON()`.\n async selectQuery(query: SelectQuery | CountQuery): Promise<SelectResult | number> {\n return this.call('selectQuery', await this.withReadGraphs(query.toJSON()));\n }\n\n async askQuery(query: AskQuery): Promise<boolean> {\n return this.call('askQuery', await this.withReadGraphs(query.toJSON()));\n }\n\n updateQuery(query: UpdateQuery): Promise<UpdateResult> {\n return this.call('updateQuery', query.toJSON());\n }\n\n createQuery(query: CreateQuery): Promise<CreateResult> {\n return this.call('createQuery', query.toJSON());\n }\n\n deleteQuery(query: DeleteQuery): Promise<DeleteResponse> {\n return this.call('deleteQuery', query.toJSON());\n }\n\n /**\n * A whole-graph operation (`graph_create`, `graph_replace`, `graph_patch`, `graph_drop`): the\n * query is plain data, so it is sent as is, the same way and with the same headers as every\n * other query. On success this store's registry cache notes it in the same step (a create adds\n * the graph, a drop removes it), so its next `.from(graphs.allOntologies)` read sees the change.\n * A refusal rejects with a `GraphQueryError` (see `graphQueryFailure`): a server or gateway\n * without the method answers `unsupported-query-kind`.\n */\n async graphQuery(query: GraphQuery): Promise<GraphResult> {\n let result: GraphResult;\n try {\n result = await this.call<GraphResult>('graphQuery', query);\n } catch (err) {\n throw graphQueryFailure(err, query);\n }\n if (this.url !== undefined) noteGraphQuery(this.graphRegistryKey(), query);\n return result;\n }\n\n /**\n * The key this store's graph-registry cache is kept under: the data server's url and the\n * dataset it named in its `ping` answer (a gateway answers `{dataset}`), so stores of the\n * same dataset share one entry; else one key per instance. (`POST /admin/graph-registry-changed`\n * drops every entry of the process, whatever its key.)\n */\n protected graphRegistryKey(): string {\n const dataset = this.pingInfo?.dataset;\n return typeof dataset === 'string' && dataset && this.url\n ? `${this.url}#${dataset}`\n : this.instanceRegistryKey;\n }\n\n /**\n * Drop the cached graph registry of `dataset` (default: this store's, see `graphRegistryKey`):\n * the next read that expands `graphs.allOntologies` re-reads it.\n */\n invalidateGraphRegistry(dataset?: string): void {\n invalidateGraphRegistry(dataset ?? this.graphRegistryKey());\n }\n\n /**\n * Read the data server's graph registry: the records of the ontology and shapes graphs, by\n * class, through the same query wire (the record shapes read the registry graph themselves).\n */\n protected async loadGraphRegistry(): Promise<GraphRegistryRow[]> {\n const rows: GraphRegistryRow[] = [];\n const kinds = [OntologyGraph, ShapesGraph] as const;\n const results = await Promise.all(kinds.map((kind) => kind.select().exec(this)));\n results.forEach((records, i) => {\n const recordClass = kinds[i].targetClass.id;\n for (const record of (records as Array<{ id?: unknown }> | null) ?? []) {\n if (typeof record?.id === 'string' && record.id) rows.push({ graph: record.id, recordClass });\n }\n });\n return rows;\n }\n\n /**\n * In the url form, expand `graphs.allOntologies` and `graphs.allShapes` in a read's `graphs`\n * from this store's registry cache (per dataset, updated by its own graph queries, dropped by a\n * pushed invalidation, re-read after 30 seconds), so the data server reads exactly the graphs\n * this process knows of. Other names (graph IRIs, `graphs.default`, `graphs.union`) go as they\n * are, for the data server's store to resolve. A name that expands to no graph is sent as is,\n * so the data server answers from its own registry; so is every name when the registry cannot\n * be read (the data server refused or failed the record selects), with one warning per store.\n * The no-url form sends `graphs` unchanged: the backend's store expands them.\n */\n private async withReadGraphs(json: unknown): Promise<unknown> {\n if (this.url === undefined || !json || typeof json !== 'object') return json;\n const names = (json as { graphs?: unknown }).graphs;\n if (!Array.isArray(names) || !names.some((name) => REGISTRY_EXPANDED.has(name))) return json;\n let registry: Awaited<ReturnType<typeof readGraphRegistry>>;\n try {\n registry = await readGraphRegistry(this.graphRegistryKey(), () => this.loadGraphRegistry());\n } catch (err) {\n if (!this.warnedRegistryRead) {\n this.warnedRegistryRead = true;\n console.warn(\n `RemoteDataset: could not read the graph registry at ${this.url}; reads send graph names unexpanded:`,\n err instanceof Error ? err.message : err,\n );\n }\n return json;\n }\n const out: string[] = [];\n const add = (iri: string) => {\n if (!out.includes(iri)) out.push(iri);\n };\n for (const name of names as string[]) {\n const recordClass = REGISTRY_EXPANDED.get(name);\n if (!recordClass) {\n add(name);\n continue;\n }\n const listed = [...registry].filter(([, classes]) => classes.has(recordClass)).map(([graph]) => graph);\n if (listed.length) listed.sort().forEach(add);\n else add(name);\n }\n return { ...(json as Record<string, unknown>), graphs: out };\n }\n\n private call<T>(method: Method, json: unknown): Promise<T> {\n if (this.url === undefined) return this.callBackend(method, json);\n return this.post(method, [json]) as Promise<T>;\n }\n\n /**\n * Opts in to `rejectOnError`, so a failed call (HTTP error status, or no\n * provider on the backend) rejects with a `ServerCallError` carrying the\n * status and the server's message instead of reading as an empty result.\n * Whatever a successful call returns, `undefined` included, resolves as is.\n */\n private callBackend<T>(method: string, json: unknown): Promise<T> {\n // The PACKAGE form, not the shape form. It posts to\n // `/call/<packageName>/<method>` with a body of `{args}` only — no `shapeURI`,\n // no `instanceNode` — and the backend answers from this package's generic\n // provider without resolving or instantiating any Shape.\n return Server.call(packageName, { method, rejectOnError: true }, json);\n }\n\n /**\n * The url form: one POST to `{url}/call/@_linked/server/<method>`. Resolves the parsed\n * body (`null` is a valid result: a method that returned `undefined`). Messages name the\n * url and the method, never the token.\n */\n private async post(method: Method, args: unknown[]): Promise<unknown> {\n const base = this.url ?? '';\n if (!base) {\n throw new RemoteDatasetUnreachableError(\n `RemoteDataset: no data server url is set (the configured url is empty); cannot ${method}`,\n '',\n 0,\n );\n }\n const target = `${base}/call/${packageName}/${method}`;\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n if (this.token) headers.Authorization = `Bearer ${this.token}`;\n if (this.forwardEndUser) {\n const endUser = currentEndUserWebId();\n if (endUser) headers[END_USER_HEADER] = endUser;\n }\n\n let response: Response;\n try {\n response = await globalThis.fetch(target, {\n method: 'POST',\n headers,\n body: JSON.stringify({ args }),\n });\n } catch (err) {\n const cause = err instanceof Error ? err.message : String(err);\n throw new RemoteDatasetUnreachableError(\n `RemoteDataset: cannot reach the data server at ${base} (${method}): ${cause}`,\n base,\n 0,\n );\n }\n\n const text = await response.text();\n let body: unknown = undefined;\n let isJson = false;\n if (text) {\n try {\n body = JSON.parse(text);\n isJson = true;\n } catch {\n body = text;\n }\n }\n\n if (response.ok) {\n if (isJson) return body;\n if (method === 'ping') {\n throw new RemoteDatasetUnreachableError(\n `RemoteDataset: the data server at ${base} answered ping with a body that is not JSON`,\n base,\n response.status,\n );\n }\n if (!text) return undefined;\n throw new RemoteDatasetError(\n `RemoteDataset: the data server at ${base} answered ${method} with a body that is not JSON`,\n base,\n response.status,\n );\n }\n\n const record = body && typeof body === 'object' ? (body as Record<string, unknown>) : undefined;\n const code = typeof record?.code === 'string' ? record.code : undefined;\n const detail =\n typeof record?.error === 'string'\n ? record.error\n : typeof body === 'string' && body\n ? body\n : response.statusText;\n const message = `RemoteDataset: the data server at ${base} answered ${method} with ${response.status}${\n code ? ` (${code})` : ''\n }${detail ? `: ${detail}` : ''}`;\n\n if (response.status === 401 || response.status === 403) {\n throw new RemoteDatasetUnauthorizedError(message, base, response.status, code);\n }\n if (method === 'ping' && (PING_UNREACHABLE_STATUSES.has(response.status) || !isJson)) {\n throw new RemoteDatasetUnreachableError(message, base, response.status, code);\n }\n throw new RemoteDatasetError(message, base, response.status, code);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"RemoteDataset.js","sourceRoot":"","sources":["../../../../src/shapes/quadstores/RemoteDataset.ts"],"names":[],"mappings":"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","sourcesContent":["import { packageName } from '../../package.js';\nimport { brandLinkedError } from '@_linked/core/utils/linkedError';\nimport { Server } from '@_linked/server-utils/utils/Server';\nimport type { CallContext } from '@_linked/server-utils/utils/CallContext';\nimport type { IDataset } from '@_linked/core/interfaces/IDataset';\nimport type { SelectQuery } from '@_linked/core/queries/SelectQuery';\nimport type { CountQuery } from '@_linked/core/queries/CountQuery';\nimport type { AskQuery } from '@_linked/core/queries/AskQuery';\nimport type { UpdateQuery } from '@_linked/core/queries/UpdateQuery';\nimport type { CreateQuery } from '@_linked/core/queries/CreateQuery';\nimport type {\n DeleteQuery,\n DeleteResponse,\n} from '@_linked/core/queries/DeleteQuery';\nimport type {\n SelectResult,\n UpdateResult,\n CreateResult,\n GraphQuery,\n GraphResult,\n} from '@_linked/core/queries/IntermediateRepresentation';\nimport {\n GraphQueryError,\n isGraphQueryError,\n unsupportedGraphQuery,\n type GraphQueryErrorCode,\n} from '@_linked/core/queries/graphQuery';\nimport {\n invalidateGraphRegistry,\n noteGraphQuery,\n readGraphRegistry,\n type GraphRegistryRow,\n} from '@_linked/core/utils/graphRegistryCache';\nimport { graphs } from '@_linked/core/ontologies/graphs';\nimport { OntologyGraph, ShapesGraph } from '@_linked/core/shapes/NamedGraph';\n\n/**\n * Constructor argument for `new RemoteDataset(config)`: the `config` object of a\n * `linked.{frontend,backend}.datasets.json` entry, passed verbatim.\n */\nexport interface RemoteDatasetConfig {\n /**\n * Base URL of the data server, e.g. `http://localhost:4040/gateway`; routes are appended\n * to it. Without it, queries go to this process's own backend through `Server.call`.\n * Present but empty is an error, never the no-url form.\n */\n url?: string;\n /** Sent as `Authorization: Bearer <token>` when set and `url` is set. */\n token?: string;\n /** Stable name; becomes the suffix of the store URI: ${DATA_ROOT}/backend-api-store/<name>. */\n name?: string;\n /** Or pass a fully-qualified URI directly. */\n id?: string;\n /**\n * In the url form, send the signed-in user's own access token with each request made\n * while their HTTP request is answered (see {@link USER_TOKEN_HEADER}). Default true;\n * `false` (or the string `'false'`, as an interpolated config gives it) turns it off: do so\n * for a data server you do not trust with your users' tokens.\n */\n forwardUserToken?: boolean | string;\n}\n\n/**\n * The header the url form sends the signed-in user's Solid-OIDC access token in, with a\n * `DPoP` proof for that request beside it. Both come from {@link USER_TOKEN_HEADERS_HOOK};\n * the data server verifies them and acts as that user. The bearer token still decides which\n * data the store reaches.\n */\nexport const USER_TOKEN_HEADER = 'x-linked-user-token';\n\n/**\n * The `globalThis` key of the hook that answers the user token headers for a request:\n * `(request, {method, url}) => Promise<{'x-linked-user-token': string; DPoP: string} | undefined>`.\n * `@_linked/auth`'s backend installs it; read here instead of importing auth, so this store\n * stays browser-safe and free of auth.\n */\nexport const USER_TOKEN_HEADERS_HOOK = Symbol.for('@_linked/auth:userTokenHeaders');\n\ntype UserTokenHeadersHook = (\n request: unknown,\n target: { method: string; url: string },\n) => Promise<unknown>;\n\n// The store `@_linked/server-utils/utils/CallContext` keeps the call context in, shared on\n// `globalThis` under this key. Read here instead of importing that module: it imports\n// `node:async_hooks`, and this store also runs in the browser (the no-url form). In a browser,\n// and in a server before any call ran, the key is unset and there is no context.\nconst CALL_CONTEXT_KEY = Symbol.for('@_linked/server-utils:callContext');\n\nfunction currentCallContext(): CallContext | undefined {\n const store = (globalThis as Record<symbol, { getStore?: () => unknown } | undefined>)[CALL_CONTEXT_KEY];\n return typeof store?.getStore === 'function' ? (store.getStore() as CallContext | undefined) : undefined;\n}\n\nlet warnedUserTokenHook = false;\nconst warnUserTokenHook = (problem: string): void => {\n if (warnedUserTokenHook) return;\n warnedUserTokenHook = true;\n console.warn(`RemoteDataset: the user token hook ${problem}; queries go without the user's token`);\n};\n\n/**\n * The user token headers for a request to `url`, made in the current call: the hook's\n * answer for the http context's request. Undefined in a system context (`runAsSystem`),\n * outside any call, without the hook, and when it answers nothing. A hook that throws or\n * answers something else than two non-empty strings sends nothing (one warning per process).\n */\nasync function userTokenHeadersFor(url: string): Promise<Record<string, string> | undefined> {\n const ctx = currentCallContext();\n if (!ctx || ctx.kind !== 'http' || !ctx.request) return undefined;\n const hook = (globalThis as Record<symbol, unknown>)[USER_TOKEN_HEADERS_HOOK];\n if (typeof hook !== 'function') return undefined;\n let answer: unknown;\n try {\n answer = await (hook as UserTokenHeadersHook)(ctx.request, { method: 'POST', url });\n } catch {\n warnUserTokenHook('failed');\n return undefined;\n }\n if (answer === undefined || answer === null) return undefined;\n const token = (answer as Record<string, unknown>)[USER_TOKEN_HEADER];\n const proof = (answer as Record<string, unknown>).DPoP;\n if (typeof token !== 'string' || !token || typeof proof !== 'string' || !proof) {\n warnUserTokenHook('answered something else than a token and a proof');\n return undefined;\n }\n return { [USER_TOKEN_HEADER]: token, DPoP: proof };\n}\n\n/**\n * A failed call to the data server at `url`. `status` is the HTTP status (0 when no HTTP\n * answer came back); `code` is the error body's `code` when the server sent one.\n */\nexport class RemoteDatasetError extends Error {\n /** The data server base url ('' when the configured url is empty). */\n readonly url: string;\n /** The HTTP status; 0 = no HTTP answer. */\n readonly status: number;\n /** The error body's `code`, when the server sent one. */\n readonly code?: string;\n\n constructor(message: string, url: string, status: number, code?: string) {\n super(message);\n this.name = new.target.name;\n this.url = url;\n this.status = status;\n if (code !== undefined) this.code = code;\n }\n}\n\n/**\n * Nothing at `url` speaks the query wire: no HTTP answer, an empty url, or (on `ping`) an\n * answer that is not the server's. A boot that gets this should stop.\n */\nexport class RemoteDatasetUnreachableError extends RemoteDatasetError {}\n\n/** The data server answered 401 or 403: it refused this dataset's token. */\nexport class RemoteDatasetUnauthorizedError extends RemoteDatasetError {}\n\n// Branded, so `isLinkedError(err, RemoteDatasetUnreachableError)` from `@_linked/core` matches an\n// error thrown by another copy of this module, where `instanceof` does not. See\n// https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md.\nbrandLinkedError(RemoteDatasetError, '@_linked/server', 'RemoteDatasetError');\nbrandLinkedError(RemoteDatasetUnreachableError, '@_linked/server', 'RemoteDatasetUnreachableError');\nbrandLinkedError(RemoteDatasetUnauthorizedError, '@_linked/server', 'RemoteDatasetUnauthorizedError');\n\n/**\n * What the data server's `ping` answered, as is. The generic `@_linked/server` ping answers\n * `{ok: true}`; a gateway may add fields that say which dataset it serves, such as\n * `{ok, projectId, branchName, dataset, forked}`. A body that is not a JSON object reads as `{}`.\n */\nexport type RemoteDatasetPingInfo = Readonly<Record<string, unknown>>;\n\ntype Method =\n | 'ping'\n | 'selectQuery'\n | 'askQuery'\n | 'updateQuery'\n | 'createQuery'\n | 'deleteQuery'\n | 'graphQuery';\n\n/** The refusal codes a data server sends back for a graph query (`{error, code}`). */\nconst GRAPH_REFUSAL_CODES: ReadonlySet<string> = new Set<GraphQueryErrorCode>([\n 'unsupported-query-kind',\n 'graph-not-registered',\n 'graph-name-read-only',\n 'graph-exists',\n]);\n\n/**\n * A failed graph query as the caller expects it: a {@link GraphQueryError} when the data server\n * refused it with one of the graph refusal codes, or when it has no graph method at all (404 or\n * 501 without any code: an older server or gateway), which reads as `unsupported-query-kind`\n * so the ontology sync falls back to the default graph. Anything else is passed on as is.\n */\nfunction graphQueryFailure(err: unknown, query: GraphQuery): unknown {\n if (isGraphQueryError(err)) return err;\n const code = (err as { code?: unknown } | null)?.code;\n const message = err instanceof Error ? err.message : String(err);\n let refusal: GraphQueryError | undefined;\n if (typeof code === 'string' && GRAPH_REFUSAL_CODES.has(code)) {\n refusal = new GraphQueryError(code as GraphQueryErrorCode, message, query.graph);\n } else if (code === undefined) {\n const status = (err as { status?: unknown } | null)?.status;\n if (status === 404 || status === 501) refusal = unsupportedGraphQuery(query, message);\n }\n if (!refusal) return err;\n (refusal as { cause?: unknown }).cause = err;\n return refusal;\n}\n\n/** The portable names a remote store expands from its own registry cache, with their record class. */\nconst REGISTRY_EXPANDED: ReadonlyMap<string, string> = new Map([\n [graphs.allOntologies, OntologyGraph.targetClass.id],\n [graphs.allShapes, ShapesGraph.targetClass.id],\n]);\n\nlet nextRegistryKey = 0;\n\n/** On `ping`, these statuses mean nothing at that url serves the query wire. */\nconst PING_UNREACHABLE_STATUSES = new Set([404, 501, 502, 503, 504]);\n\n/**\n * An {@link IDataset} whose queries are answered by a linked server.\n *\n * - **Without `url`** the server is this app's own backend: every query goes through\n * `Server.call`'s package form (`/call/@_linked/server/<method>` with a body of `{args}`),\n * and on a backend `Server`'s local shortcut answers it in process. This is the store a\n * browser uses to talk to its backend.\n * - **With `url`** the server is a remote data server: every query posts\n * `{\"args\": [query.toJSON()]}` to `{url}/call/@_linked/server/<method>` with the bearer\n * `token`, through `fetch`, never through `Server` (whose local shortcut would answer the\n * app's own provider) and never retried (a retried write could apply twice). `init()` asks\n * the server's `ping` and throws {@link RemoteDatasetUnreachableError} or\n * {@link RemoteDatasetUnauthorizedError} when it cannot be used.\n *\n * A store is not a Shape: it addresses the backend by package name, so nothing about the\n * store instance crosses the wire. Query routing is decided by the query's shape.\n *\n * `LinkedStorage.setDefaultDataset` calls `init()` without awaiting it, so a boot that must\n * fail loudly awaits `init()` itself first; a successful `init()` is memoized, and resolves\n * the ping body (also readable as `info`), so a boot can log which dataset the server serves.\n */\nexport class RemoteDataset implements IDataset {\n /**\n * Kept so the config form is still accepted and a store can be named; it is not an\n * addressing mechanism, nothing transmits or resolves it.\n */\n readonly id?: string;\n /** The data server base url, trailing slashes trimmed; undefined in the no-url form. */\n readonly url?: string;\n private readonly token?: string;\n /** Whether the url form sends {@link USER_TOKEN_HEADER}; see `RemoteDatasetConfig.forwardUserToken`. */\n readonly forwardUserToken: boolean = true;\n private initialized?: Promise<RemoteDatasetPingInfo | undefined>;\n private pingInfo?: RemoteDatasetPingInfo;\n private readonly instanceRegistryKey = `remote-dataset:${nextRegistryKey++}`;\n private warnedRegistryRead = false;\n\n constructor(config?: RemoteDatasetConfig | string) {\n if (!config) return;\n if (typeof config === 'string') {\n this.id = `${process.env.DATA_ROOT}/backend-api-store/${config}`;\n return;\n }\n if (typeof config.url === 'string') {\n this.url = config.url.trim().replace(/\\/+$/, '');\n const token = typeof config.token === 'string' ? config.token.trim() : '';\n if (token) this.token = token;\n }\n if (config.forwardUserToken === false || config.forwardUserToken === 'false') {\n this.forwardUserToken = false;\n }\n if (config.id) {\n this.id = config.id;\n return;\n }\n if (config.name) {\n this.id = `${process.env.DATA_ROOT}/backend-api-store/${config.name}`;\n }\n }\n\n /**\n * The body of the last successful `ping` (see {@link init}); undefined before one, and\n * always in the no-url form.\n */\n get info(): RemoteDatasetPingInfo | undefined {\n return this.pingInfo;\n }\n\n /**\n * In the url form, asks the server's `ping` and resolves its body (also kept as\n * {@link info}); resolves undefined in the no-url form.\n */\n init(): Promise<RemoteDatasetPingInfo | undefined> {\n // No initialization needed when queries are routed to the app's own backend.\n if (this.url === undefined) return Promise.resolve(undefined);\n if (!this.initialized) {\n this.initialized = this.post('ping', []).then(\n (body) => {\n const info: RemoteDatasetPingInfo = Object.freeze(\n body && typeof body === 'object' && !Array.isArray(body)\n ? { ...(body as Record<string, unknown>) }\n : {},\n );\n this.pingInfo = info;\n return info;\n },\n (err) => {\n // Not memoized: the next init() asks again.\n this.initialized = undefined;\n throw err;\n },\n );\n }\n return this.initialized;\n }\n\n // Queries serialize to DSL-JSON for the wire: the live (closed) query can't cross a call\n // as-is, so we ship `toJSON()` and the server's generic provider rehydrates it with\n // `fromJSON()`.\n async selectQuery(query: SelectQuery | CountQuery): Promise<SelectResult | number> {\n return this.call('selectQuery', await this.withReadGraphs(query.toJSON()));\n }\n\n async askQuery(query: AskQuery): Promise<boolean> {\n return this.call('askQuery', await this.withReadGraphs(query.toJSON()));\n }\n\n updateQuery(query: UpdateQuery): Promise<UpdateResult> {\n return this.call('updateQuery', query.toJSON());\n }\n\n createQuery(query: CreateQuery): Promise<CreateResult> {\n return this.call('createQuery', query.toJSON());\n }\n\n deleteQuery(query: DeleteQuery): Promise<DeleteResponse> {\n return this.call('deleteQuery', query.toJSON());\n }\n\n /**\n * A whole-graph operation (`graph_create`, `graph_replace`, `graph_patch`, `graph_drop`): the\n * query is plain data, so it is sent as is, the same way and with the same headers as every\n * other query. On success this store's registry cache notes it in the same step (a create adds\n * the graph, a drop removes it), so its next `.from(graphs.allOntologies)` read sees the change.\n * A refusal rejects with a `GraphQueryError` (see `graphQueryFailure`): a server or gateway\n * without the method answers `unsupported-query-kind`.\n */\n async graphQuery(query: GraphQuery): Promise<GraphResult> {\n let result: GraphResult;\n try {\n result = await this.call<GraphResult>('graphQuery', query);\n } catch (err) {\n throw graphQueryFailure(err, query);\n }\n if (this.url !== undefined) noteGraphQuery(this.graphRegistryKey(), query);\n return result;\n }\n\n /**\n * The key this store's graph-registry cache is kept under: the data server's url and the\n * dataset it named in its `ping` answer (a gateway answers `{dataset}`), so stores of the\n * same dataset share one entry; else one key per instance. (`POST /admin/graph-registry-changed`\n * drops every entry of the process, whatever its key.)\n */\n protected graphRegistryKey(): string {\n const dataset = this.pingInfo?.dataset;\n return typeof dataset === 'string' && dataset && this.url\n ? `${this.url}#${dataset}`\n : this.instanceRegistryKey;\n }\n\n /**\n * Drop the cached graph registry of `dataset` (default: this store's, see `graphRegistryKey`):\n * the next read that expands `graphs.allOntologies` re-reads it.\n */\n invalidateGraphRegistry(dataset?: string): void {\n invalidateGraphRegistry(dataset ?? this.graphRegistryKey());\n }\n\n /**\n * Read the data server's graph registry: the records of the ontology and shapes graphs, by\n * class, through the same query wire (the record shapes read the registry graph themselves).\n */\n protected async loadGraphRegistry(): Promise<GraphRegistryRow[]> {\n const rows: GraphRegistryRow[] = [];\n const kinds = [OntologyGraph, ShapesGraph] as const;\n const results = await Promise.all(kinds.map((kind) => kind.select().exec(this)));\n results.forEach((records, i) => {\n const recordClass = kinds[i].targetClass.id;\n for (const record of (records as Array<{ id?: unknown }> | null) ?? []) {\n if (typeof record?.id === 'string' && record.id) rows.push({ graph: record.id, recordClass });\n }\n });\n return rows;\n }\n\n /**\n * In the url form, expand `graphs.allOntologies` and `graphs.allShapes` in a read's `graphs`\n * from this store's registry cache (per dataset, updated by its own graph queries, dropped by a\n * pushed invalidation, re-read after 30 seconds), so the data server reads exactly the graphs\n * this process knows of. Other names (graph IRIs, `graphs.default`, `graphs.union`) go as they\n * are, for the data server's store to resolve. A name that expands to no graph is sent as is,\n * so the data server answers from its own registry; so is every name when the registry cannot\n * be read (the data server refused or failed the record selects), with one warning per store.\n * The no-url form sends `graphs` unchanged: the backend's store expands them.\n */\n private async withReadGraphs(json: unknown): Promise<unknown> {\n if (this.url === undefined || !json || typeof json !== 'object') return json;\n const names = (json as { graphs?: unknown }).graphs;\n if (!Array.isArray(names) || !names.some((name) => REGISTRY_EXPANDED.has(name))) return json;\n let registry: Awaited<ReturnType<typeof readGraphRegistry>>;\n try {\n registry = await readGraphRegistry(this.graphRegistryKey(), () => this.loadGraphRegistry());\n } catch (err) {\n if (!this.warnedRegistryRead) {\n this.warnedRegistryRead = true;\n console.warn(\n `RemoteDataset: could not read the graph registry at ${this.url}; reads send graph names unexpanded:`,\n err instanceof Error ? err.message : err,\n );\n }\n return json;\n }\n const out: string[] = [];\n const add = (iri: string) => {\n if (!out.includes(iri)) out.push(iri);\n };\n for (const name of names as string[]) {\n const recordClass = REGISTRY_EXPANDED.get(name);\n if (!recordClass) {\n add(name);\n continue;\n }\n const listed = [...registry].filter(([, classes]) => classes.has(recordClass)).map(([graph]) => graph);\n if (listed.length) listed.sort().forEach(add);\n else add(name);\n }\n return { ...(json as Record<string, unknown>), graphs: out };\n }\n\n private call<T>(method: Method, json: unknown): Promise<T> {\n if (this.url === undefined) return this.callBackend(method, json);\n return this.post(method, [json]) as Promise<T>;\n }\n\n /**\n * Opts in to `rejectOnError`, so a failed call (HTTP error status, or no\n * provider on the backend) rejects with a `ServerCallError` carrying the\n * status and the server's message instead of reading as an empty result.\n * Whatever a successful call returns, `undefined` included, resolves as is.\n */\n private callBackend<T>(method: string, json: unknown): Promise<T> {\n // The PACKAGE form, not the shape form. It posts to\n // `/call/<packageName>/<method>` with a body of `{args}` only — no `shapeURI`,\n // no `instanceNode` — and the backend answers from this package's generic\n // provider without resolving or instantiating any Shape.\n return Server.call(packageName, { method, rejectOnError: true }, json);\n }\n\n /**\n * The url form: one POST to `{url}/call/@_linked/server/<method>`. Resolves the parsed\n * body (`null` is a valid result: a method that returned `undefined`). Messages name the\n * url and the method, never the token.\n */\n private async post(method: Method, args: unknown[]): Promise<unknown> {\n const base = this.url ?? '';\n if (!base) {\n throw new RemoteDatasetUnreachableError(\n `RemoteDataset: no data server url is set (the configured url is empty); cannot ${method}`,\n '',\n 0,\n );\n }\n const target = `${base}/call/${packageName}/${method}`;\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n if (this.token) headers.Authorization = `Bearer ${this.token}`;\n if (this.forwardUserToken) {\n // a fresh proof for every request: never cached (each has its own `jti`)\n const userHeaders = await userTokenHeadersFor(target);\n if (userHeaders) Object.assign(headers, userHeaders);\n }\n\n let response: Response;\n try {\n response = await globalThis.fetch(target, {\n method: 'POST',\n headers,\n body: JSON.stringify({ args }),\n // A redirect is an answer, never followed: the bearer and the user's token and proof\n // go to the data server at `url` and nowhere else.\n redirect: 'manual',\n });\n } catch (err) {\n const cause = err instanceof Error ? err.message : String(err);\n throw new RemoteDatasetUnreachableError(\n `RemoteDataset: cannot reach the data server at ${base} (${method}): ${cause}`,\n base,\n 0,\n );\n }\n\n const text = await response.text();\n let body: unknown = undefined;\n let isJson = false;\n if (text) {\n try {\n body = JSON.parse(text);\n isJson = true;\n } catch {\n body = text;\n }\n }\n\n if (response.ok) {\n if (isJson) return body;\n if (method === 'ping') {\n throw new RemoteDatasetUnreachableError(\n `RemoteDataset: the data server at ${base} answered ping with a body that is not JSON`,\n base,\n response.status,\n );\n }\n if (!text) return undefined;\n throw new RemoteDatasetError(\n `RemoteDataset: the data server at ${base} answered ${method} with a body that is not JSON`,\n base,\n response.status,\n );\n }\n\n const record = body && typeof body === 'object' ? (body as Record<string, unknown>) : undefined;\n const code = typeof record?.code === 'string' ? record.code : undefined;\n const detail =\n typeof record?.error === 'string'\n ? record.error\n : typeof body === 'string' && body\n ? body\n : response.statusText;\n const message = `RemoteDataset: the data server at ${base} answered ${method} with ${response.status}${\n code ? ` (${code})` : ''\n }${detail ? `: ${detail}` : ''}`;\n\n if (response.status === 401 || response.status === 403) {\n throw new RemoteDatasetUnauthorizedError(message, base, response.status, code);\n }\n if (method === 'ping' && (PING_UNREACHABLE_STATUSES.has(response.status) || !isJson)) {\n throw new RemoteDatasetUnreachableError(message, base, response.status, code);\n }\n throw new RemoteDatasetError(message, base, response.status, code);\n }\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type express from 'express';
|
|
2
2
|
import type { ShapeGraphsReport } from './syncShapes.js';
|
|
3
|
+
import type { ShapeCheckSummary } from './ontologySync.js';
|
|
3
4
|
import { type EffectiveRoutes } from './effectiveRoutes.js';
|
|
4
5
|
/** The environment variable that holds the control channel's secret. */
|
|
5
6
|
export declare const ADMIN_SECRET_ENV = "APP_ADMIN_SECRET";
|
|
@@ -42,18 +43,18 @@ export interface ShapeSyncResult {
|
|
|
42
43
|
graphs?: string[];
|
|
43
44
|
droppedGraphs?: string[];
|
|
44
45
|
};
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The shape check's findings, counted by severity (each is logged), and the message of the
|
|
48
|
+
* error the check threw, if it did (`failed`). Set by `POST /admin/sync-shapes`.
|
|
49
|
+
*/
|
|
50
|
+
shapeCheck?: ShapeCheckSummary;
|
|
50
51
|
}
|
|
51
52
|
/** Whether `name` is a valid npm package name (and so can never be a path like `..`). */
|
|
52
53
|
export declare function isValidPackageName(name: string): boolean;
|
|
53
54
|
/**
|
|
54
55
|
* Refuse a package a host may not ask to reload: not a valid npm name, a framework
|
|
55
56
|
* (`@_linked/*`) package, or not in `allowed` (the app's own name and its direct
|
|
56
|
-
* dependencies). Throws {@link AdminRequestError}.
|
|
57
|
+
* dependencies, its `linked.unpublishedPackages` among them). Throws {@link AdminRequestError}.
|
|
57
58
|
*/
|
|
58
59
|
export declare function assertReloadablePackages(packages: string[], allowed: ReadonlySet<string>): void;
|
|
59
60
|
/** Thrown by a handler for a request the channel refuses with 400. */
|
|
@@ -49,7 +49,7 @@ export function isValidPackageName(name) {
|
|
|
49
49
|
/**
|
|
50
50
|
* Refuse a package a host may not ask to reload: not a valid npm name, a framework
|
|
51
51
|
* (`@_linked/*`) package, or not in `allowed` (the app's own name and its direct
|
|
52
|
-
* dependencies). Throws {@link AdminRequestError}.
|
|
52
|
+
* dependencies, its `linked.unpublishedPackages` among them). Throws {@link AdminRequestError}.
|
|
53
53
|
*/
|
|
54
54
|
export function assertReloadablePackages(packages, allowed) {
|
|
55
55
|
for (const pkg of packages) {
|
|
@@ -58,7 +58,7 @@ export function assertReloadablePackages(packages, allowed) {
|
|
|
58
58
|
if (pkg.startsWith('@_linked/'))
|
|
59
59
|
throw new AdminRequestError(`framework packages are not reloaded: ${pkg}`);
|
|
60
60
|
if (!allowed.has(pkg)) {
|
|
61
|
-
throw new AdminRequestError(`unknown package: ${pkg} is neither the app nor one of its direct dependencies`);
|
|
61
|
+
throw new AdminRequestError(`unknown package: ${pkg} is neither the app nor one of its direct dependencies (dependencies, devDependencies, linked.unpublishedPackages)`);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adminChannel.js","sourceRoot":"","sources":["../../../src/utils/adminChannel.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The host→app control channel: `/admin/*` routes a host uses on an app process it\n * started. Present only when the process was started with a shared secret in\n * `APP_ADMIN_SECRET` (see {@link resolveAdminSecret}); without it every route answers\n * 404, so a standalone app has no control channel. Every request carries the secret in\n * the `x-app-admin-secret` header (401 otherwise).\n *\n * - `GET /admin/health` → 200 `{status: 'ok'}` once the server listens (the boot\n * shape sync has finished by then).\n * - `POST /admin/restart` → 202, then the process exits; the host starts it again.\n * - `POST /admin/sync-shapes` → re-syncs the app's ontologies (each registered package's\n * ontology graph, skipped when unchanged), runs the shape check and\n * re-syncs its shapes into its app data (each package's shape graph and the app's own,\n * skipped when unchanged), as the boot does, and answers 200 with\n * {@link ShapeSyncResult}, whose `shapeGraphs` reports the shape graphs. Body (optional):\n * `{packages: string[]}`, packages whose source changed on disk: in dev they are\n * reloaded first, so the sync sees shapes added or changed since the boot. Each\n * name must be a valid npm package name and the app itself or one of its direct\n * dependencies, never an `@_linked/*` framework package; anything else answers\n * 400 `{error}`. The list never filters the syncs. A failed or timed-out sync\n * answers 500 `{error}` (a failed ontology sync too, after the shape sync ran).\n * - `POST /admin/graph-registry-changed` `{dataset}` → the host changed the named graphs of that\n * dataset (a graph query through its gateway): the process drops its cached graph registry, so\n * its next `.from(graphs.allOntologies)` read re-reads it, and answers 200\n * `{invalidated: dataset}`. A body without a dataset name answers 400. Without the push the\n * cache's 30-second expiry applies.\n * - `GET /admin/routes` → 200 {@link EffectiveRoutes}: the graph routes of every dataset the\n * router knows (shape IRI → graph IRI) and the shapes each one writes to its default graph,\n * as core's routing holds them once the app's storage file has run. The routes are written\n * only in the app's storage file; this route lets a host check what actually runs.\n */\nimport { createHash, timingSafeEqual } from 'crypto';\nimport type express from 'express';\nimport { invalidateGraphRegistry } from '@_linked/core/utils/graphRegistryCache';\nimport type { ShapeGraphsReport } from './syncShapes.js';\nimport { effectiveGraphRoutes, type EffectiveRoutes } from './effectiveRoutes.js';\n\n/** The environment variable that holds the control channel's secret. */\nexport const ADMIN_SECRET_ENV = 'APP_ADMIN_SECRET';\n/** The request header that carries the secret. */\nexport const ADMIN_SECRET_HEADER = 'x-app-admin-secret';\n\n/** The control channel's secret: `APP_ADMIN_SECRET`, else `undefined` (no control channel). */\nexport function resolveAdminSecret(\n env: Record<string, string | undefined> = process.env\n): string | undefined {\n return env[ADMIN_SECRET_ENV] || undefined;\n}\n\n/** What one shape sync did. */\nexport interface ShapeSyncResult {\n /**\n * Shape graphs written (package graphs and the app's own graph) because their stored hash\n * differed; on a store without graph queries, the default-graph sync's writes.\n */\n written: number;\n /** The orphan policy the sync ran with (see utils/syncShapes). */\n orphanScope?: 'all' | 'none';\n /** Set when no sync ran: shape sync is turned off, or there is no default dataset. */\n skipped?: 'disabled' | 'no-default-dataset';\n /**\n * The shape graph sync: packages written and skipped, the graphs written, the app's own shape\n * graph, the registered packages without `shapes.ttl` (so without a shape graph) and the\n * default-graph shapes removed. Absent when no sync ran.\n */\n shapeGraphs?: ShapeGraphsReport;\n /**\n * The requested packages whose source was reloaded before the sync (dev only: a built\n * app reloads nothing). Set by `POST /admin/sync-shapes`.\n */\n reloaded?: string[];\n /** The node shape IRIs registered in the process after the reload. Set by `POST /admin/sync-shapes`. */\n shapes?: string[];\n /**\n * The ontology sync: packages whose ontology graph (or, on a store without graph queries,\n * class hierarchy) was written, packages left as they were (unchanged, or their data could not\n * be read), and the graphs written and dropped. Set by `POST /admin/sync-shapes`.\n */\n ontologies?: { written: string[]; skipped: string[]; graphs?: string[]; droppedGraphs?: string[] };\n /** The shape check's findings, counted by severity (each is logged). Set by `POST /admin/sync-shapes`. */\n shapeCheck?: { errors: number; warnings: number };\n}\n\n/** npm's package-name rule: optional `@scope/`, lower case, URL-safe, no leading `.` or `_` (a scope may start with `_`, as `@_linked` does). */\nconst NPM_NAME = /^(?:@[a-z0-9-~_][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/;\n\n/** Whether `name` is a valid npm package name (and so can never be a path like `..`). */\nexport function isValidPackageName(name: string): boolean {\n return name.length <= 214 && NPM_NAME.test(name) && !name.split('/').some((part) => part === '.' || part === '..');\n}\n\n/**\n * Refuse a package a host may not ask to reload: not a valid npm name, a framework\n * (`@_linked/*`) package, or not in `allowed` (the app's own name and its direct\n * dependencies). Throws {@link AdminRequestError}.\n */\nexport function assertReloadablePackages(packages: string[], allowed: ReadonlySet<string>): void {\n for (const pkg of packages) {\n if (!isValidPackageName(pkg)) throw new AdminRequestError(`not a package name: ${JSON.stringify(pkg)}`);\n if (pkg.startsWith('@_linked/')) throw new AdminRequestError(`framework packages are not reloaded: ${pkg}`);\n if (!allowed.has(pkg)) {\n throw new AdminRequestError(`unknown package: ${pkg} is neither the app nor one of its direct dependencies`);\n }\n }\n}\n\n/** Thrown by a handler for a request the channel refuses with 400. */\nexport class AdminRequestError extends Error {}\n\nexport interface AdminChannelHandlers {\n /** Exit the process (the host restarts it). Called after the 202 is sent. */\n exit: () => void;\n /** Reload `packages` (when given) and sync the registered shapes into app data. */\n syncShapes: (request: { packages: string[] }) => Promise<ShapeSyncResult>;\n /**\n * Drop the cached graph registry for `dataset` (see `POST /admin/graph-registry-changed`).\n * Default: {@link dropGraphRegistryCache}.\n */\n graphRegistryChanged?: (dataset: string) => void;\n /** The effective graph routes (see `GET /admin/routes`). Default: {@link effectiveGraphRoutes}. */\n routes?: () => EffectiveRoutes;\n}\n\n/**\n * Drop the process's cached graph registry after the host changed the graphs of `dataset`.\n *\n * Every entry is dropped, not only that dataset's: an entry's key is each store's own\n * (`graphRegistryKey()`: a `FusekiStore` keys by its dataset URL, a `RemoteDataset` by the\n * dataset its server names), no store tells which key it uses, and the stores are built by the\n * app's storage config, whose aliases the server does not see. The registry is small, so the cost\n * is one registry read per dataset on its next expanding read.\n */\nexport function dropGraphRegistryCache(_dataset: string): void {\n invalidateGraphRegistry();\n}\n\n/** The `dataset` of a graph-registry-changed body: a non-empty name. */\nexport function parseGraphRegistryChangedBody(body: unknown): { dataset: string } {\n const dataset = body && typeof body === 'object' ? (body as { dataset?: unknown }).dataset : undefined;\n if (typeof dataset !== 'string' || !dataset.trim() || dataset.length > 1024) {\n throw new AdminRequestError('dataset must be the name of the dataset whose graphs changed');\n }\n return { dataset: dataset.trim() };\n}\n\n/**\n * Compare the provided secret with the configured one in constant time. Both sides are hashed\n * first, so the comparison does not reveal the secret's length either.\n */\nexport function adminSecretMatches(\n secret: string | undefined,\n provided: string | undefined\n): boolean {\n if (!secret || !provided) return false;\n const digest = (value: string) => createHash('sha256').update(value).digest();\n return timingSafeEqual(digest(provided), digest(secret));\n}\n\n/** The `packages` of a sync-shapes body: a list of package names, or none. */\nexport function parseSyncShapesBody(body: unknown): { packages: string[] } {\n if (body === undefined || body === null || body === '') return { packages: [] };\n if (typeof body !== 'object') {\n throw new AdminRequestError('the body must be a JSON object');\n }\n const packages = (body as { packages?: unknown }).packages;\n if (packages === undefined) return { packages: [] };\n if (!Array.isArray(packages) || packages.some((p) => typeof p !== 'string' || !p)) {\n throw new AdminRequestError('packages must be a list of package names');\n }\n for (const p of packages as string[]) {\n if (!isValidPackageName(p)) throw new AdminRequestError(`not a package name: ${JSON.stringify(p)}`);\n }\n return { packages: [...new Set(packages as string[])] };\n}\n\n/** Mount the control channel's routes on `app`. */\nexport function registerAdminRoutes(\n app: express.Express,\n secret: string | undefined,\n handlers: AdminChannelHandlers\n): void {\n const requireAdmin = (req: express.Request, res: express.Response): boolean => {\n if (!secret) {\n res.sendStatus(404); // no control channel in this mode\n return false;\n }\n if (!adminSecretMatches(secret, req.get(ADMIN_SECRET_HEADER))) {\n res.sendStatus(401);\n return false;\n }\n return true;\n };\n\n app.get('/admin/health', (req, res) => {\n if (!requireAdmin(req, res)) return;\n res.status(200).json({ status: 'ok' });\n });\n\n app.post('/admin/restart', (req, res) => {\n if (!requireAdmin(req, res)) return;\n res.sendStatus(202);\n setTimeout(handlers.exit, 50);\n });\n\n app.get('/admin/routes', (req, res) => {\n if (!requireAdmin(req, res)) return;\n try {\n res.status(200).json((handlers.routes ?? effectiveGraphRoutes)());\n } catch (err) {\n res.status(500).json({ error: err instanceof Error ? err.message : String(err) });\n }\n });\n\n app.post('/admin/graph-registry-changed', (req, res) => {\n if (!requireAdmin(req, res)) return;\n let dataset: string;\n try {\n ({ dataset } = parseGraphRegistryChangedBody(req.body));\n } catch (err) {\n res.status(400).json({ error: (err as Error).message });\n return;\n }\n (handlers.graphRegistryChanged ?? dropGraphRegistryCache)(dataset);\n res.status(200).json({ invalidated: dataset });\n });\n\n app.post('/admin/sync-shapes', async (req, res) => {\n if (!requireAdmin(req, res)) return;\n let request: { packages: string[] };\n try {\n request = parseSyncShapesBody(req.body);\n } catch (err) {\n res.status(400).json({ error: (err as Error).message });\n return;\n }\n try {\n res.status(200).json(await handlers.syncShapes(request));\n } catch (err) {\n const status = err instanceof AdminRequestError ? 400 : 500;\n res.status(status).json({ error: err instanceof Error ? err.message : String(err) });\n }\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"adminChannel.js","sourceRoot":"","sources":["../../../src/utils/adminChannel.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The host→app control channel: `/admin/*` routes a host uses on an app process it\n * started. Present only when the process was started with a shared secret in\n * `APP_ADMIN_SECRET` (see {@link resolveAdminSecret}); without it every route answers\n * 404, so a standalone app has no control channel. Every request carries the secret in\n * the `x-app-admin-secret` header (401 otherwise).\n *\n * - `GET /admin/health` → 200 `{status: 'ok'}` once the server listens (the boot\n * shape sync has finished by then).\n * - `POST /admin/restart` → 202, then the process exits; the host starts it again.\n * - `POST /admin/sync-shapes` → re-syncs the app's ontologies (each registered package's\n * ontology graph, skipped when unchanged), runs the shape check and\n * re-syncs its shapes into its app data (each package's shape graph and the app's own,\n * skipped when unchanged), as the boot does, and answers 200 with\n * {@link ShapeSyncResult}, whose `shapeGraphs` reports the shape graphs. Body (optional):\n * `{packages: string[]}`, packages whose source changed on disk: in dev they are\n * reloaded first, so the sync sees shapes added or changed since the boot. Each\n * name must be a valid npm package name and the app itself or one of its direct\n * dependencies, never an `@_linked/*` framework package; anything else answers\n * 400 `{error}`. The list never filters the syncs. A failed or timed-out sync\n * answers 500 `{error}` (a failed ontology sync too, after the shape sync ran).\n * - `POST /admin/graph-registry-changed` `{dataset}` → the host changed the named graphs of that\n * dataset (a graph query through its gateway): the process drops its cached graph registry, so\n * its next `.from(graphs.allOntologies)` read re-reads it, and answers 200\n * `{invalidated: dataset}`. A body without a dataset name answers 400. Without the push the\n * cache's 30-second expiry applies.\n * - `GET /admin/routes` → 200 {@link EffectiveRoutes}: the graph routes of every dataset the\n * router knows (shape IRI → graph IRI) and the shapes each one writes to its default graph,\n * as core's routing holds them once the app's storage file has run. The routes are written\n * only in the app's storage file; this route lets a host check what actually runs.\n */\nimport { createHash, timingSafeEqual } from 'crypto';\nimport type express from 'express';\nimport { invalidateGraphRegistry } from '@_linked/core/utils/graphRegistryCache';\nimport type { ShapeGraphsReport } from './syncShapes.js';\nimport type { ShapeCheckSummary } from './ontologySync.js';\nimport { effectiveGraphRoutes, type EffectiveRoutes } from './effectiveRoutes.js';\n\n/** The environment variable that holds the control channel's secret. */\nexport const ADMIN_SECRET_ENV = 'APP_ADMIN_SECRET';\n/** The request header that carries the secret. */\nexport const ADMIN_SECRET_HEADER = 'x-app-admin-secret';\n\n/** The control channel's secret: `APP_ADMIN_SECRET`, else `undefined` (no control channel). */\nexport function resolveAdminSecret(\n env: Record<string, string | undefined> = process.env\n): string | undefined {\n return env[ADMIN_SECRET_ENV] || undefined;\n}\n\n/** What one shape sync did. */\nexport interface ShapeSyncResult {\n /**\n * Shape graphs written (package graphs and the app's own graph) because their stored hash\n * differed; on a store without graph queries, the default-graph sync's writes.\n */\n written: number;\n /** The orphan policy the sync ran with (see utils/syncShapes). */\n orphanScope?: 'all' | 'none';\n /** Set when no sync ran: shape sync is turned off, or there is no default dataset. */\n skipped?: 'disabled' | 'no-default-dataset';\n /**\n * The shape graph sync: packages written and skipped, the graphs written, the app's own shape\n * graph, the registered packages without `shapes.ttl` (so without a shape graph) and the\n * default-graph shapes removed. Absent when no sync ran.\n */\n shapeGraphs?: ShapeGraphsReport;\n /**\n * The requested packages whose source was reloaded before the sync (dev only: a built\n * app reloads nothing). Set by `POST /admin/sync-shapes`.\n */\n reloaded?: string[];\n /** The node shape IRIs registered in the process after the reload. Set by `POST /admin/sync-shapes`. */\n shapes?: string[];\n /**\n * The ontology sync: packages whose ontology graph (or, on a store without graph queries,\n * class hierarchy) was written, packages left as they were (unchanged, or their data could not\n * be read), and the graphs written and dropped. Set by `POST /admin/sync-shapes`.\n */\n ontologies?: { written: string[]; skipped: string[]; graphs?: string[]; droppedGraphs?: string[] };\n /**\n * The shape check's findings, counted by severity (each is logged), and the message of the\n * error the check threw, if it did (`failed`). Set by `POST /admin/sync-shapes`.\n */\n shapeCheck?: ShapeCheckSummary;\n}\n\n/** npm's package-name rule: optional `@scope/`, lower case, URL-safe, no leading `.` or `_` (a scope may start with `_`, as `@_linked` does). */\nconst NPM_NAME = /^(?:@[a-z0-9-~_][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/;\n\n/** Whether `name` is a valid npm package name (and so can never be a path like `..`). */\nexport function isValidPackageName(name: string): boolean {\n return name.length <= 214 && NPM_NAME.test(name) && !name.split('/').some((part) => part === '.' || part === '..');\n}\n\n/**\n * Refuse a package a host may not ask to reload: not a valid npm name, a framework\n * (`@_linked/*`) package, or not in `allowed` (the app's own name and its direct\n * dependencies, its `linked.unpublishedPackages` among them). Throws {@link AdminRequestError}.\n */\nexport function assertReloadablePackages(packages: string[], allowed: ReadonlySet<string>): void {\n for (const pkg of packages) {\n if (!isValidPackageName(pkg)) throw new AdminRequestError(`not a package name: ${JSON.stringify(pkg)}`);\n if (pkg.startsWith('@_linked/')) throw new AdminRequestError(`framework packages are not reloaded: ${pkg}`);\n if (!allowed.has(pkg)) {\n throw new AdminRequestError(`unknown package: ${pkg} is neither the app nor one of its direct dependencies (dependencies, devDependencies, linked.unpublishedPackages)`);\n }\n }\n}\n\n/** Thrown by a handler for a request the channel refuses with 400. */\nexport class AdminRequestError extends Error {}\n\nexport interface AdminChannelHandlers {\n /** Exit the process (the host restarts it). Called after the 202 is sent. */\n exit: () => void;\n /** Reload `packages` (when given) and sync the registered shapes into app data. */\n syncShapes: (request: { packages: string[] }) => Promise<ShapeSyncResult>;\n /**\n * Drop the cached graph registry for `dataset` (see `POST /admin/graph-registry-changed`).\n * Default: {@link dropGraphRegistryCache}.\n */\n graphRegistryChanged?: (dataset: string) => void;\n /** The effective graph routes (see `GET /admin/routes`). Default: {@link effectiveGraphRoutes}. */\n routes?: () => EffectiveRoutes;\n}\n\n/**\n * Drop the process's cached graph registry after the host changed the graphs of `dataset`.\n *\n * Every entry is dropped, not only that dataset's: an entry's key is each store's own\n * (`graphRegistryKey()`: a `FusekiStore` keys by its dataset URL, a `RemoteDataset` by the\n * dataset its server names), no store tells which key it uses, and the stores are built by the\n * app's storage config, whose aliases the server does not see. The registry is small, so the cost\n * is one registry read per dataset on its next expanding read.\n */\nexport function dropGraphRegistryCache(_dataset: string): void {\n invalidateGraphRegistry();\n}\n\n/** The `dataset` of a graph-registry-changed body: a non-empty name. */\nexport function parseGraphRegistryChangedBody(body: unknown): { dataset: string } {\n const dataset = body && typeof body === 'object' ? (body as { dataset?: unknown }).dataset : undefined;\n if (typeof dataset !== 'string' || !dataset.trim() || dataset.length > 1024) {\n throw new AdminRequestError('dataset must be the name of the dataset whose graphs changed');\n }\n return { dataset: dataset.trim() };\n}\n\n/**\n * Compare the provided secret with the configured one in constant time. Both sides are hashed\n * first, so the comparison does not reveal the secret's length either.\n */\nexport function adminSecretMatches(\n secret: string | undefined,\n provided: string | undefined\n): boolean {\n if (!secret || !provided) return false;\n const digest = (value: string) => createHash('sha256').update(value).digest();\n return timingSafeEqual(digest(provided), digest(secret));\n}\n\n/** The `packages` of a sync-shapes body: a list of package names, or none. */\nexport function parseSyncShapesBody(body: unknown): { packages: string[] } {\n if (body === undefined || body === null || body === '') return { packages: [] };\n if (typeof body !== 'object') {\n throw new AdminRequestError('the body must be a JSON object');\n }\n const packages = (body as { packages?: unknown }).packages;\n if (packages === undefined) return { packages: [] };\n if (!Array.isArray(packages) || packages.some((p) => typeof p !== 'string' || !p)) {\n throw new AdminRequestError('packages must be a list of package names');\n }\n for (const p of packages as string[]) {\n if (!isValidPackageName(p)) throw new AdminRequestError(`not a package name: ${JSON.stringify(p)}`);\n }\n return { packages: [...new Set(packages as string[])] };\n}\n\n/** Mount the control channel's routes on `app`. */\nexport function registerAdminRoutes(\n app: express.Express,\n secret: string | undefined,\n handlers: AdminChannelHandlers\n): void {\n const requireAdmin = (req: express.Request, res: express.Response): boolean => {\n if (!secret) {\n res.sendStatus(404); // no control channel in this mode\n return false;\n }\n if (!adminSecretMatches(secret, req.get(ADMIN_SECRET_HEADER))) {\n res.sendStatus(401);\n return false;\n }\n return true;\n };\n\n app.get('/admin/health', (req, res) => {\n if (!requireAdmin(req, res)) return;\n res.status(200).json({ status: 'ok' });\n });\n\n app.post('/admin/restart', (req, res) => {\n if (!requireAdmin(req, res)) return;\n res.sendStatus(202);\n setTimeout(handlers.exit, 50);\n });\n\n app.get('/admin/routes', (req, res) => {\n if (!requireAdmin(req, res)) return;\n try {\n res.status(200).json((handlers.routes ?? effectiveGraphRoutes)());\n } catch (err) {\n res.status(500).json({ error: err instanceof Error ? err.message : String(err) });\n }\n });\n\n app.post('/admin/graph-registry-changed', (req, res) => {\n if (!requireAdmin(req, res)) return;\n let dataset: string;\n try {\n ({ dataset } = parseGraphRegistryChangedBody(req.body));\n } catch (err) {\n res.status(400).json({ error: (err as Error).message });\n return;\n }\n (handlers.graphRegistryChanged ?? dropGraphRegistryCache)(dataset);\n res.status(200).json({ invalidated: dataset });\n });\n\n app.post('/admin/sync-shapes', async (req, res) => {\n if (!requireAdmin(req, res)) return;\n let request: { packages: string[] };\n try {\n request = parseSyncShapesBody(req.body);\n } catch (err) {\n res.status(400).json({ error: (err as Error).message });\n return;\n }\n try {\n res.status(200).json(await handlers.syncShapes(request));\n } catch (err) {\n const status = err instanceof AdminRequestError ? 400 : 500;\n res.status(status).json({ error: err instanceof Error ? err.message : String(err) });\n }\n });\n}\n"]}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* store expands it under this setting.
|
|
13
13
|
*
|
|
14
14
|
* The syncs write into the default dataset only. {@link syncPinWarning} names the shapes whose
|
|
15
|
-
* routing sends their reads elsewhere.
|
|
15
|
+
* routing sends their reads elsewhere, when the boot runs those syncs.
|
|
16
16
|
*/
|
|
17
17
|
import { type GraphTargets } from '@_linked/core/utils/graphTargets';
|
|
18
18
|
/**
|
|
@@ -30,13 +30,21 @@ export declare const SHAPE_SHAPE_CLASSES: readonly string[];
|
|
|
30
30
|
* and `@_linked/owl` register): what the ontology sync writes and the hierarchy reads.
|
|
31
31
|
*/
|
|
32
32
|
export declare const ONTOLOGY_TERM_CLASSES: readonly string[];
|
|
33
|
+
/** Which boot syncs run: only the shapes a running sync writes are worth a warning. */
|
|
34
|
+
export interface BootSyncs {
|
|
35
|
+
/** The shape sync runs (`shapeSyncOn`): it writes the shape shapes. */
|
|
36
|
+
shapeSync: boolean;
|
|
37
|
+
/** The ontology sync runs (`bootSyncsOntologies`): it writes the ontology term shapes. */
|
|
38
|
+
ontologySync: boolean;
|
|
39
|
+
}
|
|
33
40
|
/**
|
|
34
|
-
* One warning when routing pins a shape
|
|
35
|
-
* `PropertyShape
|
|
36
|
-
* dataset other than the default one: the syncs
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
+
* One warning when routing pins a shape a boot sync that runs writes (the shape shapes
|
|
42
|
+
* `NodeShape` / `PropertyShape` for the shape sync, an ontology term shape such as `rdfs:Class` /
|
|
43
|
+
* `owl:ObjectProperty` for the ontology sync) to a dataset other than the default one: the syncs
|
|
44
|
+
* write into the default dataset only, so reads of those shapes on the pinned dataset miss what
|
|
45
|
+
* they wrote. Names each such shape and its dataset; a sub-shape counts under the target class it
|
|
46
|
+
* declares, or its parent's when it declares none.
|
|
47
|
+
* Undefined when there is nothing to warn about (no sync runs, or no default dataset). Reads the
|
|
48
|
+
* registered shapes outside any shape catalog (run it at boot, never inside `withShapeCatalog`).
|
|
41
49
|
*/
|
|
42
|
-
export declare function syncPinWarning(): string | undefined;
|
|
50
|
+
export declare function syncPinWarning(syncs: BootSyncs): string | undefined;
|