fix(actions): allow adding raw metadata values via appendMetadataRaw (#12567)

# Which Problems Are Solved

Since #10666 (v4.1+, backported to v4.x), metadata values set through
actions v1 (`api.metadata.push` and `api.v1.user.appendMetadata`) are
always JSON-encoded via `json.Marshal`. This made the write path
consistent with the JSON-based read path, but removed the ability to
store raw (unencoded) metadata values:

- A scalar string is now always stored quoted (`"de"` instead of `de`).
- The previous byte-array convention (mapping a string to an integer
array in the script, handled by `mapBytesToByteArray` introduced in
#5526) now stores the literal integer-array text (e.g. `[100,101]`)
instead of the raw bytes.

Customers migrating from v3.x whose downstream systems base64-decode
metadata values from tokens and expect raw bytes have no way to produce
them anymore — changing the consuming system is not always possible.

Reverting the default is not an option either, as clients that adopted
actions v1 on v4.x now rely on the JSON encoding.

# How the Problems Are Solved

- Adds a new, opt-in function `api.v1.user.appendMetadataRaw(key,
value)` to the actions v1 login flows (external / internal
authentication post authentication and pre creation), next to the
existing `appendMetadata`.
- The value is stored as raw bytes without JSON encoding:
  - a string is stored as its plain UTF-8 bytes (`de`, not `"de"`)
- byte arrays (`Uint8Array` or a plain array of integers 0-255, the old
convention) are stored as-is, so existing v3 scripts using a
string-to-byte-array helper only need to switch the function name
  - other types (and empty values) throw an error
- The existing `appendMetadata` and `api.metadata.push` behavior remains
byte-for-byte unchanged.

# Additional Changes

- Documented `appendMetadataRaw` (and the JSON encoding behavior of
`appendMetadata`) in the external and internal authentication actions
docs.
- Added unit tests for the new function (through a real goja runtime)
and a test locking in the existing `appendMetadata` JSON-encoding
behavior.

# Additional Context

- Regression introduced as a side effect of #10666 (which fixed #10470);
the raw byte handling was originally introduced in #5526.
- Reported by a customer upgrading from v3.4.x to v4.16.x, whose
PostAuthentication action maps token payload claims into user metadata.
- Requires backport to v4.x.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Livio Spring
2026-08-11 15:44:15 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent ba3a45bee4
commit 2bd42e8fc4
5 changed files with 200 additions and 4 deletions
@@ -43,7 +43,12 @@ The first parameter contains the following fields
- `v1`
- `user`
- `appendMetadata(string, Any)`
The first parameter represents the key and the second a value which will be stored
The first parameter represents the key and the second a value which will be stored.
The value is JSON-encoded, so a string is stored with surrounding quotes (e.g. `"de"`).
- `appendMetadataRaw(string, string | Uint8Array | number[])`
The first parameter represents the key and the second a value which will be stored as raw bytes without JSON encoding.
A string is stored as its plain UTF-8 bytes (e.g. `de` without quotes), a byte array (`Uint8Array` or an array of integers between 0 and 255) is stored as-is.
Use this if the systems consuming the metadata expect an unencoded value.
- `setFirstName(string)`
Sets the first name
- `setLastName(string)`
@@ -113,7 +118,12 @@ The trigger is represented by the following Ids in the API: `TRIGGER_TYPE_PRE_CR
- `v1`
- `user`
- `appendMetadata(string, Any)`
The first parameter represents the key and the second a value which will be stored
The first parameter represents the key and the second a value which will be stored.
The value is JSON-encoded, so a string is stored with surrounding quotes (e.g. `"de"`).
- `appendMetadataRaw(string, string | Uint8Array | number[])`
The first parameter represents the key and the second a value which will be stored as raw bytes without JSON encoding.
A string is stored as its plain UTF-8 bytes (e.g. `de` without quotes), a byte array (`Uint8Array` or an array of integers between 0 and 255) is stored as-is.
Use this if the systems consuming the metadata expect an unencoded value.
## Post Creation
@@ -33,7 +33,12 @@ The trigger is represented by the following Ids in the API: `TRIGGER_TYPE_POST_A
- `v1`
- `user`
- `appendMetadata(string, Any)`
The first parameter represents the key and the second a value which will be stored
The first parameter represents the key and the second a value which will be stored.
The value is JSON-encoded, so a string is stored with surrounding quotes (e.g. `"de"`).
- `appendMetadataRaw(string, string | Uint8Array | number[])`
The first parameter represents the key and the second a value which will be stored as raw bytes without JSON encoding.
A string is stored as its plain UTF-8 bytes (e.g. `de` without quotes), a byte array (`Uint8Array` or an array of integers between 0 and 255) is stored as-is.
Use this if the systems consuming the metadata expect an unencoded value.
## Pre Creation
@@ -80,7 +85,12 @@ The trigger is represented by the following Ids in the API: `TRIGGER_TYPE_PRE_CR
- `v1`
- `user`
- `appendMetadata(string, Any)`
The first parameter represents the key and the second a value which will be stored
The first parameter represents the key and the second a value which will be stored.
The value is JSON-encoded, so a string is stored with surrounding quotes (e.g. `"de"`).
- `appendMetadataRaw(string, string | Uint8Array | number[])`
The first parameter represents the key and the second a value which will be stored as raw bytes without JSON encoding.
A string is stored as its plain UTF-8 bytes (e.g. `de` without quotes), a byte array (`Uint8Array` or an array of integers between 0 and 255) is stored as-is.
Use this if the systems consuming the metadata expect an unencoded value.
## Post Creation