@_linked/server 3.5.0 → 3.6.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.
@@ -0,0 +1,29 @@
1
+ import type express from 'express';
2
+ /** What one shape sync did. */
3
+ export interface ShapeSyncResult {
4
+ /** Shapes written: those whose stored hash differed from the registered shape. */
5
+ written: number;
6
+ /** The orphan policy the sync ran with (see utils/syncShapes). */
7
+ orphanScope?: 'all' | 'none';
8
+ /** Set when no sync ran: shape sync is turned off, or there is no default dataset. */
9
+ skipped?: 'disabled' | 'no-default-dataset';
10
+ }
11
+ /** Thrown by a handler for a request the channel refuses with 400. */
12
+ export declare class AdminRequestError extends Error {
13
+ }
14
+ export interface AdminChannelHandlers {
15
+ /** Exit the process (the host restarts it). Called after the 202 is sent. */
16
+ exit: () => void;
17
+ /** Reload `packages` (when given) and sync the registered shapes into app data. */
18
+ syncShapes: (request: {
19
+ packages: string[];
20
+ }) => Promise<ShapeSyncResult>;
21
+ }
22
+ /** Compare the provided secret with the configured one in constant time. */
23
+ export declare function adminSecretMatches(secret: string | undefined, provided: string | undefined): boolean;
24
+ /** The `packages` of a sync-shapes body: a list of package names, or none. */
25
+ export declare function parseSyncShapesBody(body: unknown): {
26
+ packages: string[];
27
+ };
28
+ /** Mount the control channel's routes on `app`. */
29
+ export declare function registerAdminRoutes(app: express.Express, secret: string | undefined, handlers: AdminChannelHandlers): void;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The host→app control channel: `/admin/*` routes a host (Create Now) uses on an
3
+ * app process it started. Present only when the process was started with a
4
+ * shared secret in `CN_APP_ADMIN_SECRET`; without it every route answers 404, so
5
+ * a standalone app has no control channel. Every request carries the secret in
6
+ * the `x-cn-admin-secret` header.
7
+ *
8
+ * - `GET /admin/health` → 200 `{status: 'ok'}` once the server listens (the boot
9
+ * shape sync has finished by then).
10
+ * - `POST /admin/restart` → 202, then the process exits; the host starts it again.
11
+ * - `POST /admin/sync-shapes` → re-syncs the app's shapes into its app data, as
12
+ * the boot does, and answers 200 with {@link ShapeSyncResult}. Body (optional):
13
+ * `{packages: string[]}`, packages whose source changed on disk: in dev they are
14
+ * reloaded first, so the sync sees shapes added or changed since the boot. A
15
+ * failed sync answers 500 `{error}`; an unknown package 400 `{error}`.
16
+ */
17
+ import { timingSafeEqual } from 'crypto';
18
+ /** Thrown by a handler for a request the channel refuses with 400. */
19
+ export class AdminRequestError extends Error {
20
+ }
21
+ /** Compare the provided secret with the configured one in constant time. */
22
+ export function adminSecretMatches(secret, provided) {
23
+ if (!secret || !provided)
24
+ return false;
25
+ const a = Buffer.from(provided);
26
+ const b = Buffer.from(secret);
27
+ return a.length === b.length && timingSafeEqual(a, b);
28
+ }
29
+ /** The `packages` of a sync-shapes body: a list of package names, or none. */
30
+ export function parseSyncShapesBody(body) {
31
+ if (body === undefined || body === null || body === '')
32
+ return { packages: [] };
33
+ if (typeof body !== 'object') {
34
+ throw new AdminRequestError('the body must be a JSON object');
35
+ }
36
+ const packages = body.packages;
37
+ if (packages === undefined)
38
+ return { packages: [] };
39
+ if (!Array.isArray(packages) || packages.some((p) => typeof p !== 'string' || !p)) {
40
+ throw new AdminRequestError('packages must be a list of package names');
41
+ }
42
+ return { packages: [...new Set(packages)] };
43
+ }
44
+ /** Mount the control channel's routes on `app`. */
45
+ export function registerAdminRoutes(app, secret, handlers) {
46
+ const requireAdmin = (req, res) => {
47
+ if (!secret) {
48
+ res.sendStatus(404); // no control channel in this mode
49
+ return false;
50
+ }
51
+ if (!adminSecretMatches(secret, req.get('x-cn-admin-secret'))) {
52
+ res.sendStatus(401);
53
+ return false;
54
+ }
55
+ return true;
56
+ };
57
+ app.get('/admin/health', (req, res) => {
58
+ if (!requireAdmin(req, res))
59
+ return;
60
+ res.status(200).json({ status: 'ok' });
61
+ });
62
+ app.post('/admin/restart', (req, res) => {
63
+ if (!requireAdmin(req, res))
64
+ return;
65
+ res.sendStatus(202);
66
+ setTimeout(handlers.exit, 50);
67
+ });
68
+ app.post('/admin/sync-shapes', async (req, res) => {
69
+ if (!requireAdmin(req, res))
70
+ return;
71
+ let request;
72
+ try {
73
+ request = parseSyncShapesBody(req.body);
74
+ }
75
+ catch (err) {
76
+ res.status(400).json({ error: err.message });
77
+ return;
78
+ }
79
+ try {
80
+ res.status(200).json(await handlers.syncShapes(request));
81
+ }
82
+ catch (err) {
83
+ const status = err instanceof AdminRequestError ? 400 : 500;
84
+ res.status(status).json({ error: err instanceof Error ? err.message : String(err) });
85
+ }
86
+ });
87
+ }
88
+ //# sourceMappingURL=adminChannel.js.map
@@ -0,0 +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 (Create Now) uses on an\n * app process it started. Present only when the process was started with a\n * shared secret in `CN_APP_ADMIN_SECRET`; without it every route answers 404, so\n * a standalone app has no control channel. Every request carries the secret in\n * the `x-cn-admin-secret` header.\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 shapes into its app data, as\n * the boot does, and answers 200 with {@link ShapeSyncResult}. 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. A\n * failed sync answers 500 `{error}`; an unknown package 400 `{error}`.\n */\nimport { timingSafeEqual } from 'crypto';\nimport type express from 'express';\n\n/** What one shape sync did. */\nexport interface ShapeSyncResult {\n /** Shapes written: those whose stored hash differed from the registered shape. */\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\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\n/** Compare the provided secret with the configured one in constant time. */\nexport function adminSecretMatches(\n secret: string | undefined,\n provided: string | undefined\n): boolean {\n if (!secret || !provided) return false;\n const a = Buffer.from(provided);\n const b = Buffer.from(secret);\n return a.length === b.length && timingSafeEqual(a, b);\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 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('x-cn-admin-secret'))) {\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.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"]}
@@ -0,0 +1,12 @@
1
+ /** The subset of Vite's (back-compat) module graph this module uses. */
2
+ export interface SourceModuleGraph {
3
+ idToModuleMap: Map<string, {
4
+ file?: string | null;
5
+ }>;
6
+ onFileChange(file: string): void;
7
+ }
8
+ /**
9
+ * Invalidate every module whose file lies under one of `roots` and outside any
10
+ * `node_modules` directory. Returns the files it invalidated.
11
+ */
12
+ export declare function invalidateSourceUnder(graph: SourceModuleGraph, roots: string[]): string[];
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Mark a package's source modules stale in Vite's module graph, so the next
3
+ * `ssrLoadModule` of the package reads them from disk again.
4
+ *
5
+ * Vite's watcher does this too when a file changes, but asynchronously: a host
6
+ * that has just written a file and asks the app to re-sync its shapes cannot rely
7
+ * on the watcher having seen it. Invalidating a module also invalidates its
8
+ * importers, so a changed shapes barrel reaches the package entry that imports it,
9
+ * and a new shape module the barrel now imports is loaded with it.
10
+ */
11
+ import * as path from 'path';
12
+ /**
13
+ * Invalidate every module whose file lies under one of `roots` and outside any
14
+ * `node_modules` directory. Returns the files it invalidated.
15
+ */
16
+ export function invalidateSourceUnder(graph, roots) {
17
+ const prefixes = roots.map((r) => path.resolve(r) + path.sep);
18
+ const files = new Set();
19
+ for (const mod of graph.idToModuleMap.values()) {
20
+ const file = mod === null || mod === void 0 ? void 0 : mod.file;
21
+ if (!file)
22
+ continue;
23
+ if (file.split(path.sep).includes('node_modules'))
24
+ continue;
25
+ if (prefixes.some((p) => file.startsWith(p)))
26
+ files.add(file);
27
+ }
28
+ for (const file of files)
29
+ graph.onFileChange(file);
30
+ return [...files];
31
+ }
32
+ //# sourceMappingURL=reloadSource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reloadSource.js","sourceRoot":"","sources":["../../../src/utils/reloadSource.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAQ7B;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAAwB,EACxB,KAAe;IAEf,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IAC9D,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC;QAC/C,MAAM,IAAI,GAAG,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,IAAI,CAAC;QACvB,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,cAAc,CAAC;YAAE,SAAS;QAC5D,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChE,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACnD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC;AACpB,CAAC","sourcesContent":["/**\n * Mark a package's source modules stale in Vite's module graph, so the next\n * `ssrLoadModule` of the package reads them from disk again.\n *\n * Vite's watcher does this too when a file changes, but asynchronously: a host\n * that has just written a file and asks the app to re-sync its shapes cannot rely\n * on the watcher having seen it. Invalidating a module also invalidates its\n * importers, so a changed shapes barrel reaches the package entry that imports it,\n * and a new shape module the barrel now imports is loaded with it.\n */\nimport * as path from 'path';\n\n/** The subset of Vite's (back-compat) module graph this module uses. */\nexport interface SourceModuleGraph {\n idToModuleMap: Map<string, { file?: string | null }>;\n onFileChange(file: string): void;\n}\n\n/**\n * Invalidate every module whose file lies under one of `roots` and outside any\n * `node_modules` directory. Returns the files it invalidated.\n */\nexport function invalidateSourceUnder(\n graph: SourceModuleGraph,\n roots: string[]\n): string[] {\n const prefixes = roots.map((r) => path.resolve(r) + path.sep);\n const files = new Set<string>();\n for (const mod of graph.idToModuleMap.values()) {\n const file = mod?.file;\n if (!file) continue;\n if (file.split(path.sep).includes('node_modules')) continue;\n if (prefixes.some((p) => file.startsWith(p))) files.add(file);\n }\n for (const file of files) graph.onFileChange(file);\n return [...files];\n}\n"]}
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/linked-fw/server.git"
12
12
  },
13
- "version": "3.5.0",
13
+ "version": "3.6.0",
14
14
  "packageManager": "npm@12.2.0",
15
15
  "linkedPackage": true,
16
16
  "type": "module",
package/readme.md CHANGED
@@ -313,6 +313,15 @@ semantics are under [Error semantics](#error-semantics). `token` is ignored.
313
313
  code outside any call send no such header. The token still decides which data the store
314
314
  reaches; the data server decides what to do with the end user. `forwardEndUser: false` (or the
315
315
  string `"false"`) turns it off. No other headers are sent.
316
+
317
+ > **Warning: on by default.** Every query of a signed-in user's request sends that user's WebID
318
+ > to the data server at `url`. Point `url` only at a server you would give your users'
319
+ > identities to (for an app CN hosts, CN's gateway). To send none, set it off in the dataset's
320
+ > config:
321
+ >
322
+ > ```json
323
+ > "config": { "url": "${DATA_SERVER_URL:-}", "token": "${DATA_SERVER_TOKEN:-}", "forwardEndUser": false }
324
+ > ```
316
325
  - Trailing slashes are trimmed from `url`. A `url` that is present but empty (an unset
317
326
  `${DATA_SERVER_URL:-}`) never falls back to the no-url form: `init()` and every method throw
318
327
  `RemoteDatasetUnreachableError` with `url: ''`.