Summary
The short prefix used to identify keys in CLI/API operations (kfl_user_<3 hex chars> / kfl_sys_<4 hex chars>) is generated without any collision check and has no UNIQUE constraint in the database. A duplicate prefix causes kfl keys put, kfl keys revoke, and any other prefix-based operation to silently act on whichever matching row the DB returns first — the wrong key gets updated or revoked.
Details
Prefix keyspace is small:
kfl_user_ + 3 hex chars = 4,096 possible values
kfl_sys_ + 4 hex chars = 65,536 possible values
No collision guard at generation time (packages/server/src/routes/keys.ts:33–37, packages/server/src/routes/bootstrap.ts:57–59): the prefix is derived by slicing the first 12 chars of prefix + generateRandomHex(32). There is no retry loop or uniqueness probe before insertion.
No UNIQUE constraint on key_prefix (packages/server/src/db/schema.ts:8): only key_hash (the full key) is unique. The prefix column is NOT NULL only.
All prefix-based lookups use .limit(1): PUT /keys/:prefix, DELETE /keys/:prefix, and getKeyByPrefix all query by key_prefix and take the first result — no collision detection.
Note: The full key value is collision-safe (32 random hex chars, hashed with UNIQUE constraint). Only the short prefix used by the CLI/API is affected.
Steps to Reproduce
Collision probability grows with key count (birthday problem). With ~64 user keys the chance of at least one kfl_user_ prefix collision exceeds 50%. At that point:
- Two keys share the same prefix, e.g.
kfl_user_a1b
kfl keys put kfl_user_a1b --scope ... silently updates the wrong key
kfl keys revoke kfl_user_a1b silently revokes the wrong key
No error is returned.
Workaround
None at the CLI level. A careful operator can inspect raw DB rows to detect duplicates, but there is no CLI or API surface to do so.
Affected Commands
Exactly two CLI commands look up a key by prefix:
| CLI command |
Server route |
DB query |
kfl keys revoke <prefix> (keys.ts:99–102) |
DELETE /keys/:prefix → handleRevokeKey (keys.ts:132–148) |
revokeKeyByPrefix (queries.ts:47–56) |
kfl keys put <prefix> (keys.ts:104–131) |
PUT /keys/:prefix → handleUpdateKey (keys.ts:150–211) |
getKeyByPrefix + updateKeyScopes (queries.ts:58–81) |
kfl keys list and kfl keys create do not take a prefix as input — they are not affected.
Potential Fix
Two complementary changes:
- Add a
UNIQUE constraint on key_prefix in the D1 schema (new migration). This turns a silent collision into a hard DB error.
- Add a retry loop at prefix generation time: generate → check for existing prefix in DB → retry on collision (bounded, e.g. max 5 attempts). This prevents the error from ever reaching the user.
Do nots: For best UX, do NOT increase prefix entropy (e.g. 6 hex chars for user keys) to make collisions negligible in practice.
Summary
The short prefix used to identify keys in CLI/API operations (
kfl_user_<3 hex chars>/kfl_sys_<4 hex chars>) is generated without any collision check and has noUNIQUEconstraint in the database. A duplicate prefix causeskfl keys put,kfl keys revoke, and any other prefix-based operation to silently act on whichever matching row the DB returns first — the wrong key gets updated or revoked.Details
Prefix keyspace is small:
kfl_user_+ 3 hex chars = 4,096 possible valueskfl_sys_+ 4 hex chars = 65,536 possible valuesNo collision guard at generation time (
packages/server/src/routes/keys.ts:33–37,packages/server/src/routes/bootstrap.ts:57–59): the prefix is derived by slicing the first 12 chars ofprefix + generateRandomHex(32). There is no retry loop or uniqueness probe before insertion.No
UNIQUEconstraint onkey_prefix(packages/server/src/db/schema.ts:8): onlykey_hash(the full key) is unique. The prefix column isNOT NULLonly.All prefix-based lookups use
.limit(1):PUT /keys/:prefix,DELETE /keys/:prefix, andgetKeyByPrefixall query bykey_prefixand take the first result — no collision detection.Steps to Reproduce
Collision probability grows with key count (birthday problem). With ~64 user keys the chance of at least one
kfl_user_prefix collision exceeds 50%. At that point:kfl_user_a1bkfl keys put kfl_user_a1b --scope ...silently updates the wrong keykfl keys revoke kfl_user_a1bsilently revokes the wrong keyNo error is returned.
Workaround
None at the CLI level. A careful operator can inspect raw DB rows to detect duplicates, but there is no CLI or API surface to do so.
Affected Commands
Exactly two CLI commands look up a key by prefix:
kfl keys revoke <prefix>(keys.ts:99–102)DELETE /keys/:prefix→handleRevokeKey(keys.ts:132–148)revokeKeyByPrefix(queries.ts:47–56)kfl keys put <prefix>(keys.ts:104–131)PUT /keys/:prefix→handleUpdateKey(keys.ts:150–211)getKeyByPrefix+updateKeyScopes(queries.ts:58–81)kfl keys listandkfl keys createdo not take a prefix as input — they are not affected.Potential Fix
Two complementary changes:
UNIQUEconstraint onkey_prefixin the D1 schema (new migration). This turns a silent collision into a hard DB error.Do nots: For best UX, do NOT increase prefix entropy (e.g. 6 hex chars for user keys) to make collisions negligible in practice.