node_lookup is an extension that adds one authenticated CFMS action which resolves a human-readable
absolute node path to its database ID chain. A result is returned only when the
caller can read every node in the path, including the root and all ancestors.
Download either node_lookup-X.Y.Z.tar.gz or node_lookup-X.Y.Z.zip from the
corresponding GitHub Release.
You may verify the downloaded archive with the accompanying SHA256SUMS.txt:
sha256sum --check --ignore-missing SHA256SUMS.txtInstall the ZIP with the server's maintenance command, run from the server tree:
maintain extension install /absolute/path/node_lookup-X.Y.Z.zip --sha256 <digest>
maintain extension enable node_lookupUse maintain extension upgrade instead of install for an existing installation.
Restart the server after enabling or upgrading the extension. The ZIP contains
manifest.toml and _extension.py directly at its root.
Alternatively, extract the tar.gz archive, which includes a node_lookup/
directory, directly into the server extension catalog. For example, on Linux:
tar -xzf node_lookup-X.Y.Z.tar.gz \
-C cfms_on_websocket/src/include/extensionsThe resulting manifest must be located at:
cfms_on_websocket/src/include/extensions/node_lookup/manifest.toml
When installing directly from source, create that node_lookup directory, copy
the contents of this repository's src/ directory into it, and then copy
manifest.toml into the same directory. README.md and LICENSE may also be
copied for reference.
Enable it in config.toml and restart the server:
[extensions]
enabled = ["node_lookup"]The server advertises the node_lookup extension flag while the extension is
enabled. The action is not added to the lockdown whitelist.
Send an authenticated request with a replay-protection nonce and timestamp:
{
"action": "node_lookup.node.resolve",
"token": "<token>",
"nonce": "<at-least-16-characters>",
"timestamp": 1786200000,
"data": {
"path": "/projects/cfms_on_websocket"
}
}Use a current timestamp and a fresh nonce for each request. The server derives the
user UUID from the verified token; no envelope username is needed. The token must
include the GENERAL purpose. Missing or invalid tokens return 401; valid
tokens without the required purpose return 403.
A successful lookup includes the root ID:
{
"code": 200,
"data": {
"node_ids": ["/", "projects-id", "repository-id"]
},
"message": "Node path resolved successfully"
}The root path / resolves to {"node_ids": ["/"]}. Intermediate nodes must
be directories. The terminal node may be a directory or a document with an
active revision.
Paths are absolute UTF-8 strings. A slash separates encoded path segments; percent escapes are decoded exactly once after splitting the path:
- a slash inside a node name is written as
%2F; - a percent sign inside a node name is written as
%25; - unescaped Unicode and spaces are accepted;
- relative paths, empty segments, trailing slashes,
.and..segments, NUL, malformed escapes, and invalid UTF-8 are rejected; - paths are limited to 4096 encoded characters, 128 segments, and 255 decoded characters per segment.
For example, /teams%2Fsecurity/100%25 addresses the two node names
teams/security and 100%. A literal node name %2F is written as %252F.
Authentication alone permits invoking the action; no search permission is
required. Existing read rules, inheritance, direct and group object access
entries, and user blocks are evaluated for every node. User and group grants
remain in separate principal namespaces, and group grants apply only through a
currently active membership. A direct grant on the terminal node never makes
hidden ancestors visible, and
super_list_directory is not treated as an implicit bypass.
Missing, deleted, inaccessible, structurally invalid, and unpublished document paths all produce the same response, without a visible prefix:
{
"code": 404,
"data": {},
"message": "Node path not found"
}Audit entries identify the caller by UUID (user_id). Successful entries target
only the terminal ID. Failure entries do not include the submitted path or
partial IDs. The handler uses the server's normal
request rate control with a default cost of 3 and does not cache authorization
results.
Document availability is checked only after the entire path passes authorization. The check uses a single database existence query for an active revision instead of loading or walking revision history, so a long history cannot amplify one lookup into a database query per revision.
For non-root paths, the resolver uses one recursive database query for the
complete path. Each recursive step joins through the server's existing active
parent_id + active_name index, so sibling count does not require a linear scan.
Access rules, object grants, and user blocks are then loaded in batches and evaluated
once over the returned chain.
Database round trips therefore remain constant as path depth grows; only the number of rows and in-process authorization checks grows linearly. The test suite enforces a fixed query budget at the maximum supported depth and verifies the SQLite index plan as well as SQLite, PostgreSQL, and MySQL query compilation. Results are intentionally not cached, because doing so could expose stale authorization or rename/move state.
Use the server's uv environment. Tests create only temporary SQLite databases
and never use the server's app.db:
$env:CFMS_SERVER_ROOT = "D:\projects\cfms_on_websocket"
uv run --no-sync --project $env:CFMS_SERVER_ROOT pytest tests -vCI and release validation use CFMS commit ref
v0.13.0 (0.13.0). Install that checkout's
development dependencies with uv sync --project <server-root> --dev --all-extras
before running tests locally.