Skip to content
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
27 changes: 22 additions & 5 deletions .agents/docs/github-work-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,41 @@ When adding work, update **this file**—do not sprinkle `#N` into other paths.

## Commerce / Stripe / entitlements

Parent: [Website + Stripe: product suite checkout and subscriptions](https://github.com/BreadchainCoop/sigstack-bot/issues/13)
**Current epic:** [Epic: CipherSlate commerce — Stripe, entitlements, alpha](https://github.com/BreadchainCoop/sigstack-bot/issues/68) (children #53, #55–#65, #70)
**Superseded:** [#13](https://github.com/BreadchainCoop/sigstack-bot/issues/13) (closed — education / early site scope done)

### Fast alpha (outside epic)

| # | Title | Notes |
|---|--------|--------|
| [69](https://github.com/BreadchainCoop/sigstack-bot/issues/69) | feat: alpha code redeem via !link (static site path) | Tonight MVP; not a child of #68 |

### Related (not epic children)

| # | Title | Notes |
|---|--------|--------|
| [52](https://github.com/BreadchainCoop/sigstack-bot/issues/52) | decision: stripe checkout and webhook hosting architecture | Decision |
| [53](https://github.com/BreadchainCoop/sigstack-bot/issues/53) | feat: entitlement model and encrypted store on cvm | `entitlements.enc` / `EntitlementsStore` |
| [54](https://github.com/BreadchainCoop/sigstack-bot/issues/54) | ops: stripe products and prices for cipherslate plans | Catalog; SKUs follow `site/src/lib/content/en.ts` |

### Epic children (#68)

| # | Title | Notes |
|---|--------|--------|
| [53](https://github.com/BreadchainCoop/sigstack-bot/issues/53) | feat: entitlement model and encrypted store on cvm | Shipped; `entitlements.enc` / composition architecture |
| [55](https://github.com/BreadchainCoop/sigstack-bot/issues/55) | feat: stripe checkout session api from plan sku | Checkout session + success redirect query shape |
| [56](https://github.com/BreadchainCoop/sigstack-bot/issues/56) | feat: stripe webhooks and subscription lifecycle sync | |
| [57](https://github.com/BreadchainCoop/sigstack-bot/issues/57) | feat: !link and !claim-group for subscription binding | |
| [57](https://github.com/BreadchainCoop/sigstack-bot/issues/57) | feat: !link and !claim-group for subscription binding | Stripe `link_token` bind (alpha-only is #69) |
| [58](https://github.com/BreadchainCoop/sigstack-bot/issues/58) | feat: gate product commands on entitlements | |
| [59](https://github.com/BreadchainCoop/sigstack-bot/issues/59) | feat: alpha promo codes — 3-month bundle-all free path | `bundle-all-alpha` |
| [60](https://github.com/BreadchainCoop/sigstack-bot/issues/60) | ops: alpha code generation and revocation tooling | |
| [59](https://github.com/BreadchainCoop/sigstack-bot/issues/59) | feat: alpha promo codes — 3-month bundle-all free path | Superseded by #69 + #70 |
| [60](https://github.com/BreadchainCoop/sigstack-bot/issues/60) | ops: alpha code generation and revocation tooling | Minimal mint in #69; fuller ops later |
| [61](https://github.com/BreadchainCoop/sigstack-bot/issues/61) | feat: wire plans ctas to stripe checkout | |
| [62](https://github.com/BreadchainCoop/sigstack-bot/issues/62) | feat: checkout success cancel and alpha claim pages | `site/` commerce landings |
| [63](https://github.com/BreadchainCoop/sigstack-bot/issues/63) | feat: revise get-started flow and entitlement user comms | |
| [64](https://github.com/BreadchainCoop/sigstack-bot/issues/64) | feat: deploy commerce service and entitlements on phala cvm | |
| [65](https://github.com/BreadchainCoop/sigstack-bot/issues/65) | test: commerce and entitlement e2e test plan | |
| [70](https://github.com/BreadchainCoop/sigstack-bot/issues/70) | feat: alpha coexistence with Stripe link and paid gating | After Stripe `!link` spine |

**Recommended order (epic [#68](https://github.com/BreadchainCoop/sigstack-bot/issues/68)):** foundation #53+#62 (done) → parallel #63/#65 + related #52/#54 → #54→#55→#56 → #57 → #58 → #70 → #61 → #64 → finish #65 before prod enforce. Fast alpha #69 is **outside** this epic. Full write-up on the epic issue.

## Product suite / architecture

Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ jobs:
- run: npm run build
env:
BASE_PATH: /sigstack-bot/cypherslate
PUBLIC_SIGNAL_USERNAME_LINK: ${{ vars.PUBLIC_SIGNAL_USERNAME_LINK }}
PUBLIC_STRIPE_PORTAL_URL: ${{ vars.PUBLIC_STRIPE_PORTAL_URL }}
- name: Prepare Pages artifact for /sigstack-bot/cypherslate
shell: bash
run: |
Expand Down
4 changes: 4 additions & 0 deletions crates/signal-bot/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ edition.workspace = true
name = "signal-bot"
path = "src/main.rs"

[[bin]]
name = "mint-alpha"
path = "src/bin/mint-alpha.rs"

[dependencies]
# Workspace crates
signal-bot-core = { path = "../signal-bot-core" }
Expand Down
107 changes: 107 additions & 0 deletions crates/signal-bot/src/bin/mint-alpha.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//! Ops CLI: mint pending alpha entitlement codes into the encrypted store.
//!
//! ```bash
//! # On CVM / with dstack + volume:
//! cargo run -p signal-bot --bin mint-alpha -- --count 5
//!
//! # Env (defaults match compose):
//! # ENTITLEMENTS__STORAGE_PATH=/data/entitlements.enc
//! # DSTACK__SOCKET_PATH=/var/run/dstack.sock
//! # ENTITLEMENTS__LEGACY_COMPOSE_HASH= (optional)
//! ```

use anyhow::{bail, Context, Result};
use chrono::Utc;
use dstack_client::DstackClient;
use signal_bot::entitlements_store::EntitlementsStore;
use std::env;
use std::path::PathBuf;
use std::sync::Arc;
use tracing::info;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};

fn legacy_hashes(raw: &str) -> Vec<String> {
raw.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
}

#[tokio::main]
async fn main() -> Result<()> {
let _ = dotenvy::dotenv();
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
.with(tracing_subscriber::fmt::layer())
.init();

let args: Vec<String> = env::args().skip(1).collect();
let mut count: usize = 1;
let mut days: i64 = 90;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--count" => {
i += 1;
count = args
.get(i)
.context("--count needs a value")?
.parse()
.context("invalid --count")?;
}
"--days" => {
i += 1;
days = args
.get(i)
.context("--days needs a value")?
.parse()
.context("invalid --days")?;
}
"--help" | "-h" => {
eprintln!(
"Usage: mint-alpha [--count N] [--days 90]\n\
Env: ENTITLEMENTS__STORAGE_PATH (default /data/entitlements.enc)\n\
DSTACK__SOCKET_PATH (default /var/run/dstack.sock)\n\
ENTITLEMENTS__LEGACY_COMPOSE_HASH (optional)"
);
return Ok(());
}
other => bail!("unknown argument: {other}"),
}
i += 1;
}

let storage_path =
env::var("ENTITLEMENTS__STORAGE_PATH").unwrap_or_else(|_| "/data/entitlements.enc".into());
let socket = env::var("DSTACK__SOCKET_PATH").unwrap_or_else(|_| "/var/run/dstack.sock".into());
let legacy = env::var("ENTITLEMENTS__LEGACY_COMPOSE_HASH").unwrap_or_default();

let dstack = Arc::new(DstackClient::new(&socket));
let store = EntitlementsStore::open(
dstack,
PathBuf::from(&storage_path),
true,
legacy_hashes(&legacy),
)
.await;

let codes = store
.mint_alpha_codes(count, days)
.map_err(|e| anyhow::anyhow!(e))?;
store.flush().await.map_err(|e| anyhow::anyhow!(e))?;

info!(
count = codes.len(),
days,
path = %storage_path,
"minted alpha codes at {}",
Utc::now()
);

println!("# alpha codes (single-use, {days}-day expiry) — share /alpha?code=<token>");
for code in &codes {
println!("{code}");
}
Ok(())
}
Loading
Loading