@_linked/server 3.4.1 → 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.
- package/CHANGELOG.md +12 -0
- package/lib/esm/shapes/LinkedServer.d.ts +55 -20
- package/lib/esm/shapes/LinkedServer.js +101 -73
- package/lib/esm/shapes/LinkedServer.js.map +1 -1
- package/lib/esm/utils/adminChannel.d.ts +29 -0
- package/lib/esm/utils/adminChannel.js +88 -0
- package/lib/esm/utils/adminChannel.js.map +1 -0
- package/lib/esm/utils/reloadSource.d.ts +12 -0
- package/lib/esm/utils/reloadSource.js +32 -0
- package/lib/esm/utils/reloadSource.js.map +1 -0
- package/lib/esm/utils/rpcExposure.d.ts +3 -1
- package/lib/esm/utils/rpcExposure.js +12 -6
- package/lib/esm/utils/rpcExposure.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +10 -1
|
@@ -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"]}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { type CallableLevel } from '@_linked/server-utils/utils/callable';
|
|
1
|
+
import { type CallableLevel as DeclaredLevel } from '@_linked/server-utils/utils/callable';
|
|
2
2
|
export type RpcExposureMode = 'warn' | 'enforce';
|
|
3
|
+
/** A level as the server reports it: the deprecated `'user'` reads `'signed-in'`. */
|
|
4
|
+
export type CallableLevel = Exclude<DeclaredLevel, 'user'>;
|
|
3
5
|
export type CallableResolution = {
|
|
4
6
|
status: 'callable';
|
|
5
7
|
level: CallableLevel;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* The class that owns the method (the first prototype on the chain that has it)
|
|
11
11
|
* decides. When that class declares it, its own level applies. When it does not
|
|
12
12
|
* (an override of an inherited method), the method keeps the strictest level
|
|
13
|
-
* declared for it by any class up the chain (`'
|
|
13
|
+
* declared for it by any class up the chain (`'signed-in'` over `'public'`), so an
|
|
14
14
|
* override never loses a session requirement. Only when no class declares it is
|
|
15
15
|
* the method undeclared.
|
|
16
16
|
*
|
|
@@ -27,16 +27,22 @@
|
|
|
27
27
|
* it is also declared callable. Backend-to-backend calls still reach it.
|
|
28
28
|
* - **undeclared** — no class on the chain declares it. In `warn` mode it runs
|
|
29
29
|
* and is logged once; in `enforce` mode it is 501.
|
|
30
|
-
* - **callable** — declared (directly or inherited, see above). A `'
|
|
31
|
-
* method answers 401 without a session.
|
|
30
|
+
* - **callable** — declared (directly or inherited, see above). A `'signed-in'`
|
|
31
|
+
* method answers 401 without a session. `'user'`, the deprecated alias, is
|
|
32
|
+
* treated exactly like `'signed-in'` and reported as `'signed-in'`.
|
|
32
33
|
*
|
|
33
34
|
* Backend-to-backend calls (`Server.call` on the server) skip everything except
|
|
34
35
|
* the reserved names, internal methods included.
|
|
35
36
|
*/
|
|
36
37
|
import { BackendProvider } from '@_linked/server-utils/utils/BackendProvider';
|
|
37
38
|
import { ShapeProvider } from '@_linked/server-utils/utils/ShapeProvider';
|
|
38
|
-
import { getOwnCallableLevel, isDeclaredInternal, } from '@_linked/server-utils/utils/callable';
|
|
39
|
+
import { getOwnCallableLevel as getDeclaredLevel, isDeclaredInternal, } from '@_linked/server-utils/utils/callable';
|
|
39
40
|
import { sharedState } from '@_linked/core/utils/sharedState';
|
|
41
|
+
/** The level `cls` itself declares for `method`, with `'user'` read as `'signed-in'`. */
|
|
42
|
+
function getOwnCallableLevel(cls, method) {
|
|
43
|
+
const level = getDeclaredLevel(cls, method);
|
|
44
|
+
return level === 'user' ? 'signed-in' : level;
|
|
45
|
+
}
|
|
40
46
|
function ownNames(proto) {
|
|
41
47
|
return proto ? Object.getOwnPropertyNames(proto) : [];
|
|
42
48
|
}
|
|
@@ -124,7 +130,7 @@ export function resolveCallable(provider, method) {
|
|
|
124
130
|
}
|
|
125
131
|
return { status: 'undeclared', owner: start };
|
|
126
132
|
}
|
|
127
|
-
const STRICTNESS = { public: 0,
|
|
133
|
+
const STRICTNESS = { public: 0, 'signed-in': 1 };
|
|
128
134
|
/** The strictest level any class from `cls` up declares for `method`. */
|
|
129
135
|
function strictestDeclaredLevel(cls, method) {
|
|
130
136
|
let level;
|
|
@@ -197,7 +203,7 @@ export function warnUndeclaredCall(pkg, owner, method, hasSession) {
|
|
|
197
203
|
return;
|
|
198
204
|
warnedUndeclared.add(key);
|
|
199
205
|
console.warn(`[linked] undeclared RPC ${logSafe(pkg)} ${className}.${logSafe(method)} (session: ${hasSession ? 'yes' : 'no'}). ` +
|
|
200
|
-
`Declare it with @callable('public' | '
|
|
206
|
+
`Declare it with @callable('public' | 'signed-in') if the client calls it; ` +
|
|
201
207
|
`undeclared methods are refused (501) once rpcExposure is 'enforce'.`);
|
|
202
208
|
}
|
|
203
209
|
/** For tests: forget which undeclared calls were already logged. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpcExposure.js","sourceRoot":"","sources":["../../../src/utils/rpcExposure.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which provider methods a `/call/...` request may reach.\n *\n * A provider method is dispatched over HTTP when it is declared with\n * `@callable(level)` (or `declareCallable`) from\n * `@_linked/server-utils/utils/callable`. TypeScript `private`/`protected` do not\n * exist at runtime, so without a declaration every method of a provider would be\n * reachable, including everything it inherits.\n *\n * The class that owns the method (the first prototype on the chain that has it)\n * decides. When that class declares it, its own level applies. When it does not\n * (an override of an inherited method), the method keeps the strictest level\n * declared for it by any class up the chain (`'user'` over `'public'`), so an\n * override never loses a session requirement. Only when no class declares it is\n * the method undeclared.\n *\n * - **reserved** — never dispatched, in any mode: `Object.prototype` members,\n * every `BackendProvider`/`ShapeProvider` method (`initRequest`,\n * `registerRoute`, lifecycle hooks, ...), even when a subclass overrides it,\n * `dispose` (a lifecycle hook the server calls on reload, which the base\n * class does not define), `constructor`, `__proto__`, and anything that is not\n * a plain function (accessors, fields). Answered 501, exactly like a missing\n * method.\n * - **internal** — declared with `@internal()` / `declareInternal` by the\n * provider's class or one of its super classes (an app can declare it on a\n * class it imports). Never dispatched over HTTP, in any mode (501), even when\n * it is also declared callable. Backend-to-backend calls still reach it.\n * - **undeclared** — no class on the chain declares it. In `warn` mode it runs\n * and is logged once; in `enforce` mode it is 501.\n * - **callable** — declared (directly or inherited, see above). A `'user'`\n * method answers 401 without a session.\n *\n * Backend-to-backend calls (`Server.call` on the server) skip everything except\n * the reserved names, internal methods included.\n */\nimport { BackendProvider } from '@_linked/server-utils/utils/BackendProvider';\nimport { ShapeProvider } from '@_linked/server-utils/utils/ShapeProvider';\nimport {\n getOwnCallableLevel,\n isDeclaredInternal,\n type CallableLevel,\n} from '@_linked/server-utils/utils/callable';\nimport { sharedState } from '@_linked/core/utils/sharedState';\n\nexport type RpcExposureMode = 'warn' | 'enforce';\n\nexport type CallableResolution =\n | { status: 'callable'; level: CallableLevel; owner: Function; inherited?: boolean }\n | { status: 'undeclared'; owner: Function | undefined }\n | { status: 'internal'; owner: Function | undefined }\n | { status: 'reserved' }\n | { status: 'missing' };\n\nfunction ownNames(proto: object | undefined): string[] {\n return proto ? Object.getOwnPropertyNames(proto) : [];\n}\n\nlet reservedNames: Set<string> | undefined;\n\n/**\n * Names no RPC may dispatch to. Computed on first use (not at import), so the\n * base classes are fully defined, and including every method they have.\n */\nexport function getReservedNames(): ReadonlySet<string> {\n if (!reservedNames) {\n reservedNames = new Set([\n ...ownNames(Object.prototype),\n ...ownNames(BackendProvider.prototype),\n ...ownNames(ShapeProvider.prototype),\n // called by the server when it reloads providers; no base class defines it\n 'dispose',\n 'constructor',\n '__proto__',\n ]);\n }\n return reservedNames;\n}\n\nfunction isBaseClassPrototype(obj: object): boolean {\n return (\n obj === Object.prototype ||\n obj === BackendProvider.prototype ||\n obj === ShapeProvider.prototype\n );\n}\n\n/**\n * Decide whether `method` of `provider` may be dispatched.\n *\n * Walks the prototype chain with property descriptors only, so no getter ever\n * runs. The first object that owns the name decides: if it is a class\n * prototype, that class's own declaration; if it is the instance itself (a\n * method assigned onto the instance), there is no declaration.\n */\nexport function resolveCallable(provider: any, method: string): CallableResolution {\n if (typeof method !== 'string' || !method) return { status: 'missing' };\n if (provider == null || (typeof provider !== 'object' && typeof provider !== 'function')) {\n return { status: 'missing' };\n }\n const reserved = getReservedNames();\n let obj: any = provider;\n let descriptor: PropertyDescriptor | undefined;\n let holder: any;\n while (obj) {\n descriptor = Object.getOwnPropertyDescriptor(obj, method);\n if (descriptor) {\n holder = obj;\n break;\n }\n obj = Object.getPrototypeOf(obj);\n }\n if (!descriptor) {\n return reserved.has(method) ? { status: 'reserved' } : { status: 'missing' };\n }\n if (reserved.has(method) || isBaseClassPrototype(holder)) {\n return { status: 'reserved' };\n }\n if (!('value' in descriptor) || typeof descriptor.value !== 'function') {\n return { status: 'reserved' };\n }\n const holderCtor = Object.prototype.hasOwnProperty.call(holder, 'constructor')\n ? holder.constructor\n : undefined;\n const isClassPrototype =\n typeof holderCtor === 'function' && holderCtor.prototype === holder;\n // Internal on the provider's class or any super class wins over everything\n // else, a callable declaration included.\n const providerCtor = typeof provider === 'function' ? undefined : provider.constructor;\n if (\n (typeof providerCtor === 'function' && isDeclaredInternal(providerCtor, method)) ||\n (isClassPrototype && isDeclaredInternal(holderCtor, method))\n ) {\n return { status: 'internal', owner: isClassPrototype ? holderCtor : providerCtor };\n }\n if (isClassPrototype) {\n const own = getOwnCallableLevel(holderCtor, method);\n if (own) return { status: 'callable', level: own, owner: holderCtor };\n }\n // Not declared where it is defined (an override, or a method assigned onto\n // the instance): the strictest level declared up the class chain applies.\n const start = isClassPrototype ? holderCtor : providerCtor;\n const inherited = strictestDeclaredLevel(start, method);\n if (inherited) {\n return { status: 'callable', level: inherited, owner: start, inherited: true };\n }\n return { status: 'undeclared', owner: start };\n}\n\nconst STRICTNESS: Record<CallableLevel, number> = { public: 0, user: 1 };\n\n/** The strictest level any class from `cls` up declares for `method`. */\nfunction strictestDeclaredLevel(cls: unknown, method: string): CallableLevel | undefined {\n let level: CallableLevel | undefined;\n let c: any = cls;\n while (typeof c === 'function' && c !== Function.prototype) {\n const own = getOwnCallableLevel(c, method);\n if (own && (level === undefined || STRICTNESS[own] > STRICTNESS[level])) level = own;\n c = Object.getPrototypeOf(c);\n }\n return level;\n}\n\n/**\n * A client-supplied name made safe for a log line: control characters escaped\n * (no forged lines or terminal escapes), and cut to a bounded length.\n */\nexport function logSafe(value: unknown, max = 120): string {\n const text = String(value);\n const escaped = text.replace(/[\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029]/g, (ch) =>\n '\\\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0')\n );\n return escaped.length > max ? escaped.slice(0, max) + '…' : escaped;\n}\n\n/**\n * True when the provider's own class opts out of the generic `/call/<pkg>/...`\n * route with `static rpc = false`. Only the provider's own class counts.\n */\nexport function isGenericRpcDisabled(provider: any): boolean {\n const cls = provider?.constructor;\n return (\n typeof cls === 'function' &&\n Object.prototype.hasOwnProperty.call(cls, 'rpc') &&\n (cls as any).rpc === false\n );\n}\n\n/**\n * The configured mode, shared by every copy of this module in the process: `LinkedServer` sets\n * it from config through one copy, and the dispatcher on another copy must not fall back to\n * `'warn'`. See https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md.\n */\nconst rpcExposureState = sharedState('@_linked/server', 'rpcExposure', () => ({\n mode: undefined as RpcExposureMode | undefined,\n}));\n\nfunction parseMode(value: unknown): RpcExposureMode | undefined {\n return value === 'warn' || value === 'enforce' ? value : undefined;\n}\n\n/**\n * Set from the server's config (`server.rpcExposure`). An invalid value is\n * ignored with a warning, leaving the environment / default in place.\n */\nexport function setRpcExposureMode(mode: unknown): void {\n if (mode === undefined || mode === null) {\n rpcExposureState.mode = undefined;\n return;\n }\n const parsed = parseMode(mode);\n if (!parsed) {\n console.warn(\n `[linked] server.rpcExposure must be 'warn' or 'enforce', got ${JSON.stringify(mode)}; ignoring it`\n );\n return;\n }\n rpcExposureState.mode = parsed;\n}\n\n/** `server.rpcExposure`, else `LINKED_RPC_EXPOSURE`, else `'warn'`. */\nexport function getRpcExposureMode(): RpcExposureMode {\n return rpcExposureState.mode ?? parseMode(process.env.LINKED_RPC_EXPOSURE) ?? 'warn';\n}\n\nconst warnedUndeclared = new Set<string>();\n\n/** Log an undeclared call once per package, class and method. */\nexport function warnUndeclaredCall(\n pkg: string,\n owner: Function | undefined,\n method: string,\n hasSession: boolean\n): void {\n const className = owner?.name || 'anonymous provider';\n const key = `${pkg}\\u0000${className}\\u0000${method}`;\n if (warnedUndeclared.has(key)) return;\n warnedUndeclared.add(key);\n console.warn(\n `[linked] undeclared RPC ${logSafe(pkg)} ${className}.${logSafe(method)} (session: ${hasSession ? 'yes' : 'no'}). ` +\n `Declare it with @callable('public' | 'user') if the client calls it; ` +\n `undeclared methods are refused (501) once rpcExposure is 'enforce'.`\n );\n}\n\n/** For tests: forget which undeclared calls were already logged. */\nexport function resetUndeclaredWarnings(): void {\n warnedUndeclared.clear();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"rpcExposure.js","sourceRoot":"","sources":["../../../src/utils/rpcExposure.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which provider methods a `/call/...` request may reach.\n *\n * A provider method is dispatched over HTTP when it is declared with\n * `@callable(level)` (or `declareCallable`) from\n * `@_linked/server-utils/utils/callable`. TypeScript `private`/`protected` do not\n * exist at runtime, so without a declaration every method of a provider would be\n * reachable, including everything it inherits.\n *\n * The class that owns the method (the first prototype on the chain that has it)\n * decides. When that class declares it, its own level applies. When it does not\n * (an override of an inherited method), the method keeps the strictest level\n * declared for it by any class up the chain (`'signed-in'` over `'public'`), so an\n * override never loses a session requirement. Only when no class declares it is\n * the method undeclared.\n *\n * - **reserved** — never dispatched, in any mode: `Object.prototype` members,\n * every `BackendProvider`/`ShapeProvider` method (`initRequest`,\n * `registerRoute`, lifecycle hooks, ...), even when a subclass overrides it,\n * `dispose` (a lifecycle hook the server calls on reload, which the base\n * class does not define), `constructor`, `__proto__`, and anything that is not\n * a plain function (accessors, fields). Answered 501, exactly like a missing\n * method.\n * - **internal** — declared with `@internal()` / `declareInternal` by the\n * provider's class or one of its super classes (an app can declare it on a\n * class it imports). Never dispatched over HTTP, in any mode (501), even when\n * it is also declared callable. Backend-to-backend calls still reach it.\n * - **undeclared** — no class on the chain declares it. In `warn` mode it runs\n * and is logged once; in `enforce` mode it is 501.\n * - **callable** — declared (directly or inherited, see above). A `'signed-in'`\n * method answers 401 without a session. `'user'`, the deprecated alias, is\n * treated exactly like `'signed-in'` and reported as `'signed-in'`.\n *\n * Backend-to-backend calls (`Server.call` on the server) skip everything except\n * the reserved names, internal methods included.\n */\nimport { BackendProvider } from '@_linked/server-utils/utils/BackendProvider';\nimport { ShapeProvider } from '@_linked/server-utils/utils/ShapeProvider';\nimport {\n getOwnCallableLevel as getDeclaredLevel,\n isDeclaredInternal,\n type CallableLevel as DeclaredLevel,\n} from '@_linked/server-utils/utils/callable';\nimport { sharedState } from '@_linked/core/utils/sharedState';\n\nexport type RpcExposureMode = 'warn' | 'enforce';\n\n/** A level as the server reports it: the deprecated `'user'` reads `'signed-in'`. */\nexport type CallableLevel = Exclude<DeclaredLevel, 'user'>;\n\n/** The level `cls` itself declares for `method`, with `'user'` read as `'signed-in'`. */\nfunction getOwnCallableLevel(cls: Function, method: string): CallableLevel | undefined {\n const level = getDeclaredLevel(cls, method);\n return level === 'user' ? 'signed-in' : level;\n}\n\nexport type CallableResolution =\n | { status: 'callable'; level: CallableLevel; owner: Function; inherited?: boolean }\n | { status: 'undeclared'; owner: Function | undefined }\n | { status: 'internal'; owner: Function | undefined }\n | { status: 'reserved' }\n | { status: 'missing' };\n\nfunction ownNames(proto: object | undefined): string[] {\n return proto ? Object.getOwnPropertyNames(proto) : [];\n}\n\nlet reservedNames: Set<string> | undefined;\n\n/**\n * Names no RPC may dispatch to. Computed on first use (not at import), so the\n * base classes are fully defined, and including every method they have.\n */\nexport function getReservedNames(): ReadonlySet<string> {\n if (!reservedNames) {\n reservedNames = new Set([\n ...ownNames(Object.prototype),\n ...ownNames(BackendProvider.prototype),\n ...ownNames(ShapeProvider.prototype),\n // called by the server when it reloads providers; no base class defines it\n 'dispose',\n 'constructor',\n '__proto__',\n ]);\n }\n return reservedNames;\n}\n\nfunction isBaseClassPrototype(obj: object): boolean {\n return (\n obj === Object.prototype ||\n obj === BackendProvider.prototype ||\n obj === ShapeProvider.prototype\n );\n}\n\n/**\n * Decide whether `method` of `provider` may be dispatched.\n *\n * Walks the prototype chain with property descriptors only, so no getter ever\n * runs. The first object that owns the name decides: if it is a class\n * prototype, that class's own declaration; if it is the instance itself (a\n * method assigned onto the instance), there is no declaration.\n */\nexport function resolveCallable(provider: any, method: string): CallableResolution {\n if (typeof method !== 'string' || !method) return { status: 'missing' };\n if (provider == null || (typeof provider !== 'object' && typeof provider !== 'function')) {\n return { status: 'missing' };\n }\n const reserved = getReservedNames();\n let obj: any = provider;\n let descriptor: PropertyDescriptor | undefined;\n let holder: any;\n while (obj) {\n descriptor = Object.getOwnPropertyDescriptor(obj, method);\n if (descriptor) {\n holder = obj;\n break;\n }\n obj = Object.getPrototypeOf(obj);\n }\n if (!descriptor) {\n return reserved.has(method) ? { status: 'reserved' } : { status: 'missing' };\n }\n if (reserved.has(method) || isBaseClassPrototype(holder)) {\n return { status: 'reserved' };\n }\n if (!('value' in descriptor) || typeof descriptor.value !== 'function') {\n return { status: 'reserved' };\n }\n const holderCtor = Object.prototype.hasOwnProperty.call(holder, 'constructor')\n ? holder.constructor\n : undefined;\n const isClassPrototype =\n typeof holderCtor === 'function' && holderCtor.prototype === holder;\n // Internal on the provider's class or any super class wins over everything\n // else, a callable declaration included.\n const providerCtor = typeof provider === 'function' ? undefined : provider.constructor;\n if (\n (typeof providerCtor === 'function' && isDeclaredInternal(providerCtor, method)) ||\n (isClassPrototype && isDeclaredInternal(holderCtor, method))\n ) {\n return { status: 'internal', owner: isClassPrototype ? holderCtor : providerCtor };\n }\n if (isClassPrototype) {\n const own = getOwnCallableLevel(holderCtor, method);\n if (own) return { status: 'callable', level: own, owner: holderCtor };\n }\n // Not declared where it is defined (an override, or a method assigned onto\n // the instance): the strictest level declared up the class chain applies.\n const start = isClassPrototype ? holderCtor : providerCtor;\n const inherited = strictestDeclaredLevel(start, method);\n if (inherited) {\n return { status: 'callable', level: inherited, owner: start, inherited: true };\n }\n return { status: 'undeclared', owner: start };\n}\n\nconst STRICTNESS: Record<CallableLevel, number> = { public: 0, 'signed-in': 1 };\n\n/** The strictest level any class from `cls` up declares for `method`. */\nfunction strictestDeclaredLevel(cls: unknown, method: string): CallableLevel | undefined {\n let level: CallableLevel | undefined;\n let c: any = cls;\n while (typeof c === 'function' && c !== Function.prototype) {\n const own = getOwnCallableLevel(c, method);\n if (own && (level === undefined || STRICTNESS[own] > STRICTNESS[level])) level = own;\n c = Object.getPrototypeOf(c);\n }\n return level;\n}\n\n/**\n * A client-supplied name made safe for a log line: control characters escaped\n * (no forged lines or terminal escapes), and cut to a bounded length.\n */\nexport function logSafe(value: unknown, max = 120): string {\n const text = String(value);\n const escaped = text.replace(/[\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029]/g, (ch) =>\n '\\\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0')\n );\n return escaped.length > max ? escaped.slice(0, max) + '…' : escaped;\n}\n\n/**\n * True when the provider's own class opts out of the generic `/call/<pkg>/...`\n * route with `static rpc = false`. Only the provider's own class counts.\n */\nexport function isGenericRpcDisabled(provider: any): boolean {\n const cls = provider?.constructor;\n return (\n typeof cls === 'function' &&\n Object.prototype.hasOwnProperty.call(cls, 'rpc') &&\n (cls as any).rpc === false\n );\n}\n\n/**\n * The configured mode, shared by every copy of this module in the process: `LinkedServer` sets\n * it from config through one copy, and the dispatcher on another copy must not fall back to\n * `'warn'`. See https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md.\n */\nconst rpcExposureState = sharedState('@_linked/server', 'rpcExposure', () => ({\n mode: undefined as RpcExposureMode | undefined,\n}));\n\nfunction parseMode(value: unknown): RpcExposureMode | undefined {\n return value === 'warn' || value === 'enforce' ? value : undefined;\n}\n\n/**\n * Set from the server's config (`server.rpcExposure`). An invalid value is\n * ignored with a warning, leaving the environment / default in place.\n */\nexport function setRpcExposureMode(mode: unknown): void {\n if (mode === undefined || mode === null) {\n rpcExposureState.mode = undefined;\n return;\n }\n const parsed = parseMode(mode);\n if (!parsed) {\n console.warn(\n `[linked] server.rpcExposure must be 'warn' or 'enforce', got ${JSON.stringify(mode)}; ignoring it`\n );\n return;\n }\n rpcExposureState.mode = parsed;\n}\n\n/** `server.rpcExposure`, else `LINKED_RPC_EXPOSURE`, else `'warn'`. */\nexport function getRpcExposureMode(): RpcExposureMode {\n return rpcExposureState.mode ?? parseMode(process.env.LINKED_RPC_EXPOSURE) ?? 'warn';\n}\n\nconst warnedUndeclared = new Set<string>();\n\n/** Log an undeclared call once per package, class and method. */\nexport function warnUndeclaredCall(\n pkg: string,\n owner: Function | undefined,\n method: string,\n hasSession: boolean\n): void {\n const className = owner?.name || 'anonymous provider';\n const key = `${pkg}\\u0000${className}\\u0000${method}`;\n if (warnedUndeclared.has(key)) return;\n warnedUndeclared.add(key);\n console.warn(\n `[linked] undeclared RPC ${logSafe(pkg)} ${className}.${logSafe(method)} (session: ${hasSession ? 'yes' : 'no'}). ` +\n `Declare it with @callable('public' | 'signed-in') if the client calls it; ` +\n `undeclared methods are refused (501) once rpcExposure is 'enforce'.`\n );\n}\n\n/** For tests: forget which undeclared calls were already logged. */\nexport function resetUndeclaredWarnings(): void {\n warnedUndeclared.clear();\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.
|
|
13
|
+
"version": "3.6.0",
|
|
14
14
|
"packageManager": "npm@12.2.0",
|
|
15
15
|
"linkedPackage": true,
|
|
16
16
|
"type": "module",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@_linked/cli": "^1.45.5",
|
|
42
42
|
"@_linked/core": "^2.36.0",
|
|
43
|
-
"@_linked/server-utils": "^1.
|
|
43
|
+
"@_linked/server-utils": "^1.14.0",
|
|
44
44
|
"autoprefixer": "^10.4.18",
|
|
45
45
|
"compression": "^1.7.4",
|
|
46
46
|
"cors": "^2.8.5",
|
package/readme.md
CHANGED
|
@@ -202,7 +202,7 @@ Note that you can have **only one** generic backend provider per package.
|
|
|
202
202
|
|
|
203
203
|
### Which methods a client may call (`server.rpcExposure`)
|
|
204
204
|
|
|
205
|
-
A `/call/...` request reaches only provider methods declared with `@callable('public' | 'user')
|
|
205
|
+
A `/call/...` request reaches only provider methods declared with `@callable('public' | 'signed-in')` (`'user'` is a deprecated alias of `'signed-in'`, treated and reported exactly like it); `@internal()` methods are never reachable over HTTP. The declarations, what each level does, and the rule for methods that act for a signed-in user are documented in one place: [server-utils: Which provider methods a client may call](https://github.com/linked-fw/server-utils#which-provider-methods-a-client-may-call).
|
|
206
206
|
|
|
207
207
|
`server.rpcExposure` sets what happens to an undeclared method: `'warn'` (the default) runs it and logs it once, `'enforce'` answers `501`. Without the config key, the `LINKED_RPC_EXPOSURE` environment variable sets it.
|
|
208
208
|
|
|
@@ -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: ''`.
|