feat(git): GitHub App manifest self-provisioning + app-level webhook (#44)

* feat(branding): white-label — public /branding, admin write, shell theming

app_settings branding.* keys (product name, logo/favicon URLs, accent
color, docs/support links). Public GET /branding so login and shell
theme pre-auth; admin writes via PUT /settings.

useBranding() applies document.title, accent CSS var and favicon;
BrandWordmark replaces hardcoded wordmarks; admin Platform settings
gain a Branding form. MCP containr_branding_get + settings_update.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(invites): team invite links — admin create/revoke, public accept

Admins mint single-use invite links (sha256-hashed token at rest,
optional email binding, configurable TTL). Public endpoints let the
accept page validate the link and register — working even while
public signup is disabled.

- migration + sqlc queries/model for user_invites
- POST /admin/invites (returns one-time token+url), GET list, DELETE revoke
- GET /auth/invites/:token public validation, POST /auth/accept-invite
- CLI containr invites list|create|revoke; MCP invites tools
- /auth/accept-invite page (expired/used/invalid states, bound-email lock)
- Admin page Team invites section: create, copy link, revoke
- OpenAPI + generated types + CLI guide

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(admin): impersonate user — 15-min token, audit-logged

POST /admin/users/:id/impersonate mints a short-lived bearer token
(impersonated_by claim) for support/debugging. Admin accounts cannot
be impersonated; every mint is audit-logged with actor + target.

- CLI: containr admin impersonate <user-id> (works via CONTAINR_TOKEN)
- MCP: containr_admin_impersonate
- Admin UI: per-user Impersonate action → token modal with usage hint
- OpenAPI + generated types

Verified live: token resolves to target, admin target 403, audit row.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(git): GitHub App manifest self-provisioning + app-level webhook

Admins create the instance GitHub App in-browser instead of hand-
setting GITHUB_APP_* env vars:

- POST /admin/git/github-app/manifest builds the manifest + URL;
  the frontend auto-submits it to github.com
- /git/github-app/callback page exchanges the code via
  POST /admin/git/github-app/convert; credentials persist in
  app_settings (precedence over env)
- GET /admin/git/github-app reports configured/source
- POST /api/git/github-app/webhook — single app-level push receiver,
  HMAC-verified, dispatches via extracted dispatchPushToServices
- install-url + installation-token paths resolve settings→env
- CLI: containr admin github-app status|manifest|convert; MCP tools
- Git providers UI shows "Set up GitHub App" for admins when
  unconfigured

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomáš DvořákandDevin authored and GitHub committed 2026-10-09 10:43:12 +02:00
1 parent 50e39fe140
commit 00e69b3733
16 files changed
+1184 -28

No files matched your search

+275
View File
@@ -2717,6 +2717,63 @@ export interface paths {
};
trace?: never;
};
"/admin/users/{id}/impersonate": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Impersonate a user
* @description Mint a 15-minute bearer token acting as the target user. Admin-only; admin accounts cannot be impersonated and every mint is audit-logged. The token carries an `impersonated_by` claim.
*/
post: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Impersonation token */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
token?: string;
/** Format: date-time */
expires_at?: string;
user?: {
id?: string;
email?: string;
name?: string;
};
};
};
};
/** @description Target is an admin account */
403: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/projects/{id}": {
parameters: {
query?: never;
@@ -5661,6 +5718,162 @@ export interface paths {
patch?: never;
trace?: never;
};
"/admin/git/github-app": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* GitHub App provisioning status
* @description Shows whether the instance GitHub App is configured, and whether credentials come from app_settings or environment variables.
*/
get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Provisioning status */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
configured?: boolean;
/** @enum {string} */
source?: "settings" | "env" | "none";
app_id?: string;
slug?: string;
name?: string;
};
};
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/git/github-app/manifest": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Build GitHub App manifest
* @description Returns a GitHub App manifest for self-provisioning. The client POSTs `manifest` verbatim as a form field to `url`; GitHub then redirects to the instance's /git/github-app/callback with a code.
*/
post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": {
/** @description Public https base URL of this instance */
base_url: string;
name?: string;
/** @description Create the app under this GitHub organization */
organization?: string;
};
};
};
responses: {
/** @description Manifest + submission URL */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
url?: string;
/** @description JSON string — POST as form field `manifest` */
manifest?: string;
state?: string;
};
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/git/github-app/convert": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Exchange manifest callback code
* @description Exchanges the `code` from the manifest redirect for the app credentials and stores them in app_settings (overriding env vars).
*/
post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": {
code: string;
};
};
};
responses: {
/** @description App provisioned */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
app_id?: number;
slug?: string;
name?: string;
};
};
};
/** @description GitHub conversion failed */
502: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/git/providers/{providerId}": {
parameters: {
query?: never;
@@ -8386,6 +8599,68 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/git/github-app/webhook": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* GitHub App webhook receiver
* @description Single app-level webhook all installed repositories deliver to (configured via the manifest's hook_attributes). Verified with X-Hub-Signature-256 against the provisioned webhook secret. Push events enqueue deployments for services tracking the repo+branch.
*/
post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": Record<string, never>;
};
};
responses: {
/** @description Event accepted */
202: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
received?: boolean;
branch?: string;
deployments?: number;
ignored?: string;
};
};
};
/** @description Invalid signature */
401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description GitHub App webhook secret not configured */
503: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/agents/{id}/commands/{commandId}/result": {
parameters: {
query?: never;