iam: the device-approval lookup says what it does
Hanzo CI/CD / cicd (push) Successful in 18s
CI/CD / gate (push) Successful in 18s
CI/CD / containment (push) Successful in 1m44s
CI/CD / image (push) Skipped
CI/CD / rollout (push) Skipped
CI/CD / reach (push) Skipped
CI/CD / fanout (push) Skipped
CI/CD / receipt (push) Skipped

POST /v1/iam/oauth/device/info arrived with hanzoai/iam v1.34.0 -> v1.34.5
(85f5697c) and reached the router with no prose anywhere. It is an UNTYPED route
inside a GRAFTED app, so neither seam that normally carries prose reaches it:
zipdoc lifts doc comments off TYPED ops, and the upstream module ships no lift
for this one, so there is nothing in zip's extraction for a host to read.

openapi.Complete then refused the whole document — "1 operation(s) say nothing
about themselves" — so apps/iam could not describe itself at all, and the drift
gate stopped there without reaching any app after it.

openapi.Describe is the seam that refusal names for exactly this case. It is
additive metadata on a route the router already carries, so it cannot add, move
or rename an operation; it renders nothing if the address is not live. The
sentence is the upstream handler's own, restated where a host can reach it, and
it says the three things a caller cannot guess: the fields come off the pending
code's own application, the read is a POST because the user_code is a secret,
and every refusal is the same opaque one so a 40-bit code cannot be hunted.

The sentence belongs upstream, on the operation. When github.com/hanzoai/iam
gives the op its own prose, this goes away.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
2026-08-02 16:01:20 -07:00
parent aee62d7d6c
commit b5d8f9273b
+33
View File
@@ -84,6 +84,7 @@ import (
"net/http"
"os"
"github.com/hanzoai/cloud/openapi"
"github.com/hanzoai/cloud/sqlpool"
iamserver "github.com/hanzoai/iam/server"
"github.com/hanzoai/orm"
@@ -295,6 +296,38 @@ func patterns() []string {
return append(out, "/.well-known/*")
}
// The device-approval lookup states itself HERE because the graft leaves it nowhere
// else to. It is an untyped route in another module, so neither seam that normally
// carries prose reaches it: zipdoc lifts doc comments off TYPED ops, and the doc
// comment on its handler therefore never enters zip's extraction for a host to read.
// openapi.Describe is the seam for exactly that route, and it is additive metadata on
// a route the router already carries — it cannot add, move or rename an operation.
//
// This is the host speaking for a route it mounts, so it is second-best by
// construction: the sentence belongs upstream, on the operation, where the handler
// lives. When github.com/hanzoai/iam gives the op its own prose, delete this.
func init() {
openapi.Describe("/v1/iam/oauth/device/info", http.MethodPost,
"Name the application a pending device code is asking to sign in.",
"Answers \"what am I approving?\" for a pending user_code, so the approval page can "+
"name the application a human is about to authorize. Both fields come off the "+
"pending code's OWN application — never off the portal the browser happens to be "+
"on — so the screen cannot name one application while the code belongs to "+
"another.\n\n"+
"Requires a signed-in session, resolved from the browser's session cookie exactly "+
"as the approval itself resolves it. Not signed in is not a refusal to explain: it "+
"carries the stable login-required code the approval page branches on to sign the "+
"human in first.\n\n"+
"POST for a read, deliberately, for the same reason RFC 7662 introspection beside "+
"it is POST: the argument is a SECRET. A user_code in a request line is copied into "+
"ingress and proxy access logs, which a POST body is not.\n\n"+
"Unknown, expired, already used and already approved all get ONE opaque refusal — "+
"the same one the approval attempt would get. The user_code carries only 40 bits, "+
"so an answer that distinguished those states would be an oracle for hunting live "+
"codes; gated and opaque, this reveals strictly less than the approval the same "+
"caller could already attempt.")
}
// safeMount GRAFTS the IAM app into cloud, under a recover so its only panic path —
// a registered enterprise feature failing to mount — becomes an error the caller
// fail-closes on, never a crash of the shared cloud binary.