Repository navigation
refactor: compose plugin options outside the core - #646
Conversation
`src/config/args.ts` imported every plugin family's option set, so the core depended on the plugins it is meant to host. It now declares the core options only. `src/bin/options.ts` is the composition root: it appends the families' declarations in the previous order, and `DM` takes that template as a constructor parameter. Plugins read the parser's keys from the server instead of importing `configArgs`. `priority.json` moves to `src/config/`, the auth common options move to `auth.options.ts`, and `configuredSchemaFiles` takes the template it needs instead of importing it. Refs #397.
guimard
left a comment
There was a problem hiding this comment.
Review of 1d80165, checked out in a separate worktree: npm run check is green, and the touched suites plus the authzPerBranch/authzCombine ones give 257 passing against the Docker OpenLDAP.
The composition itself is faithful: the options snapshot is unchanged, withConfig views get optionKeys/configTemplate through Object.assign, and dist/bin/options.js is built for bin/cleanup-external-users.mjs. One move goes against the goal of the PR, though.
To fix
--ldap-concurrency moved from the core into jamesOptions (src/plugins/twake/james.options.ts:13). It sat under a "James plugin" comment in args.ts, but its only reader is the core: src/lib/ldap/state.ts:48 sizes the global LDAP pLimit with it. Before this PR it was part of the core template; now only the composed template knows it. With the core template alone — what new DM(options) now allows, and what @ldap-rest/core will ship — on this head:
new ConfigParser(coreArgs), --ldap-concurrency 5
→ config.ldap_concurrency === "5" (string, reported as unknown option)
→ pLimit("5") throws "Expected `concurrency` to be a number from 1 and up"
new ConfigParser(coreArgs), DM_LDAP_CONCURRENCY=5
→ config.ldap_concurrency === undefined (silently ignored)
Please keep it in src/config/args.ts.
Worth a decision
Options the core reads are now declared by the auth family. authzOptions (src/plugins/auth/auth.options.ts:28) carries authz_combine and authz_for, which DM itself reads at startup through src/lib/authz/composition.ts:339 and :75/:283. authz_unresolved_user, authz_transit_* and authz_filter_attached_entries (now in perBranchOptions) are read by src/lib/authz/base.ts, and auth_path_prefix by src/lib/auth/base.ts. The composed template hides it today, but after the split a core-only server, or a third-party authz plugin extending AuthzBase without @ldap-rest/plugin-auth, gets these silently ignored (same probe: DM_AUTHZ_COMBINE=true → undefined). If lib/auth and lib/authz stay in core, as the module map says, their options belong in core too; if they are meant to follow the auth family, the plan should say so.
DM still imports every family at runtime. constructor(options: ConfigTemplate = configArgs) with import configArgs from './options' (src/bin/index.ts:76, :348) keeps src/bin/index.ts → src/bin/options.ts → src/plugins/**/*.options.ts. The default is a sensible transition (176 test files and three bin/*.mjs scripts call new DM()), but docs/development/modularization-plan.md:19 now says "The core no longer imports plugin code", while the plan's own rule is "runtime option imports leave the core". Please name this edge as what remains — dropping the default, with the callers passing the template, when options.ts moves to the umbrella — rather than declaring the core decoupled.
Nits
docs/usage/plugins/auth/authz-per-route.md:17still sayssrc/plugins/priority.json.docs/architecture/adr/0003-authorization-composition.md:59points tosrc/config/args.ts; the composition options now live insrc/plugins/auth/auth.options.ts.src/bin/options.ts:144-155: the authzDynamic comment describesauthz_dynamic_bypassbut stays in the composition root, which moves to the umbrella. The authz comments moved next to their declarations in this PR; this one could move aboveauthz_dynamic_bypassinauth.options.tstoo.
— Claude
`--ldap-concurrency`, `--auth-path-prefix` and the `authz_*` options that `DM`, `lib/auth` and `lib/authz` read were moved to plugin option sets, so a core-only server stringified or ignored them. Keep them in `src/config/args.ts`; only `authz_scope_source`, read by the authzScope plugin, stays with the auth family. Also name the remaining runtime composition edge in the plan, fix the stale priority path, and move the authzDynamic comment next to its declaration. Refs #397.
|
Fixed in a2ef44f:
Re-checked: core-only |
| `options()`. | ||
|
|
||
| Plugins outside the repository cannot add to `args.ts` yet, but they can | ||
| Plugins outside the repository cannot add to `src/bin/options.ts` yet, but they can |
There was a problem hiding this comment.
This line and the one further down (Import the declaration in src/bin/options.ts ..., 83 chars) pass the 80-column wrap the rest of the file keeps. Since the paragraph was reflowed anyway by replacing args.ts, re-wrapping it here would keep the diff consistent. Cosmetic only.
— Deepseek
| import { twakeLifecycleOptions } from '../plugins/twake/twakeLifecycle.options'; | ||
| import { twakeSpacesOptions } from '../plugins/twake/twakeSpaces.options'; | ||
| import { twakeTombstoneOptions } from '../plugins/twake/twakeTombstone.options'; | ||
|
|
There was a problem hiding this comment.
Typo carried over from args.ts: should be Backchannel.
Worth noting for the record: --authz-scope-source now sits at position 229 of 290 instead of 226. That is harmless — the parser is name-based and findEntry sorts neither — and the runtime value is identical for unset, empty and set DM_AUTHZ_SCOPE_SOURCE. But it is now a defineOptions entry rather than a hand-written one, so if the composed order is meant to be stable (e.g. for the future umbrella or options.snapshot.json diffs), a short comment here stating the order is normative would make an accidental move visible.
— Deepseek
guimard
left a comment
There was a problem hiding this comment.
Review: approved
I checked out a2ef44f4 in a separate worktree and ran the checks locally (full mocha suite with Docker, check:ts, eslint, prettier, the API and options snapshots, check-openapi, hooks-doc, check-comments, scripts/contracts.ts --base <merge-base>): all green, 3800 passing / 0 failing.
Core correctness
- The composed template is equivalent to the old one. I compared
src/bin/options.tsin the head withsrc/config/args.tsin the base on the same checkout: 290 entries on both sides, no option added or removed, no duplicate, and the only value differences are the checkout path in the--schemas-path/--group-schema/--static-pathdefaults.--authz-scope-sourcegainsstring, null, null, false, which changes nothing: I also checked the parsed value forDM_AUTHZ_SCOPE_SOURCEunset, empty and set, and it is identical before and after. The entry order changed only for that option (226th → 229th), which is safe sinceConfigParser.findEntrymatches by name; see the note onsrc/bin/options.ts. - The options the core really reads are kept in
args.ts, and each claim in the body holds:authz_unresolved_userandauthz_identityare read bylib/authz/base.tsandlib/auth/base.ts,authz_for/authz_combinebylib/authz/composition.ts,auth_path_prefixbylib/auth/base.ts, andldap_concurrencybylib/ldap/state.ts. Onlyauthz_scope_sourcemoved toauth.options.ts, which is the one read solely by the authzScope plugin. - The new seams behave as advertised. With
new DM(template)the parser only knows the injected keys (optionKeyshas the injected key and notauth_token),configTemplateis what the unknown-options report diffs against, and a core-only template reports an undeclaredDM_*variable the same way.withConfigcopiesconfigTemplate/optionKeysby reference/instance as intended;staticandDmPlugin.options()read them fromthis.server, so plugin views keep working. - The remaining importers are type-only (
import type { Config },BranchPermissions, ...), so the core's runtime dependency on plugin option sets is indeed gone. The one runtime edge still there —DM's default constructor argument (src/bin/index.ts:import configArgs from './options') — is documented inmodularization-plan.md. That default is also what keeps the manynew DM()callers (tests,bin/*.mjs) working, which is a good call for this step.
The priority.json move and every reference to it (code, tests, docs, ADRs, module map) are updated; scripts/buildDockerfile.ts regenerates the Dockerfile byte-for-byte, dist/bin/options.js is emitted by Rollup as expected, and the contracts job reports no published-contract change. I agree the CHANGELOG doesn't need an entry.
The only remarks are two nits, not blocking (they are posted inline): a slightly stale line wrapping in docs/plugin-development/options.md, and the Backchannel typo in a comment in src/bin/options.ts.
One optional suggestion, for a later step rather than this PR: DmPlugin.options() and configuredSchemaFiles() are exactly the authority the package split will use, and no test drives them through an injected non-default template today. A small test constructing new DM(customTemplate) and checking optionKeys and the unknown-option report would pin the behaviour the split depends on.
Note on the GitHub verdict: the API account running this review is the author of the PR (
guimard), and GitHub refuses self-approval. This body is an explicit approval; please treat it asapproveand merge once a human code owner from.github/CODEOWNERShas reviewed, as the PR body already requires.
Signed-off-by: Deepseek
`DM`'s constructor names `ConfigTemplate` in the published API reports, but no entry point exports it, so an external caller cannot pass a template today. Phase 2 exports `ConfigTemplate` and `ConfigEntry` with the umbrella, when it starts passing the template itself. Refs #397.
|
Added a docs-only commit a7dde37 after the DeepSeek review: the plan now records that phase 2 must export |
What and why
src/config/args.tsno longer imports any plugin option set: the core declares the options it reads.src/bin/options.tscomposes them with every plugin family in the previous order, andDMaccepts that template as a constructor parameter. This is the last prerequisite of the package split tracked in #397.priority.jsonmoves tosrc/config/, the options the core reads stay inargs.ts(onlyauthz_scope_source, read by the authzScope plugin, moved toauth.options.ts), andconfiguredSchemaFilestakes the option template it needs instead of importing it.Reviews performed
--ldap-concurrencyand theauthz_*/auth_path_prefixoptions read by the core had moved into plugin option sets, so a core-only template stringified or ignored them; fixed in a2ef44f. Also flagged thatDM's default argument still imports the umbrella composition, now named in the plan.args.ts, 3800 passing, all gates green, no published-contract change. Two non-blocking nits..github/CODEOWNERSChecklist
test:devsuite passes.npm run checkpassesCHANGELOG.mdupdated when administrators see the change — not needed: internal refactor with no administrator-visible change.