{"id":51136161,"url":"https://github.com/contember/propustka","last_synced_at":"2026-06-25T18:01:44.995Z","repository":{"id":364303561,"uuid":"1266424966","full_name":"contember/propustka","owner":"contember","description":"Propustka — IAM Worker (Cloudflare Access authz + audit) + @propustka/* SDK","archived":false,"fork":false,"pushed_at":"2026-06-19T14:37:47.000Z","size":480,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-19T16:17:25.535Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/contember.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-11T15:51:50.000Z","updated_at":"2026-06-19T14:37:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/contember/propustka","commit_stats":null,"previous_names":["contember/propustka"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/contember/propustka","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/contember%2Fpropustka","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/contember%2Fpropustka/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/contember%2Fpropustka/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/contember%2Fpropustka/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/contember","download_url":"https://codeload.github.com/contember/propustka/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/contember%2Fpropustka/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34786238,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-25T02:00:05.521Z","response_time":101,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":[],"created_at":"2026-06-25T18:01:44.061Z","updated_at":"2026-06-25T18:01:44.981Z","avatar_url":"https://github.com/contember.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Propustka\n\nInternal **IAM \u0026 audit service** for apps running on Cloudflare Workers. Authentication is\nhandled at the edge by **Cloudflare Access**; Propustka owns everything after that —\nauthorization (AWS-IAM-style policies over generic, app-owned scope dimensions), auth logging,\ndomain-event audit, capability tokens, and a small admin UI. Each app declares its own authz\nvocabulary (scope dimensions, action catalog, roles) in code and reconciles it in. Apps call\nPropustka through a thin SDK over a **service binding** and just do\n`authenticate()` + `can()` + `audit()`.\n\nDivision of responsibility:\n\n| Layer                                  | Owns                                                                                                  |\n| -------------------------------------- | ----------------------------------------------------------------------------------------------------- |\n| **Cloudflare Access** (not built here) | authentication (who you are) + coarse edge gate                                                       |\n| **Propustka Worker**                   | authorization (policies over app-owned scopes), auth log, audit ingest, capabilities, request context |\n| **Apps**                               | emit domain audit events; call `authenticate()` / `can()` / `audit()`                                 |\n\nDesign docs: [`iam-service-spec.md`](./iam-service-spec.md) ·\n[`admin-ui-spec.md`](./admin-ui-spec.md) · [`architecture.md`](./architecture.md).\n\n## Packages\n\nBun monorepo (`packages/*`). Acyclic graph: everything depends on `core`; `client` and\n`admin-ui` never depend on each other.\n\n| Package                   | What it is                                                                                                                                                                                                                                                                                            |\n| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **`@propustka/core`**     | Pure shared lib: action matcher (`*` / `prefix.*` / exact), `permits()`, `uuidv7()`, shared types, and the **`IamRpc`** contract the worker implements and the SDK consumes. No I/O, no deps.                                                                                                         |\n| **`@propustka/worker`**   | The IAM Worker. `WorkerEntrypoint` implementing the RPC surface + the `/admin/*` REST API + the admin SPA assets + a cron that prunes the auth log. D1 datastore, `jose` JWT validation, Cloudflare Access provisioning, `oblaka` provisioning.                                                       |\n| **`@propustka/client`**   | The app-facing SDK (the only published package): `IamClient`, `AuthContext`, `Capability`, `applyScope`, and `FakeIamClient` for `wrangler dev`. Depends only on `core`.                                                                                                                              |\n| **`@propustka/admin-ui`** | buzola + React admin SPA served by the worker at `/`. Manages principals, grants (named role or inline action set, over generic scope dimensions), custom policies, group→role mappings, API keys, capabilities; inspects each app's reconciled schema and role catalog; views the audit + auth logs. |\n\n## Quick start\n\nRequires **[Bun](https://bun.sh)** (≥ 1.3).\n\n```bash\nbun install\nbun run typecheck     # tsc --noEmit across all packages\nbun test              # 203 tests\nbun run lint          # biome\nbun run format        # dprint\n```\n\n## Local development\n\nLocal Cloudflare runtime is **[lopata](https://github.com/contember/lopata)** (a `wrangler dev`\ndrop-in on Bun). Bindings (D1, static assets) are backed by SQLite + files under `.lopata/`.\n\n### Click through the admin demo\n\n```bash\ncd packages/admin-ui \u0026\u0026 bun run build                  # build the admin SPA the worker serves\ncd ../worker \u0026\u0026 bun run oblaka                          # generate the worker's wrangler.jsonc\ncp .dev.vars.example .dev.vars                          # local secret placeholders (gitignored)\n(cd ../../examples/app \u0026\u0026 bun run oblaka)               # generate the example app's wrangler.jsonc\nbunx lopata d1 migrations apply propustka               # create the local D1 schema\nbunx lopata d1 execute propustka --file seed.dev.sql    # load sample data (optional, but populates the UI)\nbun run dev                                             # http://127.0.0.1:18191\n```\n\nOpen **http://127.0.0.1:18191** — the admin UI, fully clickable. There is no Cloudflare Access\nlocally, so the worker runs a **dev bypass**: when `ENVIRONMENT=local` and no Access JWT is\npresent it resolves a fixed `local-dev-admin` global admin (see `src/auth.ts`). Strictly local —\na real token still validates normally, so stage/prod never reach this branch.\n\n`packages/worker`'s `lopata.config.ts` also runs the [example app](./examples/app) as an\nauxiliary worker at **`/demo`**, so the example's audit writes land in the same local D1 the\nadmin UI reads:\n\n```bash\ncurl http://127.0.0.1:18191/demo     # the example authenticates + emits an `example.viewed` audit event\n```\n\n…then open the admin **Audit** page (or `GET /admin/audit?action=example.viewed`) to watch the\nrecords appear — the app → IAM `audit()` path over the service binding, end to end.\n\nThe example app also **owns its authz vocabulary** — scope dimensions, an action catalog, and\nroles — declared in code in [`examples/app/propustka.schema.ts`](./examples/app/propustka.schema.ts)\nas a typed `AppSchema`. Reconcile it into Propustka (Access-as-code, authz edition) via the\nidempotent `PUT /admin/apps/:app/schema` endpoint:\n\n```bash\ncd examples/app\nbun run provision-schema -- --dry-run                          # print the intended reconcile\nPROPUSTKA_URL=http://127.0.0.1:18191 bun run provision-schema  # push it (local dev bypass → no auth)\n```\n\nso the admin UI's role / scope / action pickers offer this app's real vocabulary. See\n[`examples/app/README.md`](./examples/app/README.md) for the full walkthrough.\n\nFor the admin UI with hot reload, run the worker as above and in another shell:\n\n```bash\ncd packages/admin-ui \u0026\u0026 bun run dev  # vite on http://127.0.0.1:18192, proxies /admin → :18191\n```\n\n**What still needs real Cloudflare Access** (cannot be exercised locally): validating a real\nAccess JWT, resolving IdP group membership via `get-identity`, and service-token provisioning.\nSee _Status_ below.\n\n## Using the SDK in an app\n\nIn the **app's** `oblaka.ts`, bind the IAM Worker by name:\n\n```ts\nimport { ServiceReference } from 'oblaka-iac'\n// ...\nbindings: {\n  IAM: new ServiceReference('propustka-worker'),\n}\n```\n\nIn app code:\n\n```ts\nimport { applyScope, FakeIamClient, IamClient } from '@propustka/client'\n\nconst iam = env.DEV\n\t? new FakeIamClient({ deny: ['project.settings.update'] }) // wrangler dev: no Access, no IAM Worker\n\t: new IamClient(env.IAM, 'app-projects')\n\nconst auth = await iam.authenticate(req)\nif (!auth.ok) return new Response(auth.reason, { status: auth.status }) // 401 or 403\n\n// can(action, scope?) — scope is a flat { type, value } coordinate the app owns;\n// omit it to require a global permission. `project` here is one declared dimension.\nif (!auth.can('project.settings.update', { type: 'project', value: id })) {\n\treturn new Response('forbidden', { status: 403 })\n}\n\n// list filtering by scope (three-state: all / some / none).\n// scopedTo(action, dimension) — the dimension is required; values are this app's\n// opaque scope values for that dimension.\nconst scope = auth.scopedTo('project.read', 'project')\nconst projects = applyScope(scope, {\n\tall: () =\u003e db.listAllProjects(),\n\tsome: (ids) =\u003e db.listProjects({ ids }), // WHERE id IN (...)\n\tnone: () =\u003e [],\n})\n\nctx.waitUntil(\n\tauth.audit({\n\t\taction: 'project.settings.update',\n\t\tresourceType: 'project',\n\t\tresourceId: id,\n\t\tdiff,\n\t}),\n)\n```\n\n## Deploy\n\n```bash\n# Vars (per-env ACCESS_APPS / TEAM / IAM_BOOTSTRAP_ADMINS) are read from the environment\n# by oblaka.ts on stage/prod. CF_API_TOKEN / CF_ACCOUNT_ID are Worker secrets (oblaka has\n# no secrets field) — provisioned out-of-band with `wrangler secret put`, never as vars.\ncd packages/admin-ui \u0026\u0026 bun run build\ncd ../worker\nbunx oblaka oblaka.ts --remote --env stage         # then --env prod\nwrangler secret put CF_API_TOKEN                    # and CF_ACCOUNT_ID, per deployed env\nwrangler d1 migrations apply propustka --remote\n```\n\nThe first admin is bootstrapped statelessly: set `IAM_BOOTSTRAP_ADMINS` (JSON array of emails);\nthose users resolve to global `admin` until removed from the env var.\n\n## Status\n\nImplemented and verified (typecheck, 203 unit tests, admin-ui build, `oblaka` config gen, a local\n`lopata` HTTP smoke, and the app↔IAM RPC path via [`examples/app`](./examples/app)). Two\nintegration points depend on a live Cloudflare/Access environment and are **implemented to spec\nbut not yet verified against real infrastructure**:\n\n1. **`get-identity`** group resolution — must be checked against a real Access-protected host.\n2. **Service-token provisioning** — the mint/principal/grant flow is implemented; adding the\n   token to the app's _Service Auth policy_ is left **manual for v1** (`policyInclusion:\n   'manual'`) pending confirmation that the Access policy API supports it.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcontember%2Fpropustka","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcontember%2Fpropustka","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcontember%2Fpropustka/lists"}