Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .agents/skills/hypercerts-api/references/reliability.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

A handle is a resolvable actor identifier, not a DID or record URI. `app.certified.actor.getProfile` accepts a DID or handle and uses the configured handle resolver; the declared contract does not claim independent DID-document verification. Batch profile/organization lookups take DIDs.

The API's shared DID syntax check permits `%` without requiring a following pair of hexadecimal digits, so a value such as `did:example:abc%ZZ` passes DID syntax validation. When sending such a DID in a query string, use `URLSearchParams` to encode it as query data; see [request construction](request-patterns.md#build-one-public-xrpc-get). The shared syntax also permits empty internal colon-separated segments (for example, `did:example:a::b`), including for EVM-link identifiers; a trailing colon remains invalid.

### Exact collection item versions

`org.hypercerts.collection.listCollectionItems` resolves each embedded item's strong reference by exact URI **and** CID. If the requested CID is unavailable, the item's `record` can be null; do not silently substitute a newer record at the same URI. Nested collections are summaries, not recursive expansion. By contrast, collection discovery filters such as `itemUris` and `tagUris` currently match AT-URIs without selecting a CID version. See [recipes.md](recipes.md#load-exact-collection-item-versions) and the operation's details in the bundled OpenAPI contract before relying on exact-version behavior.
Expand Down
5 changes: 5 additions & 0 deletions .changeset/broad-did-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hypercerts-org/hypercerts-api': patch
---

Query endpoints now accept DID values consistently, including percent characters that are not followed by two hexadecimal digits. EVM-link queries also accept otherwise-valid DIDs with empty colon-separated segments.
22 changes: 0 additions & 22 deletions api/lua/endpoints/getAcknowledgement.lua
Original file line number Diff line number Diff line change
Expand Up @@ -44,28 +44,6 @@ local PROFILE = "app.certified.actor.profile"
local ORGANIZATION = "app.certified.actor.organization"
local NULL = json.decode("null")

local generic_valid_did = valid_did
local generic_valid_record_uri = valid_record_uri

local function acknowledgement_valid_did(value)
if not generic_valid_did(value) then return false end
local position = 1
while true do
local percent = value:find("%", position, true)
if not percent then break end
local escape = value:sub(percent + 1, percent + 2)
if #escape ~= 2 or escape:find("[^0-9A-Fa-f]") then return false end
position = percent + 3
end
return true
end

valid_record_uri = function(value)
local valid, collection, authority = generic_valid_record_uri(value)
if not valid or not acknowledgement_valid_did(authority) then return false end
return true, collection
end

local function acknowledgement_query(sql, values)
if db.backend() ~= "postgres" then
error("AcknowledgementQueryFailed: acknowledgement API requires PostgreSQL", 0)
Expand Down
17 changes: 1 addition & 16 deletions api/lua/endpoints/getContribution.lua
Original file line number Diff line number Diff line change
Expand Up @@ -83,25 +83,10 @@ end

local CONTRIBUTION = "org.hypercerts.claim.contribution"

local function valid_contribution_did(value)
if type(value) ~= "string" or not valid_did(value) then return false end
local index = 1
while index <= #value do
if value:sub(index, index) == "%" then
local escape = value:sub(index + 1, index + 2)
if #escape ~= 2 or escape:find("[^0-9A-Fa-f]") then return false end
index = index + 3
else
index = index + 1
end
end
return true
end

local function valid_contribution_uri(value)
local valid, collection, authority = valid_record_uri(value)
if not valid or collection ~= CONTRIBUTION then return false end
return valid_contribution_did(authority)
return valid_did(authority)
end

local function contribution_query(sql, values)
Expand Down
14 changes: 1 addition & 13 deletions api/lua/endpoints/getContributorInformation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,10 @@ end

local CONTRIBUTOR_INFORMATION_COLLECTION = "org.hypercerts.claim.contributorInformation"

local function contributor_information_did(value)
if not valid_did(value) then return false end
local position = 1
while true do
local percent = value:find("%", position, true)
if not percent then return true end
local escape = value:sub(percent + 1, percent + 2)
if #escape ~= 2 or escape:find("[^%x]") then return false end
position = percent + 3
end
end

local function contributor_information_uri(value)
local valid, collection, authority = valid_record_uri(value)
if not valid or collection ~= CONTRIBUTOR_INFORMATION_COLLECTION then return false end
return contributor_information_did(authority)
return valid_did(authority)
end

local NULL = json.decode("null")
Expand Down
30 changes: 1 addition & 29 deletions api/lua/endpoints/getEvmLink.lua
Original file line number Diff line number Diff line change
Expand Up @@ -53,40 +53,12 @@ end

local EVMLINK_COLLECTION = "app.certified.link.evm"

local function valid_evm_did(value)
if type(value) ~= "string" or #value > 2048 then return false end
if not valid_did(value) then return false end
local method, specific = value:match("^did:([a-z]+):(.+)$")
if not method or not specific then return false end

local segment_length, index = 0, 1
while index <= #specific do
local char = specific:sub(index, index)
if char == ":" then
if segment_length == 0 then return false end
segment_length = 0
index = index + 1
elseif char == "%" then
local escape = specific:sub(index + 1, index + 2)
if #escape ~= 2 or escape:find("[^%x]") then return false end
segment_length = segment_length + 1
index = index + 3
elseif char:match("^[%w._%-]$") then
segment_length = segment_length + 1
index = index + 1
else
return false
end
end
return segment_length > 0
end

local function valid_evm_link_uri(value)
if type(value) ~= "string" or value:find("[?#]") then return false end
local valid, collection = valid_record_uri(value)
if not valid or collection ~= EVMLINK_COLLECTION then return false end
local authority = value:match("^at://([^/]+)/")
return valid_evm_did(authority)
return valid_did(authority)
end

local function evm_link_record_view(row)
Expand Down
14 changes: 1 addition & 13 deletions api/lua/endpoints/getFeature.lua
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,11 @@ end

local FEATURE_COLLECTION = "org.hypercerts.entity.feature"

local function feature_valid_did(value)
if not valid_did(value) then return false end
local position = 1
while true do
local escape_start = value:find("%", position, true)
if not escape_start then return true end
local escape = value:sub(escape_start + 1, escape_start + 2)
if not escape:match("^[0-9a-fA-F][0-9a-fA-F]$") then return false end
position = escape_start + 3
end
end

local function feature_valid_uri(value)
if type(value) ~= "string" or #value > 8192 then return false end
local valid, collection, authority = valid_record_uri(value)
if not valid or collection ~= FEATURE_COLLECTION then return false end
return feature_valid_did(authority)
return valid_did(authority)
end

local FEATURE_PROJECTION_PROFILE = "app.certified.actor.profile"
Expand Down
16 changes: 1 addition & 15 deletions api/lua/endpoints/getMeasurement.lua
Original file line number Diff line number Diff line change
Expand Up @@ -84,22 +84,8 @@ end
local MEASUREMENT = "org.hypercerts.context.measurement"
local MEASUREMENT_NULL = json.decode("null")

local function measurement_valid_did(value)
if not valid_did(value) then return false end
local position = 1
while true do
local percent = value:find("%", position, true)
if not percent then return true end
local hex = value:sub(percent + 1, percent + 2)
if #hex ~= 2 or hex:find("[^0-9A-Fa-f]") then return false end
position = percent + 3
end
end

local function measurement_valid_record_uri(value)
local valid, collection, authority = valid_record_uri(value)
if not valid then return false end
return measurement_valid_did(authority), collection
return valid_record_uri(value)
end

local function measurement_query(sql, values)
Expand Down
23 changes: 12 additions & 11 deletions api/lua/endpoints/getOrganization.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,8 @@ local function scalar(params, key)
return tostring(value)
end

local PROFILE = "app.certified.actor.profile"
local ORGANIZATION = "app.certified.actor.organization"
local NULL = json.decode("null")

local function query(sql, values)
if db.backend() ~= "postgres" then error("OrganizationQueryFailed: organization API requires PostgreSQL", 0) end
local ok, result = pcall(db.raw, sql, values)
if not ok or type(result) ~= "table" then
error("OrganizationQueryFailed: organization lookup failed", 0)
end
return result
end

local function record_view(row)
return {
uri = row.uri,
Expand All @@ -50,6 +39,18 @@ local function record_view(row)
}
end

local PROFILE = "app.certified.actor.profile"
local ORGANIZATION = "app.certified.actor.organization"

local function query(sql, values)
if db.backend() ~= "postgres" then error("OrganizationQueryFailed: organization API requires PostgreSQL", 0) end
local ok, result = pcall(db.raw, sql, values)
if not ok or type(result) ~= "table" then
error("OrganizationQueryFailed: organization lookup failed", 0)
end
return result
end

local function organization_actor_view(row)
return {
did = row.did,
Expand Down
37 changes: 13 additions & 24 deletions api/lua/endpoints/getOrganizations.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,8 @@ local function scalar(params, key)
return tostring(value)
end

local PROFILE = "app.certified.actor.profile"
local ORGANIZATION = "app.certified.actor.organization"
local NULL = json.decode("null")

local function query(sql, values)
if db.backend() ~= "postgres" then error("OrganizationQueryFailed: organization API requires PostgreSQL", 0) end
local ok, result = pcall(db.raw, sql, values)
if not ok or type(result) ~= "table" then
error("OrganizationQueryFailed: organization lookup failed", 0)
end
return result
end

local function record_view(row)
return {
uri = row.uri,
Expand All @@ -50,6 +39,18 @@ local function record_view(row)
}
end

local PROFILE = "app.certified.actor.profile"
local ORGANIZATION = "app.certified.actor.organization"

local function query(sql, values)
if db.backend() ~= "postgres" then error("OrganizationQueryFailed: organization API requires PostgreSQL", 0) end
local ok, result = pcall(db.raw, sql, values)
if not ok or type(result) ~= "table" then
error("OrganizationQueryFailed: organization lookup failed", 0)
end
return result
end

local function organization_actor_view(row)
return {
did = row.did,
Expand Down Expand Up @@ -85,18 +86,6 @@ local function hydrate_organization_actors(actors)
end
end

local function valid_organization_batch_did(value)
if not valid_did(value) then return false end

local percent = value:find("%", 1, true)
while percent do
local escape = value:sub(percent + 1, percent + 2)
if #escape ~= 2 or escape:find("[^%x]") then return false end
percent = value:find("%", percent + 3, true)
end
return true
end

local function requested_organization_actors(value)
if value == nil then invalid("actors is required") end

Expand Down Expand Up @@ -124,7 +113,7 @@ local function requested_organization_actors(value)

local unique, seen = {}, {}
for _, did in ipairs(actors) do
if not valid_organization_batch_did(did) then invalid("each actors value must be a valid DID") end
if not valid_did(did) then invalid("each actors value must be a valid DID") end
if not seen[did] then
seen[did] = true
unique[#unique + 1] = did
Expand Down
38 changes: 12 additions & 26 deletions api/lua/endpoints/getProfile.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,9 @@ local function scalar(params, key)
return tostring(value)
end

local PROFILE = "app.certified.actor.profile"
local NULL = json.decode("null")

local function query(sql, values)
if db.backend() ~= "postgres" then error("ProfileQueryFailed: profile API requires PostgreSQL", 0) end
local ok, result = pcall(db.raw, sql, values)
if not ok then error("ProfileQueryFailed: profile lookup failed", 0) end
return result
end

local function row_view(row)
local function record_view(row)
return {
uri = row.uri,
cid = row.cid,
Expand All @@ -47,6 +39,15 @@ local function row_view(row)
}
end

local PROFILE = "app.certified.actor.profile"

local function query(sql, values)
if db.backend() ~= "postgres" then error("ProfileQueryFailed: profile API requires PostgreSQL", 0) end
local ok, result = pcall(db.raw, sql, values)
if not ok then error("ProfileQueryFailed: profile lookup failed", 0) end
return result
end

local function valid_handle(value)
if type(value) ~= "string" or #value > 253 or value:find("[^%w%.%-]")
or value:find("%.%.") or value:sub(1, 1) == "." or value:sub(-1) == "." then
Expand Down Expand Up @@ -173,25 +174,10 @@ local function resolver_base_url()
return "https://" .. authority
end

local function valid_resolver_did(value)
if not valid_did(value) then return false end
local index = 1
while index <= #value do
if value:sub(index, index) == "%" then
local escape = value:sub(index + 1, index + 2)
if #escape ~= 2 or not escape:match("^%x%x$") then return false end
index = index + 3
else
index = index + 1
end
end
return true
end

local function resolve_handle(handle)
local url = resolver_base_url() .. "/xrpc/com.atproto.identity.resolveHandle?handle=" .. encode_query_value(handle)
local resolved = fetch_json(url)
if type(resolved.did) ~= "string" or not valid_resolver_did(resolved.did) then fail_resolution() end
if type(resolved.did) ~= "string" or not valid_did(resolved.did) then fail_resolution() end
return resolved.did
end

Expand All @@ -214,7 +200,7 @@ local function get_profile()
"SELECT uri, did, cid, indexed_at::text AS indexed_at, record::text AS record FROM happyview_records WHERE collection = $1 AND did = $2 AND rkey = 'self' LIMIT 1",
{ PROFILE, did })
if #rows == 0 then error("RecordNotFound: profile is not indexed", 0) end
return { profile = row_view(rows[1]) }
return { profile = record_view(rows[1]) }
end

function handle()
Expand Down
Loading
Loading