feat(server): let a gateway list, show and run a package's files off its surface for the package's authors - #1261
Merged
Merged
Conversation
…sks, without making them queryable A root index.malloy hides every file it does not export: listModels leaves them out and the model GET answers 404, so a package's own authors cannot read its code through the API. includeOffSurface=true on those two routes lists every model file, marks each with onSurface, and returns a hidden file's sourceText. Queries, notebooks and dashboards stay held to the surface. Publisher does not decide who may ask; a gateway in front of it does. Signed-off-by: James Swirhun <james@credibledata.com>
…uery can reach Signed-off-by: James Swirhun <james@credibledata.com>
jswir
marked this pull request as ready for review
September 29, 2026 21:33
…rface on the query route The same option now lifts the surface for one query request, as queryableSources "all" does for every request, so an author can run a hidden file or source. #(authorize) and #(access_filter) still apply. Signed-off-by: James Swirhun <james@credibledata.com>
Sha-Bang
approved these changes
Sep 30, 2026
Sha-Bang
left a comment
Collaborator
There was a problem hiding this comment.
No way to use this to skip restricted mode, #(authorize), #(access_filter) or givens: the flag only clears the surface check, and every gate after it runs as before. Three things don't match what the description and api-doc promise, plus a few doc nits. None of them block. Details inline. One more: the legacy /projects/... routes in server-old.ts neither honor nor 400 includeOffSurface, so it is silently ignored there. Probably fine for a compat alias, but it deserves one line in the docs.
… keeps a lock's 403
Two query-route passes still read the package surface when the request
lifted it:
- The caller-join boundary pass refused a caller's own join to a hidden
source with 404, though `run: hidden` ran. It is now skipped under the
option, as it is under queryableSources "all".
- The lock passes that read aliases and caller joins turned a refused
lock on a hidden name into a 404 ("not queryable"), on a route that
had just admitted the file. They now keep the lock's 403.
The api-doc now says the query route lifts the surface for any model
path it is sent, dashboard and notebook files included, and names the
routes that ignore the option. It also says the model GET returns the
file's sourceText beyond what queryableSources "all" shows.
Signed-off-by: James Swirhun <james@credibledata.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A package with a root
index.malloyhides every file thatindex.malloydoes not export:listModelsleaves those files out,GET …/models/{path}answers 404 for them, and a query to them answers 404. So the package's own authors can't read or try their own code through the API. In Credible, a modeler opening a curated package sees onlyindex.malloy.This adds
includeOffSurface=trueto those three routes. For one request, it serves the package as if it had no surface, the wayqueryableSources: "all"does for every request.#(authorize)and#(access_filter)still apply: it lifts curation, never the lock.What changes
includeOffSurface=trueGET …/modelsonSurfaceboolean. Dashboard files stay on the dashboards route.GET …/models/{path}sourceTexteven when the text names a source the file does not publish. The compiled body is curated per file, as today.POST …/models/{path}/queryindex.malloydoesn't export.Without the option, nothing changes. Every listing entry now carries
onSurface:truefor everything in a package with no surface.A bad value is refused with 400 on all three routes (anything other than
trueorfalse), through the existingbooleanParamOr400.One definition. The option is a single shared
components/parametersentry.The query change.
Model.getQueryResultsgets anincludeOffSurfaceargument that treats the early surface check as cleared. The compiled backstop and the off-surface explanation hang off that check, so both are skipped for that request. Two other passes read the surface on their own, and the argument reaches them too:#(authorize)evaluation runs as before.What does not change
/projects/...aliases ignore the option.get_contextandexecute_querycall without it.queryableSources: "all"has.Who may send it
Publisher is unauthenticated, so it does not decide. A gateway in front of it does. Credible's router forwards the option only for a caller with
can_update_package(admins and modelers), and answers 403 to anyone else (ms2data/service#6609).On the model GET the option shows more than
queryableSources: "all"does. It returns the file'ssourceTexteven when the text names a source the file does not publish, so a gateway passing it hands over#(authorize)expressions,given:defaults, connection names and the raw SQL of hidden sources. That is the point for an author, and it is why the gateway limits the option to people who can edit the package.This is a plain query option rather than a shared-secret header like
x-publisher-bypass-authorize. The bypass skips the lock (#(authorize)), and this doesn't: the surface is curation ("curation hides, authorize denies"). A source locked with#(authorize) falseis still refused with the option, and a test pins it.How it was checked
model.controller.spec.ts):assertFileOnSurfaceandshowsFileTextare skipped and the text is returned.false, both checks still apply.explore_visibility.spec.ts), with a rootindex.malloyimportingbase.malloy:{index.malloy: true}. With the option it is{base.malloy: false, index.malloy: true}.run: base_sourceon the hidden file andrun: helper(a sourceindex.malloydoesn't export) both return rows. Without the option,helperthrowsNotQueryableError.helperruns. Without it, it throwsNotQueryableError.#(authorize) falsestill throwsAccessDeniedErrorwith the option, whether the query names it directly, through an alias (source: x is locked extend {}) or through a caller join.onSurface: trueeither way.tests/integration/index_convention/include_off_surface.integration.spec.ts, on theindex-convention-testfixture), 4 of 4 pass:yes,1andmaybe;sourceTextbyte-for-byte the file on disk;POST …/internal.malloy/queryanswers 404 by default and 200 with one row with the option.bun run build:server-only,dist/server.mjson a scratch server root), against the 23 index.malloy test packages from fix(server): make index.malloy the one list of what dashboards, notebooks and the model GET can read #1236. Each package is one surface shape: rootindex.malloy, layered re-exports, hidden joins,exploresin each form, dashboards, a notebook,queryableSources: "all", and a hidden#@ persistsource..malloyfile minus served dashboards.onSurfaceis true exactly for the default listing.sourceTextis byte-for-byte the file on disk.queryableSources: "all"already answers it without the option.onSurfaceand 400 checks and still passes fix(server): make index.malloy the one list of what dashboards, notebooks and the model GET can read #1236's 54.get_contextis byte-identical for 8 packages between a build without this change and one with the listing and model-GET part. The query-route part doesn't touch that path.config.spec/config.theme.specfailures happen on unmodifiedmainunder bun 1.4.v0.8.2, and requests went through the router. They behaved as above, and the workers stayed healthy.🤖 Generated with Claude Code