@_linked/server 3.4.0 → 3.5.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 +4 -1
- package/lib/esm/shapes/LinkedServer.js +5 -2
- package/lib/esm/shapes/LinkedServer.js.map +1 -1
- package/lib/esm/shapes/quadstores/RemoteDataset.js +10 -2
- package/lib/esm/shapes/quadstores/RemoteDataset.js.map +1 -1
- 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 +1 -1
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { packageName } from '../../package.js';
|
|
2
2
|
import { brandLinkedError } from '@_linked/core/utils/linkedError';
|
|
3
3
|
import { Server } from '@_linked/server-utils/utils/Server';
|
|
4
|
-
import { getCallContext } from '@_linked/server-utils/utils/CallContext';
|
|
5
4
|
/**
|
|
6
5
|
* The header the url form sends with a query made while a signed-in user's HTTP request is
|
|
7
6
|
* being answered: that user's WebID. The data server decides what to do with it; the bearer
|
|
8
7
|
* token still decides which data the store reaches.
|
|
9
8
|
*/
|
|
10
9
|
export const END_USER_HEADER = 'x-linked-end-user';
|
|
10
|
+
// The store `@_linked/server-utils/utils/CallContext` keeps the call context in, shared on
|
|
11
|
+
// `globalThis` under this key. Read here instead of importing that module: it imports
|
|
12
|
+
// `node:async_hooks`, and this store also runs in the browser (the no-url form). In a browser,
|
|
13
|
+
// and in a server before any call ran, the key is unset and there is no context.
|
|
14
|
+
const CALL_CONTEXT_KEY = Symbol.for('@_linked/server-utils:callContext');
|
|
15
|
+
function currentCallContext() {
|
|
16
|
+
const store = globalThis[CALL_CONTEXT_KEY];
|
|
17
|
+
return typeof (store === null || store === void 0 ? void 0 : store.getStore) === 'function' ? store.getStore() : undefined;
|
|
18
|
+
}
|
|
11
19
|
/**
|
|
12
20
|
* The WebID of the signed-in user behind the current call: the http context's
|
|
13
21
|
* `request.linkedAuth.userAccount.accountOf.id`. Undefined in a system context
|
|
@@ -15,7 +23,7 @@ export const END_USER_HEADER = 'x-linked-end-user';
|
|
|
15
23
|
*/
|
|
16
24
|
export function currentEndUserWebId() {
|
|
17
25
|
var _a, _b, _c, _d;
|
|
18
|
-
const ctx =
|
|
26
|
+
const ctx = currentCallContext();
|
|
19
27
|
if (!ctx || ctx.kind !== 'http')
|
|
20
28
|
return undefined;
|
|
21
29
|
const webId = (_d = (_c = (_b = (_a = ctx.request) === null || _a === void 0 ? void 0 : _a.linkedAuth) === null || _b === void 0 ? void 0 : _b.userAccount) === null || _c === void 0 ? void 0 : _c.accountOf) === null || _d === void 0 ? void 0 : _d.id;
|
|
@@ -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 { getCallContext } 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} from '@_linked/core/queries/IntermediateRepresentation';\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/**\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 = getCallContext();\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\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\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 selectQuery(query: SelectQuery | CountQuery): Promise<SelectResult | number> {\n return this.call('selectQuery', query.toJSON());\n }\n\n askQuery(query: AskQuery): Promise<boolean> {\n return this.call('askQuery', 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 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} from '@_linked/core/queries/IntermediateRepresentation';\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\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\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 selectQuery(query: SelectQuery | CountQuery): Promise<SelectResult | number> {\n return this.call('selectQuery', query.toJSON());\n }\n\n askQuery(query: AskQuery): Promise<boolean> {\n return this.call('askQuery', 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 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,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.5.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
|
|