Skip to content
This repository was archived by the owner on Oct 5, 2026. It is now read-only.
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
59 changes: 59 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: deploy

# The deployable auth Worker (auth#41): `main` -> staging, a `v*` tag ->
# production behind the environment approval gate.
on:
push:
branches: [main]
tags: ["v*"]

env:
CARGO_TERM_COLOR: always

jobs:
staging:
name: deploy staging
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.98.1"
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- name: Deploy (staging) + apply migrations
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy
preCommands: |
cargo install -q worker-build
npx wrangler d1 migrations apply DB --remote
- name: Smoke staging
run: ./smoke.sh "${{ vars.AUTH_STAGING_URL }}"

production:
name: deploy production
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
environment: production # the approval gate
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.98.1"
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- name: Deploy (production) + apply migrations
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --env production
preCommands: |
cargo install -q worker-build
npx wrangler d1 migrations apply DB --env production --remote
- name: Smoke production
run: ./smoke.sh "https://auth.factory0.ventures"
57 changes: 57 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ askama = "0.14.0"
cratefield-core = "0.3"
cratefield-testing = "0.1"
cratefield-adapter-sqlite = "0.1"
cratefield-runtime-cloudflare = "0.1"
cratefield-adapter-resend = "0.1"

# Versions mirror the harness workspace so the git harness crates and our
# direct dependencies unify into one dependency tree.
Expand Down Expand Up @@ -134,6 +136,9 @@ sha2 = { version = "0.11.0", default-features = false }
# Enabled by wasm leaf crates (same as the harness venture example):
# getrandom (via ulid/rand through cratefield-core) needs the opt-in
# wasm_js backend on wasm32-unknown-unknown.
# workers-rs runtime for the deployable auth Worker (auth#41). `http` stays
# OFF (harness note: it hangs D1); `d1` on for the database binding.
worker = { version = "0.8.5", default-features = false, features = ["d1"] }
getrandom = { version = "0.4.3", default-features = false, features = [
"wasm_js",
] }
Expand Down
31 changes: 31 additions & 0 deletions crates/auth-worker/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "factory0-auth-worker"
description = "The deployable auth Worker: composes the auth modules into one harness venture (auth#41)"
publish = false
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true

[lib]
crate-type = ["cdylib", "rlib"]

[lints]
workspace = true

[dependencies]
factory0-auth-core = { path = "../auth-core" }
factory0-auth-oidc = { path = "../auth-oidc" }
factory0-auth-passkeys = { path = "../auth-passkeys" }
factory0-auth-magic-link = { path = "../auth-magic-link" }
factory0-auth-password = { path = "../auth-password" }
factory0-auth-meta = { path = "../auth-meta" }
cratefield-core = { workspace = true }
cratefield-runtime-cloudflare = { workspace = true }
cratefield-adapter-resend = { workspace = true }
worker = { workspace = true }
async-trait = { workspace = true }

[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { workspace = true }
114 changes: 114 additions & 0 deletions crates/auth-worker/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//! The deployable auth Worker (issue #41): composes every merged auth module
//! into one harness venture and exposes the Cloudflare `fetch`/`scheduled`
//! entry points. Before this, the repository was library crates with no
//! artifact to deploy, so `auth.factory0.ventures` did not exist and every
//! consumer (Yoginini, the Cratefield control plane) was blocked.
//!
//! It is an ordinary harness venture — one Worker, one D1 — following the
//! `venture-backend-template` layout. Which login methods ship is simply which
//! modules are mounted here; the rest mount later without redeploying consumers.
//!
//! **Owner-only (needs-human):** `wrangler d1 create` for each environment, the
//! `HARNESS_SECRET`/signing material, the `auth.factory0.ventures` route on the
//! Kontinuum Cloudflare account, and the first production tag (auth#41).

#![forbid(unsafe_code)]

use std::sync::{Arc, OnceLock};

use async_trait::async_trait;
use cratefield_adapter_resend::Resend;
use cratefield_core::{Harness, MailError, Mailer, Message, SendOutcome, Venture};
use cratefield_runtime_cloudflare::{Cloudflare, FetchClient, serve, serve_scheduled};
use worker::{Context, Env, Request, Response, event};

/// The verified sending address (a `send.` subdomain verified in Resend).
const MAIL_FROM: &str = "no-reply@auth.factory0.ventures";

/// Reports a send as done without sending — used until `RESEND_API_KEY` is set,
/// so magic-link/verification requests still create their rows rather than
/// failing the whole flow. Real delivery begins the moment the key is present.
struct NoopMailer;

#[async_trait]
impl Mailer for NoopMailer {
async fn send(&self, _message: Message) -> Result<SendOutcome, MailError> {
Ok(SendOutcome::Sent {
id: "noop".to_owned(),
})
}
}

/// Resend when `RESEND_API_KEY` is present on the Worker `Env` (read from the
/// binding, since `std::env` is empty on Workers), else the capture-only no-op.
fn build_mailer(env: &Env) -> Arc<dyn Mailer> {
let key = env
.secret("RESEND_API_KEY")
.ok()
.map(|secret| secret.to_string())
.filter(|key| !key.is_empty());
match key {
Some(key) => Arc::new(Resend::new(
Arc::new(FetchClient),
Some(key),
MAIL_FROM,
None,
)),
None => Arc::new(NoopMailer),
}
}

static INSTANCE: OnceLock<(Harness, Cloudflare)> = OnceLock::new();

/// The composed auth venture: `auth-core` plus every merged login method, over
/// the Cloudflare runtime. Built once per isolate (ADR 0007: no ambient
/// request state).
fn instance(env: &Env) -> &'static (Harness, Cloudflare) {
INSTANCE.get_or_init(|| {
let mailer = build_mailer(env);
let harness = Harness::builder()
.venture(
Venture::new("factory0-auth", "auth.factory0.ventures")
.public_url("https://auth.factory0.ventures")
// Consumers call the discovery/JWKS documents (public GETs);
// the browser-facing authorize flow is a top-level redirect,
// not CORS. These are the first-party app origins.
.cors_origins([
"https://app.cratefield.com",
"https://cratefield.com",
"https://yoginini.us",
]),
)
// Magic-link renders its mail through the shared registry.
.templates(factory0_auth_magic_link::default_templates())
.module(factory0_auth_core::AuthCore::new())
.module(factory0_auth_oidc::Oidc::new())
.module(factory0_auth_passkeys::Passkeys::new())
.module(factory0_auth_magic_link::MagicLink::new())
.module(factory0_auth_password::Password::new())
.module(factory0_auth_meta::Meta::new())
.runtime(Cloudflare::new().db("DB").mailer_arc(Arc::clone(&mailer)))
.build()
.expect("the auth venture is a valid harness");
// The runtime `serve` resolves ports from must carry the mailer too.
let runtime = Cloudflare::new().db("DB").mailer_arc(mailer);
(harness, runtime)
})
}

#[event(fetch)]
/// Worker fetch entry point.
///
/// # Errors
///
/// Propagates `worker::Error` from the harness router.
pub async fn fetch(req: Request, env: Env, ctx: Context) -> worker::Result<Response> {
let (harness, runtime) = instance(&env);
serve(harness, runtime, req, env, ctx).await
}

#[event(scheduled)]
pub async fn scheduled(event: worker::ScheduledEvent, env: Env, ctx: worker::ScheduleContext) {
let (harness, runtime) = instance(&env);
serve_scheduled(harness, runtime, event, env, ctx).await;
}
1 change: 1 addition & 0 deletions migrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Collected by `fz migrations collect` (needs-human: run once the fz CLI is installed against this venture). Empty until then.
26 changes: 26 additions & 0 deletions smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Smoke test for the deployed auth Worker (auth#41): the health check and the
# OIDC discovery documents must all answer 200 over TLS.
#
# ./smoke.sh https://auth.factory0.ventures
set -euo pipefail

BASE="${1:?usage: smoke.sh <base-url>}"
BASE="${BASE%/}"

paths=(
/__health
/.well-known/openid-configuration
/.well-known/jwks.json
)

for path in "${paths[@]}"; do
code="$(curl -fsS -o /dev/null -w '%{http_code}' "${BASE}${path}" || true)"
if [ "$code" != "200" ]; then
echo "FAIL ${path} -> ${code}"
exit 1
fi
echo "ok ${path}"
done

echo "smoke passed against ${BASE}"
34 changes: 34 additions & 0 deletions wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# The deployable auth Worker (auth#41). One Worker + one D1 per environment,
# the shape of any harness venture (venture-backend-template).
#
# needs-human before first deploy: `wrangler d1 create` for each environment
# and paste the ids below; set HARNESS_SECRET (+ HARNESS_SECRET_PREVIOUS on
# rotation) and RESEND_API_KEY as secrets per environment; add the
# auth.factory0.ventures custom domain on the Kontinuum Cloudflare account.
name = "factory0-auth"
main = "build/worker/shim.mjs"
compatibility_date = "2026-09-01"
workers_dev = false

[build]
command = "cargo install -q worker-build && worker-build --release"

[observability]
enabled = true

# --- Staging (deployed from `main`) ---
[[d1_databases]]
binding = "DB"
database_name = "factory0-auth-staging"
database_id = "REPLACE_WITH_STAGING_D1_ID"
migrations_dir = "migrations"

# --- Production (deployed from a v* tag, behind the approval gate) ---
[env.production]
route = { pattern = "auth.factory0.ventures", custom_domain = true }

[[env.production.d1_databases]]
binding = "DB"
database_name = "factory0-auth-production"
database_id = "REPLACE_WITH_PRODUCTION_D1_ID"
migrations_dir = "migrations"
Loading