Skip to content

Commit 496e1bd

Browse files
committed
feat: co-version onto @propustka/* 0.0.6 (native auth, seeded provisioning key)
propustka is fully native now (it issues its own tokens, no Cloudflare Access), published as @propustka/* 0.0.6. Roll vozka onto it: - Pin bump ^0.0.5 → ^0.0.6 (config, core, worker). - Drop reconcileAccess + AppAccess entirely — no CF Access edge to reconcile; per-path gates are runtime SDK config in each app. reconcileSchema auth { clientId, clientSecret } → { adminKey } (one px_ bearer); the plan loses its reconcile-access step. - vozka-config drops the `access` field + the AppAccess/AccessAppDecl/AccessRule re-exports. - Worker auth seam (src/iam.ts): IamClient.authenticate is gone → PropustkaAuth over the IAM binding (gates: service + human), with a vozka-synthesized dev-persona AuthContext for DEV. Set-Cookie (refreshed px_token) attached centrally in handleApi; 401 carries loginUrl. - Collapse PROPUSTKA_CLIENT_ID/_SECRET → one PROPUSTKA_PROVISIONING_KEY across cli/runner/worker. - @vozka/cli generates one px_ provisioning key + writes one Environment secret. Stage 1 (deploying propustka itself from the platform pipeline) stays deferred to the account bring-up — it also needs propustka's OIDC + signing config in the Environment. typecheck + 208 tests + lint + format all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AYk5MFbQjvsWEsPZDeeHWa
1 parent 345af0d commit 496e1bd

42 files changed

Lines changed: 477 additions & 457 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
# vozka
22

33
A deploy control plane for a Cloudflare Workers ecosystem. An app declares its full deploy surface
4-
— CF resources (`oblaka-iac`), propustka Access/authz, and a build pipeline — in one
4+
— CF resources (`oblaka-iac`), propustka authz (`schema`), and a build pipeline — in one
55
`vozka.config.ts`; vozka provisions + deploys it (CLI today; control-plane Worker + dashboard).
66

77
## Tech Stack
88

99
- **Bun** — runtime + workspaces. Libraries run TypeScript directly (`exports.bun``src`); no build step.
1010
- **TypeScript** strict, ESM (`"type": "module"`) everywhere.
1111
- **Cloudflare Workers** — Worker + Durable Objects + Containers + D1 + Queues + R2.
12-
- `oblaka-iac` (CF provisioning DSL), `@propustka/*` (Access edge + IAM), `@buzola/*` (SPA router).
12+
- `oblaka-iac` (CF provisioning DSL), `@propustka/*` (native auth + IAM, no Cloudflare Access), `@buzola/*` (SPA router).
1313

1414
## Commands
1515

@@ -50,6 +50,10 @@ resource primitive and the propustka declaration types, so a `vozka.config.ts` n
5050
- **`oblaka-iac` resolves from npm, pinned to `^0.0.17`** (the first published version with the programmatic
5151
`deploy()` the engine calls). The old `file:../oblaka` override is gone. vozka + oblaka + propustka are a
5252
co-versioned suite — bump the pin deliberately (every package + the runner image's `docker/package.json`).
53+
**`@propustka/* ^0.0.6`** is the native-auth model (propustka issues its own tokens, no Cloudflare Access):
54+
`reconcileSchema({ adminKey })` is the only reconcile, `PropustkaAuth` is the request-auth front door,
55+
the provisioning credential is one seeded `px_` key (`PROPUSTKA_PROVISIONING_KEY`). Bumping it again is a
56+
suite-wide co-version.
5357
- **`config`, `core`, `worker`, `runner` relax exactly two strict flags** (`noUncheckedIndexedAccess`,
5458
`noPropertyAccessFromIndexSignature`) ONLY to tolerate oblaka's raw-TS source — `runner` joined the set
5559
when it began hosting `vozka-runner.config.ts` (the executor split), the same reason `worker` relaxes

MIGRATION.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ Cloudflare account — never one vozka spanning accounts (vozka is single-accoun
55

66
## Current state (today)
77

8-
| Account | propustka | Apps deployed via `.github/workflows/deploy.yml` |
9-
| ------------- | --------- | ------------------------------------------------------------------------------------------- |
10-
| **contember** | live | **opice** (`deploy/prod` only), **poplach** (`deploy/prod`) |
11-
| **mangoweb** | live | **poplach** (`deploy/mangoweb`), **revizor** (`deploy/mangoweb`, mangoweb-only — no opice) |
8+
| Account | propustka | Apps deployed via `.github/workflows/deploy.yml` |
9+
| ------------- | --------- | ------------------------------------------------------------------------------------------ |
10+
| **contember** | live | **opice** (`deploy/prod` only), **poplach** (`deploy/prod`) |
11+
| **mangoweb** | live | **poplach** (`deploy/mangoweb`), **revizor** (`deploy/mangoweb`, mangoweb-only — no opice) |
1212

1313
revizor's `deploy.yml` carries a `deploy/prod → contember` target option, but it was never configured
1414
(only the `mangoweb` GitHub Environment exists, `REVIZOR_HOSTNAME = revizor.mgwsite.com`) — so revizor is

bun.lock

Lines changed: 9 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/cli-init-design.md

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
# `@vozka/cli init` — per-account base bring-up (design)
22

3-
Status: **Phase A built** (`@vozka/cli`, `vozka init <account>`); **Phase B pending** the propustka refactor.
4-
Supersedes the `scripts/bootstrap-*.ts` wizard (kept as legacy until the CLI is proven). Decided 2026-06-26
5-
with the operator.
3+
Status (2026-06-27): **Phase A built** + **Phase B landed except Stage 1 wiring**. propustka native-auth is
4+
published (`@propustka/* 0.0.6`) WITH the seed-from-env primitive (`PROPUSTKA_PROVISIONING_KEY`); vozka is
5+
co-versioned onto it — `reconcileAccess`/`AppAccess` dropped, `reconcileSchema({ adminKey })`, the worker
6+
auth migrated to `PropustkaAuth`, the key collapsed to one `px_`, and `@vozka/cli` generates it. The ONE
7+
remaining Phase B item is **wiring Stage 1 (propustka's own deploy) into `platform.yml`** — deferred to the
8+
account bring-up (#24) because it needs propustka's OIDC + signing config provisioned into the Environment.
9+
Supersedes the `scripts/bootstrap-*.ts` wizard (kept as legacy until the CLI is proven). Decided 2026-06-26.
610

711
## Goal
812

@@ -59,32 +63,56 @@ by BOTH stages — no minting, no propustka checkout, no branch-mismatch.
5963
| **propustka (Stage 1)** | reads it from env and **idempotently upserts** it as an admin/provisioning credential at deploy — the machine analog of `PROPUSTKA_BOOTSTRAP_ADMINS`. _(= the seed-from-env primitive to add in the propustka refactor.)_ |
6064
| **vozka (Stage 2)** | reads the **same** key and authenticates its `reconcileSchema`/`reconcileAccess` calls with it |
6165

62-
### ⚠️ Key SHAPE is the one open item — depends on the new propustka auth model
63-
64-
- **Old model:** `PROPUSTKA_CLIENT_ID` + `PROPUSTKA_CLIENT_SECRET` (Access service-token / client-credentials
65-
pair). vozka reads both today.
66-
- **New model (refactor):** credentials are single `px_` bearer keys (`issueKey`/`mintFromKey`). Then the
67-
provisioning key is likely **one `px_` token**, not an id/secret pair — e.g. env `PROPUSTKA_PROVISIONING_KEY`.
68-
The CLI generates `px_<random>`; propustka Stage 1 upserts a credential whose hash = hash(token) under a
69-
stable identity (so re-deploy is a no-op / rotation); vozka presents the token as a bearer.
70-
- **Co-version implication:** if provisioning auth collapses to a single key, vozka's `@propustka/client`
71-
reconcile auth changes from `(client_id, client_secret)` to one key — that's part of the
72-
`@propustka/* ^0.0.5 → ^0.0.6` bump (the co-versioned suite). Confirm the exact surface against the new
73-
`@propustka/client`.
74-
75-
**To finalize the seeded-key env contract + the vozka co-version change, I need:** the new propustka
76-
provisioning auth shape (single `px_` key vs id/secret), the seed-from-env env name propustka will read, and
77-
the `@propustka/client` reconcile auth signature in the new version.
66+
### Key SHAPE — RESOLVED (2026-06-27, against propustka `feat/propustka-native-auth`)
67+
68+
- **Single `px_` bearer.** A propustka credential is ONE opaque `px_<random>` token (SHA-256-hashed in the
69+
`credentials` table, plaintext returned once by `issueKey`). NOT an id/secret pair. So the provisioning key
70+
is one token in **one** env var (proposed `PROPUSTKA_PROVISIONING_KEY`), not `PROPUSTKA_CLIENT_ID`/`_SECRET`.
71+
- **Reconcile auth = `adminKey` bearer.** `reconcileSchema({ url, app, schema, adminKey })` sends
72+
`Authorization: Bearer ${adminKey}` (the `px_`). vozka passes the single provisioning key as `adminKey`.
73+
- **No `reconcileAccess` / no `AppAccess`.** CF Access is fully removed; the old `reconcileAccess` +
74+
`AppAccess`/`AccessAppDecl`/`AccessRule` are DELETED from `@propustka/*`. Per-path gating is now `AppGates`,
75+
**pure runtime SDK config consumed by `PropustkaAuth` in each app — NOT reconciled at deploy** and not a
76+
vozka concern. So vozka's whole "reconcile access" step + the `access` field on `defineApp` go away.
77+
- **`IamClient.authenticate(request)` is gone** (caller resolution is now server-side `resolveCaller`); the
78+
app-side request-auth surface is `PropustkaAuth`. vozka's control-plane worker auth must migrate to it.
79+
80+
### The ONE missing primitive on the propustka side — seed-from-env
81+
82+
propustka has `IAM_BOOTSTRAP_ADMINS` (admit a human by email at resolution time) but **no machine analog**:
83+
nothing reads a `px_` from env and admits it as a provisioning admin. Recommended (idiomatic, no DB writes):
84+
in `worker/src/auth.ts resolveCaller`, alongside the local-dev bypass + the bootstrap-admin email check, add a
85+
`PROPUSTKA_PROVISIONING_KEY` env — when a presented bearer's hash equals `hashToken(env)`, resolve a synthetic
86+
global-admin caller (`permissions: [{ action: '*', scope: null, source: 'bootstrap' }]`), no `credentials`
87+
row. Idempotent by construction (env compare), rotatable (change the env), no chicken-and-egg, no migration.
88+
89+
**Still required from propustka before vozka Phase B can land:** (1) the seed-from-env primitive above;
90+
(2) bump `@propustka/core` + `@propustka/client` 0.0.5 → 0.0.6 and **publish** (the branch is unmerged,
91+
unpublished — vozka on `^0.0.5` still resolves the OLD model).
7892

7993
## Build phases + dependencies
8094

8195
- **Phase A — propustka-agnostic CLI plumbing (buildable now).** `packages/cli` skeleton; `init` flow; CF
8296
token → account/zones; repo scaffold from templates; GitHub App (manifest flow, public-when-cross-org —
8397
already wired in `f58b910`); GitHub Environment + secrets/vars; `.env`; trigger. Treats the provisioning
8498
key as an opaque env value, so it needs nothing from propustka.
85-
- **Phase B — seeded key + propustka Stage 1 + vozka co-version (after the propustka refactor lands).**
86-
Finalize the key shape; wire Stage 1 (propustka deploy + seed) into `platform.yml`; bump vozka's
87-
`@propustka/*` pin + adapt reconcile auth; drop any temporary escape.
99+
- **Phase B — seeded key + propustka Stage 1 + vozka co-version (after propustka publishes 0.0.6).** This is
100+
BIGGER than "finalize the key shape" — the native-auth refactor reshapes the whole reconcile + auth surface.
101+
All of it is coupled to the `@propustka/* ^0.0.5 → ^0.0.6` bump (it won't typecheck against 0.0.5), so it
102+
lands as one change:
103+
1. **Drop `reconcileAccess` entirely** (`core/deploy.ts`, `core/runtime.ts`): no successor — gates are
104+
runtime SDK config. Remove `ReconcileAccessError`, `AccessReconciler`, the access reconcile call.
105+
2. **`reconcileSchema` auth** `{ clientId, clientSecret }``{ adminKey }` (one `px_` bearer).
106+
3. **Config surface** (`vozka-config`): drop `AppAccess`/`AccessAppDecl`/`AccessRule` import + re-export and
107+
the `access` field on `defineApp`; per-app configs (poplach/revizor) drop their `access` block.
108+
4. **Worker auth seam** (`worker/iam.ts`): `IamClient.authenticate(request)` is gone → migrate to
109+
`PropustkaAuth`; rework `BootstrapAdminAuthContext`/the guard.
110+
5. **Key threading**: collapse `PROPUSTKA_CLIENT_ID` + `_SECRET` → one `PROPUSTKA_PROVISIONING_KEY` across
111+
`core/cli.ts`, `runner/protocol.ts`, `worker/run-lifecycle.ts`, `worker/env.ts`.
112+
6. **@vozka/cli**: generate one `px_` (not `{clientId, clientSecret}`); one Environment secret
113+
`PROPUSTKA_PROVISIONING_KEY`; update `init.ts` + `templates/platform.yml` + README.
114+
7. **Pin bump** `^0.0.5 → ^0.0.6` in every package.json + `docker/package.json`.
115+
8. **Wire Stage 1** propustka deploy into `platform.yml` (propustka already ships a `vozka.config.ts`).
88116

89117
## Out of scope / later
90118

packages/cli/CLAUDE.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ the bring-up surface (it triggers CI, it does not deploy from the laptop).
2525
reuses external resources (GitHub App, vault key) instead of orphaning them.
2626
- **App visibility is DERIVED:** public iff any install repo is in a different org than the App's owner org
2727
(GitHub forbids a private App installing cross-org). Same-org stays private.
28-
- **The provisioning key is a SEEDED key (Phase B):** the CLI generates it; propustka Stage 1 will SEED it,
29-
vozka Stage 2 USES it. No local-propustka mint. Until Phase B lands the account is intentionally not live.
28+
- **The provisioning key is a SEEDED `px_` key:** the CLI generates one opaque `px_` bearer
29+
(`PROPUSTKA_PROVISIONING_KEY`); propustka (≥ 0.0.6) admits a bearer matching it as a synthetic admin
30+
(`resolveCaller`), and vozka reconciles its schema with it. No local-propustka mint. Stage 1 (deploying
31+
propustka itself from this pipeline) is deferred to the account bring-up — it also needs propustka's OIDC
32+
and signing config in the Environment.
3033
- **`@vozka/core` owns the deploy.** This package never runs `wrangler`/oblaka/the engine — it triggers the
3134
scaffolded GitHub Actions pipeline, which calls `vozka platform deploy`.
3235

packages/cli/src/init.ts

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* into `.env`, `gh` over stdin, and child env — never through `log.ts`.
1010
*/
1111

12-
import { randomBytes, randomUUID } from 'node:crypto'
12+
import { randomBytes } from 'node:crypto'
1313
import { findZone, listZones, resolveAccountId, verifyToken } from './cloudflare'
1414
import { fromEnv, persistEnv } from './envfile'
1515
import { configureEnvironment, triggerPlatformWorkflow } from './environment'
@@ -55,8 +55,7 @@ export async function runInit(account: string): Promise<void> {
5555
VOZKA_VAULT_KEY: vaultKey,
5656
GITHUB_APP_PRIVATE_KEY: app.pem,
5757
GITHUB_WEBHOOK_SECRET: app.webhookSecret,
58-
PROPUSTKA_CLIENT_ID: provisioning.clientId,
59-
PROPUSTKA_CLIENT_SECRET: provisioning.clientSecret,
58+
PROPUSTKA_PROVISIONING_KEY: provisioning,
6059
},
6160
vars: {
6261
VOZKA_DOMAIN: collected.vozkaDomain,
@@ -146,31 +145,24 @@ async function ensureVaultKey(): Promise<string> {
146145
return key
147146
}
148147

149-
/** The provisioning key (vozka's propustka credential). */
150-
interface ProvisioningKey {
151-
clientId: string
152-
clientSecret: string
153-
}
154-
155148
/**
156-
* Generate the operator-side provisioning key (the "seeded key"): random, stored once. Phase B wires
157-
* propustka Stage 1 to SEED it as an admin credential at deploy and vozka Stage 2 to USE it. Until then it
158-
* is a placeholder — the account is NOT live yet ("clean, wait for propustka"). An operator who already has
159-
* a key can pre-set PROPUSTKA_CLIENT_ID/_SECRET in env and it is reused verbatim.
149+
* Generate the operator-side provisioning key (the "seeded key"): a single opaque `px_` bearer, stored
150+
* once. propustka Stage 1 SEEDS it (the `PROPUSTKA_PROVISIONING_KEY` secret — `resolveCaller` admits a
151+
* bearer matching it as a synthetic admin) and vozka Stage 2 USES it to authenticate schema reconciles.
152+
* Shaped like a propustka-native key (`px_` + 160 bits base64url). An operator who already has one can
153+
* pre-set `PROPUSTKA_PROVISIONING_KEY` in env and it is reused verbatim.
160154
*/
161-
async function ensureProvisioningKey(): Promise<ProvisioningKey> {
162-
step('Provisioning key (PROPUSTKA_CLIENT_ID / _SECRET)')
163-
const id = fromEnv('PROPUSTKA_CLIENT_ID')
164-
const secret = fromEnv('PROPUSTKA_CLIENT_SECRET')
165-
if (id !== undefined && secret !== undefined) {
155+
async function ensureProvisioningKey(): Promise<string> {
156+
step('Provisioning key (PROPUSTKA_PROVISIONING_KEY)')
157+
const existing = fromEnv('PROPUSTKA_PROVISIONING_KEY')
158+
if (existing !== undefined) {
166159
ok('Reusing the provisioning key from .env (resume).')
167-
return { clientId: id, clientSecret: secret }
160+
return existing
168161
}
169-
const key = { clientId: randomUUID(), clientSecret: randomBytes(32).toString('base64url') }
170-
await persistEnv('PROPUSTKA_CLIENT_ID', key.clientId)
171-
await persistEnv('PROPUSTKA_CLIENT_SECRET', key.clientSecret)
162+
const key = `px_${randomBytes(20).toString('base64url')}`
163+
await persistEnv('PROPUSTKA_PROVISIONING_KEY', key)
172164
ok('Provisioning key generated + saved to .env.')
173-
detail('Phase B: propustka Stage 1 will SEED this; vozka uses it. Until then the account is not live (by design).')
165+
detail('propustka Stage 1 seeds this as an admin credential; vozka Stage 2 reconciles with it.')
174166
return key
175167
}
176168

packages/cli/src/templates/README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ To close the escape hatch once propustka grants you admin: set the `{{ACCOUNT}}`
3030
- Bump [`vozka.ref`](./vozka.ref) to a new `contember/vozka` commit/tag and push → redeploys the base.
3131
- Or _Actions → platform → Run workflow_ manually (build_runner_image stays false).
3232

33-
## Stage 1 — propustka (Phase B)
34-
35-
propustka is the authz root vozka depends on, so it belongs in this repo's pipeline. It's **not wired yet**
36-
propustka's deploy is being refactored. Once it lands, Stage 1 deploys propustka here and **seeds** the
37-
operator-generated provisioning key (`PROPUSTKA_CLIENT_ID`/`_SECRET`) that Stage 2 uses — no minting, no
38-
local propustka checkout.
33+
## Stage 1 — propustka (deferred to the account bring-up)
34+
35+
propustka is the authz root vozka depends on, so it belongs in this repo's pipeline. It is native-auth now
36+
and ships its own `vozka.config.ts`, so Stage 1 can deploy it here and **seed** the operator-generated
37+
provisioning key — the `PROPUSTKA_PROVISIONING_KEY` secret propustka admits as a synthetic admin (no minting,
38+
no local propustka checkout) and Stage 2 reconciles with. Wiring it needs propustka's own deploy config in
39+
this Environment (signing keys, OIDC secret + issuer/client, human email domains). Until then propustka
40+
deploys via its own repo pipeline; this Environment carries the shared `PROPUSTKA_PROVISIONING_KEY`.

0 commit comments

Comments
 (0)