{"id":26722366,"url":"https://github.com/danhtran94/authzed-codegen","last_synced_at":"2026-05-09T10:28:12.575Z","repository":{"id":284508711,"uuid":"955166312","full_name":"danhtran94/authzed-codegen","owner":"danhtran94","description":"Type-Safety Code Generation tools for AuthZed. (Golang)","archived":false,"fork":false,"pushed_at":"2025-03-27T15:06:40.000Z","size":43,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-05-29T09:06:12.593Z","etag":null,"topics":["acl","authzed","fga","generator","permission","rbac"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/danhtran94.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-03-26T08:01:53.000Z","updated_at":"2025-03-30T12:16:39.000Z","dependencies_parsed_at":"2025-03-26T09:23:14.734Z","dependency_job_id":"287be6a9-6125-4b98-8212-e4ff88963a09","html_url":"https://github.com/danhtran94/authzed-codegen","commit_stats":null,"previous_names":["danhtran94/authzed-codegen"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/danhtran94/authzed-codegen","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danhtran94%2Fauthzed-codegen","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danhtran94%2Fauthzed-codegen/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danhtran94%2Fauthzed-codegen/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danhtran94%2Fauthzed-codegen/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/danhtran94","download_url":"https://codeload.github.com/danhtran94/authzed-codegen/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danhtran94%2Fauthzed-codegen/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28436237,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T21:32:52.117Z","status":"ssl_error","status_checked_at":"2026-01-14T21:32:33.442Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["acl","authzed","fga","generator","permission","rbac"],"created_at":"2025-03-27T20:02:18.686Z","updated_at":"2026-05-09T10:28:12.567Z","avatar_url":"https://github.com/danhtran94.png","language":"Go","funding_links":[],"categories":["Clients"],"sub_categories":["Third-party Libraries"],"readme":"# authzed-codegen\n\nType-safe Go bindings for [AuthZED / SpiceDB](https://authzed.com/) schemas.\nEach `definition` block in a `.zed` file becomes a `.gen.go` with typed\nconstructors, relation writers, and per-permission `Check` / `Lookup`\nmethods over the runtime engine in `pkg/authz/`.\n\n## Example\n\nGiven a schema:\n\n```hcl\ndefinition menusvc/order {\n    relation creator: menusvc/user | menusvc/customer\n    relation belongs_company: menusvc/company\n    permission write = creator + creator-\u003emanage + belongs_company-\u003emanage\n}\n```\n\nThe codegen produces typed bindings:\n\n```go\norder := menusvc.Order(\"o-1\")\nuser  := menusvc.User(\"u-1\")\n\nif err := order.CreateCreatorRelations(ctx, menusvc.OrderCreatorObjects{\n    User: []menusvc.User{user},\n}); err != nil {\n    return err\n}\n\nok, err := order.CheckWrite(ctx, menusvc.CheckOrderWriteInputs{\n    User: []menusvc.User{user},\n})\n```\n\nEach method dispatches through the `authz.Engine` interface; the SpiceDB\nclient lives in `pkg/authz/spicedb/`.\n\n## Install\n\n```sh\ngo install github.com/danhtran94/authzed-codegen/cmd/authzed-codegen@latest\n```\n\n## Usage\n\n```sh\nauthzed-codegen --output \u003cout-dir\u003e \u003cschema.zed\u003e\n```\n\nOne `.gen.go` is emitted per `definition` block, grouped by namespace\n(`menusvc/order` → `\u003cout-dir\u003e/menusvc/order.gen.go`). See `example/` for\na complete schema and its generated output.\n\n## Schema Support\n\n| Construct                              | Status                                                                                          |\n|----------------------------------------|-------------------------------------------------------------------------------------------------|\n| Union (`+`), arrow (`-\u003e`)              | ✓                                                                                               |\n| Wildcard relations (`type:*`)          | ✓ — `Wildcards` sub-struct on `\u003cRel\u003eObjects`; sibling `Read\u003cRel\u003e\u003cType\u003eWildcard` read methods    |\n| Intersection (`\u0026`), exclusion (`-`)    | ✓                                                                                               |\n| Caveats (`with \u003ccaveat\u003e`)              | ✓ — typed `\u003cPascal\u003eArgs` per caveat; nested `Caveats` sub-struct on `\u003cRel\u003eObjects` and `Check\u003cPerm\u003eInputs`; multi-caveat-per-permission supported |\n| Expiration (`with expiration`)         | ✓ — per-tuple TTL via `Expirations` sub-struct on `\u003cRel\u003eObjects`; auto-switches to `OPERATION_TOUCH`; combines with caveats |\n| Sub-relation references (`foo#bar`)    | ✓ — typed userset write field (`\u003cTypeName\u003e\u003cPascalSubRel\u003e`) on `\u003cRel\u003eObjects`; userset Check input field; `SubRelation` on metadata struct |\n\nParsing delegates to `github.com/authzed/spicedb/pkg/schemadsl/compiler` —\nany schema SpiceDB accepts will parse. The codegen layer is narrower;\nrejected constructs surface schema-relative errors before any output is\nwritten. Rationale: `docs/ADR-001-parser-migration.md`.\n\n## Caveats\n\nRelations and allowed types declared `with \u003ccaveat\u003e` generate a typed\n`\u003cCaveatPascal\u003eArgs` struct per caveat (one per namespace) plus a\n`Caveats` sub-struct on the relation's `\u003cRel\u003eObjects` and the\npermission's `Check\u003cPerm\u003eInputs`. Scalar fields (`*string`, `*int`,\n`*bool`, `*float64`) are pointer-typed so callers can defer individual\nparameters to check time; container fields (`[]string`, `[]byte`, `map`)\nstay direct (nil = unset).\n\n```hcl\ncaveat extsvc/tenant_match(tenant string) {\n    tenant == \"acme\"\n}\n\ndefinition extsvc/folder {\n    relation tenanted_viewer: extsvc/user with extsvc/tenant_match\n    permission tenanted_browse = tenanted_viewer\n}\n```\n\nPre-bind the policy at write time (caveat travels with the tuple):\n\n```go\nfolder.CreateTenantedViewerRelations(ctx, extsvc.FolderTenantedViewerObjects{\n    User: []extsvc.User{user},\n    Caveats: extsvc.FolderTenantedViewerCaveats{\n        User: \u0026extsvc.TenantMatchArgs{Tenant: new(\"acme\")},\n    },\n})\n```\n\nOr defer all binding to check time (write attaches the caveat name with\nno pre-context; check supplies the value):\n\n```go\nfolder.CreateTenantedViewerRelations(ctx, extsvc.FolderTenantedViewerObjects{\n    User: []extsvc.User{user},\n    // Caveats omitted — write-time pre-context is nil\n})\n\nok, err := folder.CheckTenantedBrowse(ctx, extsvc.CheckFolderTenantedBrowseInputs{\n    User: []extsvc.User{user},\n    Caveats: extsvc.CheckFolderTenantedBrowseCaveats{\n        TenantMatch: \u0026extsvc.TenantMatchArgs{Tenant: new(\"acme\")},\n    },\n})\n```\n\nPer-key precedence is per SpiceDB's wire model: write-time values win on\ncollision, unbound keys fall through to check-time. Permissions reaching\n2+ distinct caveats are supported — `Check\u003cPerm\u003eCaveats` gets one field\nper unique caveat, the generated method merges all non-nil entries into\none wire `Context`. Cross-caveat parameter-name collisions (two caveats\ndeclaring the same key) are detected at codegen and emit a clear error.\n\n`Lookup\u003cPerm\u003e\u003cType\u003eResources` and `Lookup\u003cPerm\u003e\u003cType\u003eSubjects` thread\ncaveat context through too — for caveat-reaching permissions, both\nmethods accept a `Caveats` argument (positional for Subjects, on the\nexisting input struct for Resources) and route through\n`LookupResourcesWithCaveat` / `LookupSubjectsWithCaveat`.\n`CONDITIONAL_PERMISSION` results are filtered out of the returned slice,\nmatching `Check\u003cPerm\u003e`'s collapse-to-deny semantics.\n\nSee `docs/spec-002-caveat-codegen.md` and `docs/spec-003-write-time-caveat-codegen.md`.\n\n## Expiration\n\nSchemas declaring `use expiration` at the top can mark relations with `with expiration`. Tuples carry per-tuple TTL via `OptionalExpiresAt`; SpiceDB filters expired entries server-side from Check / Lookup / Read. The codegen surfaces a `*time.Time` field per expiring allowed type on a new `Expirations` sub-struct (parallel to `Wildcards` and `Caveats`):\n\n```hcl\nuse expiration\n\ndefinition extsvc/folder {\n    relation expiring_viewer: extsvc/user with expiration\n    permission expiring_browse = expiring_viewer\n}\n```\n\n```go\nexpiresAt := time.Now().Add(1 * time.Hour)\nfolder.CreateExpiringViewerRelations(ctx, extsvc.FolderExpiringViewerObjects{\n    User: []extsvc.User{user},\n    Expirations: extsvc.FolderExpiringViewerExpirations{\n        User: \u0026expiresAt,\n    },\n})\n```\n\nCombined with caveats — `relation gated: extsvc/user with extsvc/tenant_match and expiration` — both `Caveats` and `Expirations` sub-structs are populated independently. The codegen routes through `CreateRelationsWithExpiration` (auto-switching to `OPERATION_TOUCH` because un-garbage-collected expired tuples may collide on tuple identity). See `docs/spec-004-expiration-codegen.md`.\n\n## Read with Metadata\n\n`Read\u003cRel\u003e\u003cType\u003eRelations` returns `[]\u003cRel\u003e\u003cType\u003eRelation` — a typed metadata struct per tuple carrying the subject ID alongside the caveat name, decoded caveat context, and expiration timestamp:\n\n```go\ntype FolderTenantedViewerUserRelation struct {\n    ID            extsvc.User\n    CaveatName    string         // \"\" when no caveat is attached\n    CaveatContext map[string]any // nil when no caveat or empty pre-context\n    ExpiresAt     *time.Time     // nil when no per-tuple TTL\n}\n```\n\nThe metadata fields are nil/empty for plain relations; they populate from SpiceDB's `Relationship.OptionalCaveat` and `Relationship.OptionalExpiresAt` for trait-bearing tuples. Use cases — admin/audit UIs that need to surface \"user X has access via tenant=acme until 2026-Q4\" without bypassing the codegen.\n\nFor callers that just want the IDs (matching the pre-v1.4.0 shape):\n\n```go\nrels, _ := folder.ReadViewerUserRelations(ctx)\nusers := authz.IDsOf(rels)  // []User\n```\n\n`authz.IDsOf` is a generic helper; type inference resolves the typed slice from the single positional argument.\n\nWildcard reads return the same metadata struct alongside the presence bool:\n\n```go\nmeta, isWildcard, err := folder.ReadGuestUserWildcard(ctx)\nif isWildcard \u0026\u0026 meta.ExpiresAt != nil {\n    // public-for-everyone-until-timestamp pattern\n}\n```\n\nSee `docs/spec-005-read-with-metadata.md` for the full Engine surface and constraints (no auto-decoded `\u003cCaveat\u003eArgs`, slice materialization vs streaming, wildcard split discipline).\n\n## Sub-relation References\n\nSchemas declaring `relation X: Type#SubRelation` grant access via inheritance — anyone reaching `Type#SubRelation` (typically a permission or relation on the target type) is implicitly granted on the resource. The codegen surfaces userset writes as a typed field on `\u003cRel\u003eObjects`:\n\n```hcl\ndefinition extsvc/team {\n    relation owner: extsvc/user\n    relation manager: extsvc/user\n    permission admin = owner + manager\n}\n\ndefinition extsvc/folder {\n    relation collab: extsvc/team#admin\n    permission collab_view = collab\n}\n```\n\n```go\n// Grant team t1's admin set as a collaborator. SpiceDB stores\n// (folder:f1, collab, team:t1#admin) — the wire keeps the team ID\n// as the anchor; user resolution happens at Check time.\nfolder.CreateCollabRelations(ctx, extsvc.FolderCollabObjects{\n    TeamAdmin: []extsvc.Team{team},\n})\n```\n\nCommon-case Check (does user u1 have access?) — SpiceDB walks the userset chain server-side:\n\n```go\n// u1 must be owner or manager of t1 for this Check to grant.\nok, _ := folder.CheckCollabView(ctx, extsvc.CheckFolderCollabViewInputs{\n    TeamAdmin: []extsvc.Team{team},  // userset-as-subject input\n})\n```\n\nPermissions reaching userset allowed types expose userset input fields on `Check\u003cPerm\u003eInputs`. The userset-as-subject Check (rare case) matches the literal userset reference — useful for \"does this group itself have permission?\" admin/audit tooling. Direct-subject Check (the common case) walks the chain transparently when the schema includes both branches.\n\nRead-side rows surface a `SubRelation` field on the metadata struct — empty for direct subjects, non-empty for userset references. Mixed schemas (`relation viewer: user | team#admin`) produce distinct Read methods per subject type (`ReadViewerUserRelations` and `ReadViewerTeamRelations`); each returns disjoint rows.\n\nSee `docs/spec-006-sub-relation-references.md` for the wire-level walkthrough, the rare-case Check semantics (literal-match vs chain-walking), and the deferred Lookup-with-userset-results work.\n\n## Conditional Permission\n\nSpiceDB returns `CONDITIONAL_PERMISSION` when a caveat reaches the Check chain but the request is missing parameter context. `Check\u003cPerm\u003e` paths surface this as a typed error so callers can distinguish recoverable failures (missing context) from hard denies:\n\n```go\nerr := folder.CheckTenantedBrowse(ctx, extsvc.CheckFolderTenantedBrowseInputs{\n    User: []extsvc.User{user},\n    // Caveats omitted — caller forgot to supply tenant\n})\n\nswitch {\ncase err == nil:\n    // granted\n\ncase errors.Is(err, authz.ErrConditionalPermission):\n    var cpe *authz.ConditionalPermissionError\n    errors.As(err, \u0026cpe)\n    // cpe.MissingKeys == [\"tenant\"] — fetch from request context and retry\n\ncase errors.Is(err, authz.ErrPermissionDenied):\n    // hard deny — user genuinely lacks permission\n}\n```\n\nThe typed error's custom `Is` method matches both `ErrConditionalPermission` (for the rich-signal opt-in path) and `ErrPermissionDenied` (for backward compat with existing deny checks). Callers that only care about \"denied vs. granted\" keep working unchanged.\n\nLookup paths return a typed `LookupResult` partitioning definite grants from conditional grants — the same recovery hint surfaces on both Check and Lookup. Caller pattern:\n\n```go\nresult, err := folder.LookupTenantedBrowseUserSubjects(ctx, caveats)\n// result.Definite — confirmed grants\n// result.Conditional — partial grants; each has MissingKeys for caller to fetch and retry\n\nfor _, c := range result.Conditional {\n    fetched := fetchTenantContext(c.MissingKeys)\n    // retry Check / Lookup with the fetched context\n}\n```\n\nPer-type `\u003cType\u003eLookupResult` and `\u003cType\u003eConditionalLookupEntry` structs are generated once per object type and shared across every Lookup method returning that type. Wildcard subject methods (`Lookup\u003cPerm\u003e\u003cType\u003eWildcardSubjects`) keep their `(bool, error)` signature — they check `result.Definite` for the wildcard sentinel internally.\n\nSee `docs/spec-007-conditional-permission-signal.md` for the Check path, `docs/spec-008-lookup-conditional-surfacing.md` for the Lookup path.\n\n## Consistency\n\nThe `*spicedb.Engine` defaults to a time-based consistency policy: pin to `AtExactSnapshot` when a recent write token exists (read-your-own-writes), fall through to SpiceDB's `MinimumLatency` otherwise. For security-sensitive checks where stale reads are unacceptable, opt into `FullyConsistent`:\n\n```go\n// Default behavior — recent-token-or-nil from the engine's time-based policy:\nerr := folder.CheckTenantedBrowse(ctx, input)\n\n// Force fresh evaluation — bypasses cached snapshot:\nctx = authz.WithConsistency(ctx, authz.ConsistencyFullyConsistent)\nerr := folder.CheckTenantedBrowse(ctx, input)\n```\n\nThe override is per-call via context. Caller scopes it at the request boundary; all downstream Check / Lookup / Read methods called with that ctx honor the mode automatically. Zero codegen change — ctx already flows through every generated method.\n\nToken-based modes (`AtLeastAsFresh`, `AtExactSnapshot` with caller-supplied tokens) are deferred — the engine already uses `AtExactSnapshot` internally for read-your-own-writes. See `docs/spec-009-consistency-mode-opt-in.md`.\n\n## Schema Drift Detection\n\nThe codegen captures the source `.zed` bytes verbatim and emits `\u003coutput-dir\u003e/schema.gen.go` with `SchemaText`, `SchemaDigest`, and a `VerifySchema(ctx)` helper. At startup, callers compare the binary's baseline against the deployed schema in SpiceDB and decide whether to proceed:\n\n```go\nimport authzed \"github.com/danhtran94/authzed-codegen/example/authzed\"\n\ndrift, err := authzed.VerifySchema(ctx)\nif err != nil {\n    log.Fatalf(\"schema verification failed: %v\", err)\n}\nif drift.IsBreaking() {\n    log.Fatalf(\"schema drift: %d removed, %d changed\", len(drift.Removed), len(drift.Changed))\n}\nif !drift.IsClean() {\n    log.Warnf(\"schema is ahead: %d added, %d cosmetic\", len(drift.Added), len(drift.Cosmetic))\n}\n```\n\n`SchemaDrift` partitions the typed diffs into four severity buckets:\n- **Added** — deployed schema has things baseline doesn't (additive, safe)\n- **Removed** — baseline expects things deployed lacks (breaking)\n- **Changed** — semantic divergence in permission / caveat expressions or caveat parameter types (breaking)\n- **Cosmetic** — doc comment changes only (safe)\n\n`DriftEntry.Raw` exposes the typed `*v1.ReflectionSchemaDiff` for callers needing fine-grained handling. The package name of the generated file derives from the output dir's last segment (e.g. `--output example/authzed` → `package authzed`).\n\nServer-side normalisation happens in SpiceDB's `DiffSchema` RPC — whitespace, comment formatting, and ordering don't false-positive. See `docs/spec-010-schema-drift-detection.md`.\n\n## Behavior Notes\n\n- **Permission chains.** `Check\u003cPermission\u003eInputs` exposes the full set\n  of input types reachable through arrow expressions in referenced\n  permissions, including cross-definition arrows. Cycles\n  (`permission p = p + q`) exit non-zero with `cycle detected`.\n- **Wildcards.** `Create\u003cRel\u003eRelations` accepts `Wildcards{User: true}`\n  regardless of which permissions reference the relation. AuthZED's\n  guidance is to grant wildcards only on read-side relations (e.g.\n  `viewer`) to avoid universal write access; the codegen does not enforce\n  this — callers own the discipline.\n\n## Verification\n\nRound-trip the fixture (regression bar for the codegen itself):\n\n```sh\ngo run ./cmd/authzed-codegen --output example/authzed example/schema.zed\ngit diff --quiet example/authzed/\n```\n\nEnd-to-end tests exercise the generated stubs against a real SpiceDB\ncontainer via `testcontainers-go`. The harness lives in\n`pkg/authz/spicedbtest/`; the test packages are\n`example/authzed/{bookingsvc,menusvc,extsvc}` and `pkg/authz/spicedb/`.\n\n```sh\ngo test ./pkg/authz/spicedb/... ./example/authzed/...\n```\n\nTests skip cleanly when Docker is unavailable.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanhtran94%2Fauthzed-codegen","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdanhtran94%2Fauthzed-codegen","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanhtran94%2Fauthzed-codegen/lists"}