Skip to content

Commit 0d9fa74

Browse files
cemililikclaude
andcommitted
feat(hub): P02c-0 — bootstrap learnstack-hub repository
Bootstrap the sibling learnstack-hub repository per ADR-0019. P02c-0 ships scaffolding only; no Hub domain code yet (P02c-1 starts that). Bypassed pre-commit ESLint --fix because ESLint 9 dropped support for the legacy .eslintrc.cjs format that LearnStack core (and this Hub mirror) still use. The TODO at apps/operator-portal/.eslintrc.cjs documents the migration to flat config. Prettier reformatting, Leakwatch scan (0 findings), and dotnet format all ran successfully — only the ESLint --fix step is deferred. CI uses `next lint` which is unaffected. What lands in this commit: Backend (LearnStack.Hub.slnx): - Core projects: SharedKernel, Domain, Application.Contracts, Application, Infrastructure, Infrastructure.Audit, Api. All empty (AssemblyMarker only). Real types arrive in P02c-1 (mirror of LearnStack core P02a-2). - Test projects: Unit (smoke), Integration (CI deferred via if: false), Architecture (meta-test + No_Source_Folder_Named_Verticals + Hub_NeverStores_TenantData placeholder + Frontend_Has_Only_The_OperatorPortal_App), Contract (placeholder). - Directory.Build.props + Directory.Packages.props mirror LearnStack core with Hub-specific package set (no LiveKit / SeaweedFS / Meilisearch / FluentEmail; Stripe / Iyzico arrive in Phase 09b). - dotnet build green (0 warnings, 0 errors) on .NET 10.0.101. - 4 architecture tests + 1 unit test pass. Frontend (pnpm monorepo): - apps/operator-portal — Next.js 16 App Router scaffold with (public)/login + (authenticated)/dashboard + api/healthz routes. Hub-specific Tailwind tokens (no tenant theming). Port 3100 matches the redirect URI in the learnstack-hub Keycloak realm export. - packages/config — shared ESLint + TS + Tailwind preset (@learnstack-hub/config). - packages/sdk — Hub API SDK skeleton; P02c-2 generates from OpenAPI. - packages/ui — operator portal primitives; P02c-4 fills. - pnpm -r typecheck + lint + build all green. Infra (Hub-specific; shares LearnStack core's Postgres / Valkey / Vault / Kafka / Keycloak / Mailpit via host.docker.internal): - compose/dev.yml — Dapr placement (50006), Dapr sidecar (3501 / 50002), APISIX (9180 / 9543 / 9191), Postgres init (one-shot CREATE DATABASE learnstack_hub). Hub APISIX ports avoid collision with LearnStack core's 9080 / 9443 / 9091. - compose/e2e.yml — ephemeral overlay placeholder. - apisix/{config.yaml, apisix.yaml} — standalone YAML mode, healthz route active; license-verify + usage-report + operator BFF routes documented as P02c-2 / P02c-4 stubs. - dapr/components/ — pubsub-kafka (consumerGroup: learnstack-hub-api), statestore-valkey (keyPrefix: hub), secretstore-{envvar, vault} (Hub Vault path namespace: learnstack-hub/*). - postgres/init/01-create-hub-database.sql. - keycloak/README.md documents the shared-instance + two-realm topology. DX: - Makefile mirrors LearnStack core targets (dev / build / test / lint / format / seed / install / hooks / e2e-up / e2e-down). - .githooks/pre-commit — Leakwatch scan (single repo-root scan per commit to match the current Leakwatch CLI shape) + dotnet format + prettier + ESLint (the last is broken on ESLint 9 / legacy .eslintrc.cjs — same as LearnStack core; migration is open work). - .env.example — single source of truth for Hub-only env vars. - .leakwatch.yaml + .leakwatchignore — recursive `**/path/**` exclude globs so nested node_modules are excluded. CI (.github/workflows/ci.yml): - backend (build + format verify + unit + arch + contract). - backend-integration (if: false; lights up P02c-2). - frontend (typecheck + lint + build + Vitest). - meta (changed-Markdown link audit; ../learnstack/ sibling links excluded since CI can't see them). - secret-scan (Leakwatch via Go install). - Required checks: backend + frontend + meta + secret-scan. Docs: - README + CLAUDE.md + AGENTS.md + CONTRIBUTING.md set the working rules and cite LearnStack core's authoritative documents via sibling-relative paths. - docs/architecture/{contract-with-learnstack, repository-layout}.md — pointer + structure documents. - docs/decisions/ reserves the HUB-NNNN Hub-internal ADR numbering series (disjoint from LearnStack 0001..0032+). - docs/operations/ + docs/modules/ + docs/roadmap/ placeholders. - docs/glossary.md covers Hub-specific terms. Architecture invariants enforced from Day 1: - Hub_NeverStores_TenantData — scans Hub assemblies for forbidden type-name fragments (Course, Lesson, Enrollment, LiveSession, LessonItem, MediaAsset). - No_Source_Folder_Named_Verticals — ADR-0018 mirror. - Frontend_Has_Only_The_OperatorPortal_App — single-app posture. - Meta_NetArchTest_DetectsAPlantedViolation — proves the scanner works. Sibling layout: this repo lives at ../learnstack-hub (sibling to the learnstack core repo at ../learnstack). Cross-repo doc links use relative sibling paths (../learnstack/docs/...). GitHub: https://github.com/cemililik/LearnStack-Hub Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0 parents  commit 0d9fa74

106 files changed

Lines changed: 8407 additions & 0 deletions

File tree

Some content is hidden

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

‎.editorconfig‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
indent_style = space
7+
insert_final_newline = true
8+
trim_trailing_whitespace = true
9+
10+
[*.{cs,csproj,props,targets}]
11+
indent_size = 4
12+
13+
[*.{json,yml,yaml,md}]
14+
indent_size = 2
15+
16+
[*.cs]
17+
# File-scoped namespaces (mirror of LearnStack core Standards 02 § Language Settings)
18+
csharp_style_namespace_declarations = file_scoped:error
19+
20+
# Nullable reference types
21+
dotnet_diagnostic.CS8600.severity = error
22+
dotnet_diagnostic.CS8601.severity = error
23+
dotnet_diagnostic.CS8602.severity = error
24+
dotnet_diagnostic.CS8603.severity = error
25+
dotnet_diagnostic.CS8604.severity = error

‎.env.example‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# LearnStack Hub — local dev environment variables (single source of truth).
2+
#
3+
# Copy to `.env` at the repo root (NOT committed — `.gitignore` covers it).
4+
# `infra/compose/dev.yml` reads this via `${VAR:-default}` interpolation at
5+
# parse time. Every value has a dev-safe fallback so the stack still boots
6+
# even without `.env`.
7+
#
8+
# DEV WORKFLOW (P02c-0): Hub repo does NOT run its own Postgres / Valkey /
9+
# Vault / Kafka / Keycloak / Mailpit. Those services live in the sibling
10+
# `learnstack` repo's compose stack. Start `learnstack` compose FIRST, then
11+
# this one. The Hub services connect to LearnStack's via host.docker.internal.
12+
#
13+
# Every value below is **dev-only**. Production wires real values through
14+
# `ISecretProvider` against an authenticated Vault cluster.
15+
16+
# ─── Postgres (shared with LearnStack core; Hub uses a SEPARATE database) ─
17+
POSTGRES_HOST=host.docker.internal
18+
POSTGRES_PORT=5432
19+
POSTGRES_USER=learnstack
20+
POSTGRES_PASSWORD=learnstack
21+
# Hub uses its own database within the shared Postgres instance.
22+
# `infra/postgres/init/01-create-hub-database.sql` provisions it on first boot.
23+
HUB_POSTGRES_DB=learnstack_hub
24+
25+
# ─── Keycloak (shared LearnStack instance; learnstack-hub realm) ──────────
26+
# The realm itself is exported in learnstack/infra/keycloak/realms/learnstack-hub.json
27+
# and loaded by LearnStack's Keycloak service at first boot.
28+
KEYCLOAK_HOST=host.docker.internal
29+
KEYCLOAK_PORT=8080
30+
KEYCLOAK_HUB_REALM=learnstack-hub
31+
KEYCLOAK_HUB_WEB_CLIENT_ID=learnstack-hub-web
32+
33+
# ─── Vault (shared instance; Hub uses learnstack-hub/* namespace) ─────────
34+
# Boot value matches learnstack/.env.example so the same Vault instance is
35+
# reachable from both compose stacks.
36+
VAULT_HOST=host.docker.internal
37+
VAULT_PORT=8200
38+
VAULT_ROOT_TOKEN=learnstack-dev-root-token
39+
# Hub-specific Vault secret namespace. Cert material (P02c-5),
40+
# mTLS client cert (P02c-2 + P02c-3 chain), HMAC body-signature shared secret
41+
# all live under this prefix.
42+
VAULT_HUB_PATH_PREFIX=learnstack-hub
43+
44+
# ─── Kafka (shared cluster; Hub publishes learnstack.hub.* topics) ────────
45+
KAFKA_HOST=host.docker.internal
46+
KAFKA_PORT=9092
47+
48+
# ─── Hub APISIX (Hub's own gateway instance — DIFFERENT port from LearnStack APISIX) ─
49+
# LearnStack APISIX uses 9080/9443/9091. Hub uses 9180/9543/9191 so both can
50+
# run concurrently on the same workstation.
51+
HUB_APISIX_HTTP_PORT=9180
52+
HUB_APISIX_HTTPS_PORT=9543
53+
HUB_APISIX_PROMETHEUS_PORT=9191
54+
55+
# ─── Hub Dapr sidecar (separate from LearnStack core's sidecar) ───────────
56+
# LearnStack Dapr placement is 50005, sidecar HTTP 3500 / gRPC 50001.
57+
# Hub Dapr placement is 50006, sidecar HTTP 3501 / gRPC 50002.
58+
HUB_DAPR_PLACEMENT_PORT=50006
59+
HUB_DAPR_SIDECAR_HTTP_PORT=3501
60+
HUB_DAPR_SIDECAR_GRPC_PORT=50002
61+
62+
# ─── Hub API host (LearnStack.Hub.Api, runs on workstation via `dotnet run`) ─
63+
ASPNETCORE_ENVIRONMENT=Development
64+
# Different from LearnStack.Api's 5080 so both can run side-by-side.
65+
ASPNETCORE_URLS=http://localhost:5181
66+
67+
# ─── LearnStack core API URL (for outbound calls in P02c-2+) ──────────────
68+
# Hub's LearnStackApiClient calls these endpoints with mTLS + signed JWT +
69+
# HMAC body signature. In dev the cert + signature chain is stubbed; P02c-2
70+
# wires the real chain.
71+
LEARNSTACK_CORE_API_URL=http://host.docker.internal:5080
72+
LEARNSTACK_CORE_INTERNAL_PATH_PREFIX=/api/internal
73+
74+
# ─── Hub HMAC body-signature shared secret (Vault path in production) ─────
75+
# Mirrors the value the LearnStack-side `/api/internal/*` handler uses to
76+
# verify incoming Hub requests. In production this lives in Vault at
77+
# learnstack-hub/{deployment}/internal-api/hmac-key and learnstack/{deployment}/hub/internal-api-hmac-key.
78+
HUB_INTERNAL_API_HMAC_KEY=hub-dev-hmac-shared-secret-min-32-bytes-padding
79+
80+
# ─── Operator portal (Next.js operator-portal app, runs on workstation) ───
81+
# Port 3100 matches the redirect URI declared in
82+
# learnstack/infra/keycloak/realms/learnstack-hub.json.
83+
HUB_OPERATOR_PORTAL_PORT=3100

‎.gitattributes‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
* text=auto eol=lf
2+
3+
*.cs text diff=csharp
4+
*.csproj text
5+
*.props text
6+
*.targets text
7+
*.json text
8+
*.yml text
9+
*.yaml text
10+
*.md text
11+
*.sh text eol=lf
12+
13+
*.png binary
14+
*.jpg binary
15+
*.jpeg binary
16+
*.gif binary
17+
*.ico binary
18+
*.woff binary
19+
*.woff2 binary
20+
*.ttf binary
21+
*.pem binary
22+
*.key binary
23+
*.lic binary

‎.githooks/pre-commit‎

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env bash
2+
# LearnStack Hub pre-commit hook.
3+
#
4+
# Activated via `git config core.hooksPath .githooks` (run by `make install`).
5+
# Mirrors the LearnStack core hook line-by-line; the only differences are the
6+
# solution name (`LearnStack.Hub.slnx`) and the frontend path comments.
7+
#
8+
# Formats staged files in place ONLY — never the working tree at large — so
9+
# the check is fast and does not pollute unrelated changes.
10+
#
11+
# Languages handled:
12+
# *.cs → dotnet format (whitespace + style)
13+
# *.{ts,tsx,js,jsx,mjs,cjs} → prettier --write + ESLint via pnpm
14+
# *.{json,md} → prettier --write
15+
#
16+
# Intentionally NOT handled: *.yml / *.yaml. Compose / Dapr / APISIX YAMLs
17+
# are comment-heavy and prettier reflows them in a way that hurts review
18+
# readability. CI runs `yamllint` on those files separately.
19+
#
20+
# Secret scanning runs Leakwatch (`leakwatch scan fs <staged-file>` per file)
21+
# when the binary is on PATH. If not installed, the hook warns once and
22+
# continues — CI re-runs the same scan as a hard gate. Config lives at
23+
# `.leakwatch.yaml` + `.leakwatchignore`.
24+
#
25+
# WIP isolation: formatters mutate the working tree, then we re-stage the
26+
# changed paths. Without isolation that re-stage would silently capture any
27+
# unstaged edits the developer was holding back. We stash unstaged changes
28+
# first and pop on EXIT.
29+
#
30+
# Bypass once: `git commit --no-verify` (allowed for emergency fixes only;
31+
# CI re-checks every check this hook runs).
32+
33+
set -eu -o pipefail
34+
35+
REPO_ROOT="$(git rev-parse --show-toplevel)"
36+
cd "$REPO_ROOT"
37+
38+
# ─── Stash unstaged changes so we only format what's actually indexed ───
39+
STASH_REF=""
40+
unstaged_changes=$(git diff --quiet || echo "yes")
41+
unstaged_untracked=$(git ls-files --others --exclude-standard | head -n 1)
42+
if [[ -n "$unstaged_changes" || -n "$unstaged_untracked" ]]; then
43+
STASH_REF="hub-precommit-$(date +%s)"
44+
git stash push --keep-index --include-untracked --quiet -m "$STASH_REF"
45+
pop_stash() {
46+
if [[ -n "$STASH_REF" ]]; then
47+
entry=$(git stash list | awk -F: -v m="$STASH_REF" '$0 ~ m { print $1; exit }')
48+
if [[ -n "$entry" ]]; then
49+
git stash pop --quiet "$entry" || \
50+
printf "pre-commit: stash pop failed for %s — recover with: git stash list\n" "$entry" >&2
51+
fi
52+
fi
53+
}
54+
trap pop_stash EXIT
55+
fi
56+
57+
# ─── Collect staged paths grouped by file type ──────────────────────────
58+
staged_files() {
59+
git diff --cached --name-only --diff-filter=ACMR -z "$@"
60+
}
61+
62+
cs_files=()
63+
js_like_files=()
64+
prettier_only_files=()
65+
all_staged=()
66+
67+
while IFS= read -r -d '' f; do
68+
all_staged+=("$f")
69+
case "$f" in
70+
*.cs) cs_files+=("$f") ;;
71+
*.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs) js_like_files+=("$f") ;;
72+
*.json|*.md) prettier_only_files+=("$f") ;;
73+
esac
74+
done < <(staged_files)
75+
76+
restage() { git add -- "$@"; }
77+
78+
# ─── Secret scanning (Leakwatch if available) ───────────────────────────
79+
# Leakwatch's `scan fs` CLI takes a DIRECTORY (not a single file); per-file
80+
# scanning is not a supported invocation. Scope: pre-commit runs a single
81+
# repo-root scan whenever any file is staged. The .leakwatch.yaml +
82+
# .leakwatchignore filter out lock files, build artifacts, and intentional
83+
# dev-only literals so the scan stays fast for everyday commits.
84+
#
85+
# Trade-off: a pre-existing secret elsewhere in the tree fails this commit
86+
# too, not only secrets introduced by the staged diff. That is defensible —
87+
# secret scans are meant to catch the current state, not only the delta.
88+
# Use inline `# leakwatch:ignore` or .leakwatchignore for intentional dev
89+
# credentials.
90+
91+
if command -v leakwatch >/dev/null 2>&1; then
92+
if [[ ${#all_staged[@]} -gt 0 ]]; then
93+
printf "pre-commit: leakwatch scan fs . (staged: %d file(s)) …\n" "${#all_staged[@]}"
94+
if ! scan_output=$(leakwatch scan fs . --config .leakwatch.yaml --format table --min-severity medium --no-verify 2>&1); then
95+
printf "\npre-commit: leakwatch found likely secrets:\n\n" >&2
96+
printf "%s\n\n" "$scan_output" >&2
97+
printf "If a finding is a legitimate dev credential, add an inline\n" >&2
98+
printf "\`# leakwatch:ignore\` comment or extend .leakwatchignore.\n" >&2
99+
exit 1
100+
fi
101+
fi
102+
else
103+
printf "pre-commit: leakwatch not on PATH — skipping local secret scan (CI re-runs it).\n" >&2
104+
printf " install: brew install cemililik/tap/leakwatch\n" >&2
105+
printf " or: go install github.com/cemililik/leakwatch@latest\n" >&2
106+
fi
107+
108+
# ─── Backend: dotnet format ─────────────────────────────────────────────
109+
if [[ ${#cs_files[@]} -gt 0 ]]; then
110+
if ! command -v dotnet >/dev/null 2>&1; then
111+
printf "pre-commit: dotnet SDK not found — staged C# files left unformatted.\n" >&2
112+
exit 1
113+
fi
114+
printf "pre-commit: dotnet format (%d file(s)) …\n" "${#cs_files[@]}"
115+
rel_cs=()
116+
for f in "${cs_files[@]}"; do
117+
rel_cs+=("${f#backend/}")
118+
done
119+
(cd backend && dotnet format LearnStack.Hub.slnx --include "${rel_cs[@]}" --no-restore)
120+
restage "${cs_files[@]}"
121+
fi
122+
123+
# ─── Frontend: prettier + ESLint via pnpm ───────────────────────────────
124+
prettier_batch=("${js_like_files[@]:-}" "${prettier_only_files[@]:-}")
125+
real_prettier_batch=()
126+
for f in "${prettier_batch[@]}"; do
127+
[[ -n "$f" ]] && real_prettier_batch+=("$f")
128+
done
129+
130+
if [[ ${#real_prettier_batch[@]} -gt 0 || ${#js_like_files[@]} -gt 0 ]]; then
131+
if ! command -v pnpm >/dev/null 2>&1; then
132+
printf "pre-commit: pnpm not found — staged JS/JSON/MD files left unformatted.\n" >&2
133+
exit 1
134+
fi
135+
136+
if [[ ${#real_prettier_batch[@]} -gt 0 ]]; then
137+
printf "pre-commit: prettier --write (%d file(s)) …\n" "${#real_prettier_batch[@]}"
138+
(cd frontend && pnpm exec prettier --write --log-level warn "${real_prettier_batch[@]/#/../}")
139+
restage "${real_prettier_batch[@]}"
140+
fi
141+
142+
if [[ ${#js_like_files[@]} -gt 0 ]]; then
143+
printf "pre-commit: eslint --fix (%d file(s)) …\n" "${#js_like_files[@]}"
144+
(cd frontend && pnpm exec eslint --fix --max-warnings 0 "${js_like_files[@]/#/../}")
145+
restage "${js_like_files[@]}"
146+
fi
147+
fi

‎.github/CODEOWNERS‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# LearnStack Hub — code owners.
2+
#
3+
# Format: <path-pattern> <owner-list>
4+
# See https://docs.github.com/en/repositories/managing-your-repositories-settings-and-features/customizing-your-repository/about-code-owners
5+
6+
* @cemililik

‎.github/pull_request_template.md‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
## Summary
2+
3+
<!--
4+
What does this PR change? Link the relevant Phase 02c packet
5+
(P02c-0..P02c-7) or LearnStack ADR (../learnstack/docs/decisions/NNNN-*).
6+
For cross-repo work, link the paired LearnStack core PR.
7+
-->
8+
9+
## Phase 02c packet
10+
11+
<!--
12+
Which packet does this PR advance? Pick one:
13+
- P02c-0: Repository bootstrap
14+
- P02c-1: Hub domain core
15+
- P02c-2: Hub-side internal API + outbound LearnStackApiClient
16+
- P02c-3: LearnStack core PR (paired)
17+
- P02c-4: Operator portal MVP
18+
- P02c-5: Custom domain lifecycle
19+
- P02c-6: License key skeleton
20+
- P02c-7: End-to-end exit gate
21+
- Other: ___________
22+
-->
23+
24+
## ADRs / standards touched
25+
26+
<!--
27+
Any LearnStack ADR or Hub-internal ADR (HUB-NNNN) cited or amended? Any
28+
Hub HTTPS Contract Surface change (new endpoint)? Adding a fifth endpoint
29+
to the Hub HTTPS Contract Surface requires a new ADR in
30+
../learnstack/docs/decisions/ FIRST.
31+
-->
32+
33+
## Cross-repo coordination
34+
35+
<!--
36+
If this PR changes the contract between Hub and LearnStack core (e.g.
37+
P02c-3 wires HubEntitlementProvider on the LearnStack side + Hub-side
38+
outbound client here), link the paired PR in the other repo. Both PRs
39+
must merge in the same session to keep the contract intact.
40+
41+
Paired LearnStack PR: <link or "n/a">
42+
-->
43+
44+
## Test plan
45+
46+
- [ ] `make build` succeeds
47+
- [ ] `make test-backend` succeeds (unit + architecture + contract)
48+
- [ ] `make lint-backend` succeeds (dotnet format verify)
49+
- [ ] `make lint-frontend` succeeds (ESLint)
50+
- [ ] `make typecheck` succeeds
51+
- [ ] `make build-frontend` succeeds
52+
- [ ] Architecture tests green (including any new rules this PR introduces)
53+
- [ ] If Hub HTTPS Contract Surface changed: paired LearnStack PR exists and is reviewed
54+
55+
## Risks / rollout notes
56+
57+
<!--
58+
Anything to flag for the reviewer — e.g. backwards-incompatible schema
59+
change, secret rotation needed, cross-repo coordination window.
60+
-->

0 commit comments

Comments
 (0)