Skip to content

feat(#603): json_file setup fields — upload the key, do not transcribe it - #682

Merged
Weegy merged 1 commit into
mainfrom
feat/603-json-file-setup-field
Aug 13, 2026
Merged

feat(#603): json_file setup fields — upload the key, do not transcribe it#682
Weegy merged 1 commit into
mainfrom
feat/603-json-file-setup-field

Conversation

@Weegy

@Weegy Weegy commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

OM-17, structural fix. The reported near-miss: a tester typed their real Google account password into gw_sa_private_key. Nothing careless about it — the form asked them to hand-transcribe two values out of a service-account key file into an email field and a masked field stacked directly beneath it, which is the visual pattern of a login.

#599 made that mistake detectable (caution copy + pattern validation). Uploading the file removes the opportunity to make it. The tester proposed exactly this, and they were right: it is structural, and it removes a step rather than adding one.

The type is mirrored in six places, not three

The issue named three. Two more only surface when you follow the value, and a sixth lives in web-ui:

# Where What a missing member does
1 admin-v1.ts — the SetupFieldType union type error
2 manifestLoader.isSetupFieldType field dropped from the catalog
3 agentSpec's .strict() z.enum spec rejected outright
4 SUPPORTED_TYPES in installService.ts no error at allisSupportedType skips the field and the upload vanishes from the install wizard silently
5 InstallSetupField's shape what extractSetupSchema returns, i.e. what the new route reads
6 web-ui/app/_lib/storeTypes.ts renderer cannot match on it

All six carry json_file now, each commented with a pointer to the others.

Server-side extraction

middleware/src/plugins/setupJsonFile.ts. The client posts the file's text; the server parses, validates, explodes it into the keys named in extracts, and stores only those. The browser is never trusted to decide which bytes become gw_sa_private_key.

Guard rails, each pinned by a test:

  • the size cap runs on the raw text, before JSON.parse — the fixture is valid JSON on purpose, so the test would catch a cap that ran after parsing;
  • expect rejects the wrong file before any value is extracted — an OAuth client secret and a service-account key look alike at a glance;
  • a missing / empty / non-string value is a readable error, never a silently empty secret — that outcome would look like a successful setup and fail later, far from the cause;
  • Object.hasOwn on every path segment, so $.constructor cannot reach a prototype property;
  • the raw document is never returned, logged, or persisted.

Paths are a deliberate subset ($.a.b), not JSONPath: service-account files are flat, and the full grammar would add a dependency plus an evaluation surface for no case anyone has. An unsupported path fails loudly instead of matching nothing.

The write path is shared, not copied

POST /installed/:id/secrets/from-json extracts, then hands the derived values to the same applySetupValues that the typed PATCH …/secrets now uses.

The security claim of #603 is that an extracted value is treated exactly like a typed one — same pattern validation, same vault/config split, same reactivation, same response (key names only, never a value). Two implementations of that would be two implementations that can drift, and the drift would be invisible until a json_file field quietly skipped a check the typed path applies.

The extraction map comes from the manifest, never the request: a caller-supplied extracts would let anyone write any vault key from any file.

coerce() refuses a value submitted under a json_file key rather than ignoring it — silently dropping it would let a client believe it had stored a credential. The install form skips the field for the same reason, so both sides agree.

UI

The install wizard renders a file picker. Falling through to the text input would be worse than not shipping the type: it would ask the operator to paste a raw key into a field, which is the transcription step this removes.

The post-install CredentialsEditor omits json_file fields — every row there is a text input, and the derived fields are ordinary secrets listed as usual, so post-install editing still works. Offering the upload there too is a follow-up, not a gap in the flow.

The errorHelp guard earned its keep

The new codes were first built with a template (runtime.json_file_${failure.code}), which the coverage guard cannot see — they would have shipped with no operator copy while the suite stayed green.

Now a JSON_FILE_ERROR_CODES table holds literals, typed exhaustively over JsonFileFailureCode so a new failure kind cannot ship without a wire code, and the forwarder is registered in the guard. 11 new help entries per locale; the two spec-level ones say plainly that the fault is in the plugin, not the operator's file — that distinction is the difference between "try another file" and "report this".

(Amusingly, the guard then caught my own explanatory comment, because it cannot tell a comment from code. Reworded.)

Verification

  • test/setupJsonFile.test.ts: 12/12
  • related middleware suites (manifest loader, install service): 45/45
  • web-ui: 693/693
  • tsc --noEmit: clean on both sides
  • typecheck:test ratchet: 406 = baseline
  • eslint: 0 errors

TemplateInstantiateForm.test.tsx failed once in a full-suite run and passes both in isolation (22/22) and on a full-suite re-run (693/693) — the repo's known cross-file pollution, not this change.

Not included

  • The Google Workspace manifest itself (separate repo — see the linked issue).
  • Upload from the post-install credentials editor.

Closes #603


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…e it

OM-17, structural fix. The reported near-miss: a tester typed their real Google
account password into `gw_sa_private_key`. Nothing careless about it — the form
asked them to hand-transcribe two values out of a service-account key file into
an email field and a masked field stacked directly beneath it, which is the
visual pattern of a login. #599 made that mistake DETECTABLE (caution copy +
`pattern` validation). Uploading the file removes the opportunity to make it.
The tester proposed exactly this, and they were right: it is structural, and it
removes a step rather than adding one.

## The type is mirrored in SIX places, not three

The issue named three (`admin-v1.ts`'s union, `manifestLoader.isSetupFieldType`,
`agentSpec`'s `.strict()` z.enum). Two more only surface when you follow the
value: `SUPPORTED_TYPES` in `installService.ts` — where a missing member does
NOT error, `isSupportedType` just skips the field and the upload vanishes from
the install wizard with no diagnostic — and `InstallSetupField`'s own shape,
which is what `extractSetupSchema` returns and therefore what the new route
reads. A sixth lives in web-ui (`storeTypes.ts`). All six carry `json_file` now,
each with a comment naming the others.

## Server-side extraction

`src/plugins/setupJsonFile.ts` — the client posts the file's TEXT, the server
parses it, validates it, explodes it into the keys named in `extracts`, and
stores only those. The browser is never trusted to decide which bytes become
`gw_sa_private_key`.

Guard rails, each pinned by a test:
  - the size cap is applied to the RAW text, before `JSON.parse` runs;
  - `expect` rejects the wrong file BEFORE any value is extracted (an OAuth
    client secret and a service-account key look alike at a glance);
  - a missing/empty/non-string value is a readable error, never a silently empty
    secret — that outcome would look like a successful setup and fail later, far
    from the cause;
  - `Object.hasOwn` on every path segment, so `$.constructor` cannot reach a
    prototype property;
  - the raw document is never returned, logged or persisted.

Paths are a deliberate subset (`$.a.b`), not JSONPath: service-account files are
flat, and the full grammar would add a dependency plus an evaluation surface for
no case anyone has. An unsupported path fails loudly instead of matching nothing.

## The write path is SHARED, not copied

`POST /installed/:id/secrets/from-json` extracts, then hands the derived values
to the same `applySetupValues` the typed `PATCH …/secrets` now uses. The
security claim of #603 is that an extracted value is treated exactly like a
typed one — same `pattern` validation, same vault/config split, same
reactivation, same response (key NAMES only, never a value). Two implementations
of that would be two implementations that can drift, and the drift would be
invisible until a `json_file` field quietly skipped a check the typed path
applies.

The extraction map comes from the MANIFEST, never the request: a
caller-supplied `extracts` would let anyone write any vault key from any file.

`coerce()` refuses a value submitted under a `json_file` key rather than
ignoring it — silently dropping it would let a client believe it had stored a
credential. The install form skips the field for the same reason, so the two
sides agree.

## UI

The install wizard renders a file picker. Falling through to the text input
would be worse than not shipping the type: it would ask the operator to paste a
raw key into a field, which is the transcription step this removes.

The post-install `CredentialsEditor` omits `json_file` fields — every row there
is a text input, and the DERIVED fields are ordinary secrets listed as usual, so
post-install editing still works. Offering the upload there too is a follow-up.

## The errorHelp guard earned its keep

The new codes were first built with a template
(`runtime.json_file_${failure.code}`), which the coverage guard cannot see — the
codes would have shipped with no operator copy while the suite stayed green. Now
a `JSON_FILE_ERROR_CODES` table holds literals, exhaustive over
`JsonFileFailureCode` so a new failure kind cannot ship without a wire code, and
the forwarder is registered in the guard. 11 new help entries in both locales;
the two spec-level ones say plainly that the fault is in the plugin, not the
operator's file — that distinction is the difference between "try another file"
and "report this".

Verification: `test/setupJsonFile.test.ts` 12/12, related middleware suites
45/45, web-ui 693/693, `tsc --noEmit` clean on both sides, `typecheck:test`
ratchet 406 = baseline, eslint 0 errors.

`TemplateInstantiateForm.test.tsx` failed once in a full-suite run and passes
both in isolation (22/22) and on a full-suite re-run (693/693) — the repo's
known cross-file pollution, not this change.

Not included: the Google Workspace manifest itself lives in a separate repo, and
post-install upload in the credentials editor.

Closes #603
@Weegy Weegy self-assigned this Aug 13, 2026
@Weegy
Weegy merged commit 1f2b70f into main Aug 13, 2026
9 checks passed
@Weegy
Weegy deleted the feat/603-json-file-setup-field branch August 14, 2026 06:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OM-17: add a json_file setup-field type so service-account keys are uploaded, not transcribed

1 participant