Add JSON-field token extraction and custom admin headers to ServiceDeployer - #105
Conversation
…tration Support extracting a bearer token from a JSON field of the auth-token secret, and forwarding static headers on every admin API request. Both options are opt-in and only forwarded to the custom resource when set, avoiding CFN property diffs for existing users. JSON extraction happens inside the handler so the plaintext token is never exposed as a custom-resource property. Co-Authored-By: Claude Code
Simplify header handling: instead of stripping reserved headers (Authorization, Content-Type, Accept) from additionalHeaders, spread additionalHeaders last so a caller can override them when a proxy or gateway in front of the admin endpoint requires it. Co-Authored-By: Claude Code
buildBaseHeaders returns both the bearer token and the caller's static headers, so the delete path's catch was discarding headers that never depended on the secret. A caller using additionalHeaders to satisfy a proxy in front of the admin endpoint would see the best-effort delete rejected, silently leaving the deployment registered in Restate. Co-Authored-By: Claude Code
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
pcholakov
left a comment
There was a problem hiding this comment.
Hey @nolanmurphey, this is a solid take on tier 1 of #103; only forwarding the new props when set means existing stacks see no property diff. I am approving - leaving some non-blocking notes below, but we're happy to pick any of them up in a follow-up before the next release if you'd rather not do this right now.
additionalHeaders values are stored and logged in plaintext. They're custom-resource properties so they'll show up in the synthesized template; also the handler logs the whole event on entry (console.log({ event })), which would dump them in CloudWatch Logs. Two cheap mitigations: a line in the prop docs saying not to put credentials there, and logging only header names in that event log. We can do the log redaction on our side. A secret-backed header option could get added in tier 2.
Overriding Accept breaks cleanup silently. The /query call relies on Accept: application/json, because Restate otherwise returns Arrow IPC data. This can break pruning and delete-time cleanup, and since both only log a warning, this risks a deployment silently staying registered after resource deletion. Authorization is a reasonable header to override with the secret storage caveat. For Accept and Content-Type, either add a line to the docs - or keep them fixed.
Handler tests. The new tests cover forwarding at synth time. The handler logic itself isn't covered: JSON extraction and its error cases, header precedence, and the delete-path fallback from the third commit. Exporting buildBaseHeaders, or pulling the extraction out as a pure function, would make these easy to test with a mocked Secrets Manager client.
| * satisfying a proxy/gateway in front of the Restate admin endpoint. | ||
| * | ||
| * These are applied by the shipped handler and do not require bundling. They take precedence over headers the | ||
| * handler sets itself (`Authorization`, `Content-Type`, `Accept`); use standard header casing to override one. |
There was a problem hiding this comment.
You can drop "use standard header casing to override" here and in the handler's RegistrationProperties doc. Node lowercases header names and the last one set wins, so authorization overrides Authorization as well (confirmed against a local server).
There was a problem hiding this comment.
Ah you're right - updated!
| adminUrl: options?.adminUrl ?? environment.adminUrl, | ||
| authTokenSecretArn: authToken?.secretArn, | ||
| // Forward JSON-field extraction and extra headers only when set, to avoid CFN property diffs for existing users. | ||
| ...(options?.authTokenJsonField !== undefined ? { authTokenJsonField: options.authTokenJsonField } : {}), |
There was a problem hiding this comment.
authTokenJsonField without an auth token is silently ignored. A synth-time error would catch the misconfiguration.
There was a problem hiding this comment.
Good catch - added a synth-time check
| * `{"token":"rst_xxx","version":3}`, set this to `"token"`. Only flat, top-level keys are supported; nested paths | ||
| * are not. | ||
| */ | ||
| authTokenJsonField?: string; |
There was a problem hiding this comment.
The JSON shape belongs to the secret, so I think this might work better in RestateEnvironment next to authToken rather than being repeated on every register() call.
There was a problem hiding this comment.
You're right that would make more sense. Moved this to RestateEnvironment and added the caveat that it wouldn't be used if you override at the per-registration level!
| throw new Error(`Secret value is not valid JSON; cannot extract field "${props.authTokenJsonField}".`); | ||
| } | ||
| const field = parsed?.[props.authTokenJsonField]; | ||
| if (typeof field !== "string") { |
There was a problem hiding this comment.
If the JSON field holds an empty string, it passes this check and will send Bearer with no token. Worth rejecting the same way as a missing field.
There was a problem hiding this comment.
Added a check for this!
Narrow auth review follow-up Move JSON token field to RestateEnvironment Test JSON auth token extraction Revert JSON extraction unit test refactor
Implements tier 1 of #103 — declarative authTokenJsonField and additionalHeaders.
Adds two opt-in registration options to the Restate ServiceDeployer, for environments where the admin auth token isn't a bare string or the admin endpoint sits behind a proxy.
Options
Both are only forwarded to the custom resource when set, so existing stacks see no CloudFormation property diff. additionalHeaders is also preserved on the best-effort delete path when the token fails to load, since those headers don't depend on the secret.
Changes
Scope
Tier 1 only: both options ride across as custom-resource properties and are applied by the existing shipped handler, so the deployer Lambda is unchanged.
Tests
npm run build, npm run lint, and npm test all pass (14/14 tests, 9/9 snapshots).