Summary
A password-typed field declared on a non-auth object (e.g. showcase_field_zoo.f_password) is stored by the generic CRUD path as-is — it is neither one-way hashed nor masked-on-read the way secret is. So writing a password field and reading the record back over the data API returns the plaintext value.
Surfaced while completing the dogfood field-zoo round-trip matrix (#2033). Not a regression — this is the current behavior; filing for a deliberate decision rather than fixing inline.
Why it matters
This is a low-code platform where field types are author-driven (often by an AI). Someone modeling a password field on a custom object reasonably expects credential-grade handling. Today they silently get plaintext storage + plaintext reads, with no warning — a runtime/security trap the static gates don't catch.
Current behavior (verified)
secret (type==='secret'): encrypt-on-write into sys_secret, opaque ref on the row, masked to SECRET_MASK (••••••••) on read. See packages/objectql/src/secret-fields.ts + engine.ts mask path.
password: the generic engine has no hashing/masking for it. Hashing lives only in the auth subsystem's dedicated CRUD pipeline (packages/plugins/plugin-security/.../default-permission-sets.ts comment: "password hashing … rather than the generic CRUD pipeline"). The record validator treats password like text (record-validator.ts). So on a generic object it round-trips plaintext.
- Dogfood proof:
field-zoo-roundtrip asserts f_password only present (persists) precisely because asserting a plaintext-equality contract would be undesirable to lock in.
Options to decide between
- Mask-on-read everywhere — treat
password like secret in the generic read path (return SECRET_MASK), so plaintext never leaves the engine outside the auth subsystem. Lowest-surprise.
- Hash-on-write in the generic path — but one-way hashing only makes sense for credential verification, which non-auth objects don't do; likely wrong fit.
- Author-time guard —
build/lint error (or warn) when a password field is declared on a non-auth object, steering authors to secret or the auth user object.
- Document as intended —
password is auth-subsystem-only by contract; generic use is unsupported. Weakest (silent trap remains).
Leaning 1 (+ maybe 3 as a guardrail). Needs a deliberate call.
Acceptance
- Decision recorded (ADR if it changes the read contract).
- A dogfood/unit test pins the chosen behavior for
password on a generic object.
Found via #2025 / #2028 / #2033 (field-type round-trip fidelity work).
Summary
A
password-typed field declared on a non-auth object (e.g.showcase_field_zoo.f_password) is stored by the generic CRUD path as-is — it is neither one-way hashed nor masked-on-read the waysecretis. So writing apasswordfield and reading the record back over the data API returns the plaintext value.Surfaced while completing the dogfood field-zoo round-trip matrix (#2033). Not a regression — this is the current behavior; filing for a deliberate decision rather than fixing inline.
Why it matters
This is a low-code platform where field types are author-driven (often by an AI). Someone modeling a
passwordfield on a custom object reasonably expects credential-grade handling. Today they silently get plaintext storage + plaintext reads, with no warning — a runtime/security trap the static gates don't catch.Current behavior (verified)
secret(type==='secret'): encrypt-on-write intosys_secret, opaque ref on the row, masked toSECRET_MASK(••••••••) on read. Seepackages/objectql/src/secret-fields.ts+engine.tsmask path.password: the generic engine has no hashing/masking for it. Hashing lives only in the auth subsystem's dedicated CRUD pipeline (packages/plugins/plugin-security/.../default-permission-sets.tscomment: "password hashing … rather than the generic CRUD pipeline"). The record validator treatspasswordliketext(record-validator.ts). So on a generic object it round-trips plaintext.field-zoo-roundtripassertsf_passwordonlypresent(persists) precisely because asserting a plaintext-equality contract would be undesirable to lock in.Options to decide between
passwordlikesecretin the generic read path (returnSECRET_MASK), so plaintext never leaves the engine outside the auth subsystem. Lowest-surprise.build/lint error (or warn) when apasswordfield is declared on a non-auth object, steering authors tosecretor the auth user object.passwordis auth-subsystem-only by contract; generic use is unsupported. Weakest (silent trap remains).Leaning 1 (+ maybe 3 as a guardrail). Needs a deliberate call.
Acceptance
passwordon a generic object.Found via #2025 / #2028 / #2033 (field-type round-trip fidelity work).