Feat v0.6.3 dead code sweep (#38)
All checks were successful
All checks were successful
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #38.
This commit is contained in:
41
CHANGELOG.md
41
CHANGELOG.md
@@ -2,6 +2,47 @@
|
|||||||
|
|
||||||
All notable changes to Switchboard Core are documented here.
|
All notable changes to Switchboard Core are documented here.
|
||||||
|
|
||||||
|
## v0.6.3 — Dead Code Sweep + Registry Fix
|
||||||
|
|
||||||
|
Pre-fork hardening: fix broken registry install, add registry settings UI,
|
||||||
|
clean up dead code, narrow default bundle.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Registry install**: SDK was sending `{ url }` but the Go handler expects
|
||||||
|
`{ download_url }`, causing every registry Install click to return 400.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Package Registry settings**: New "Package Registry" section in Admin >
|
||||||
|
Settings with a URL input field for configuring the external registry.
|
||||||
|
- **`scripts/generate-registry.sh`**: Shell script that reads `.pkg` ZIPs,
|
||||||
|
extracts manifests, and emits a `registry.json` for self-hosted discovery.
|
||||||
|
- **`docs/PACKAGE-REGISTRY.md`**: Documents registry JSON format, admin
|
||||||
|
configuration, script usage, and self-hosting setup.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- **Dead Go code**: Orphaned channel/message type comments from
|
||||||
|
`interfaces.go`, unused `roleFilterType()` from `pages.go`, stale version
|
||||||
|
comment from `main.go`.
|
||||||
|
- **Dead vendor JS**: `marked.min.js` (40KB) and `purify.min.js` (22KB) —
|
||||||
|
unused vendor copies with zero production imports. Removed cache entries
|
||||||
|
from `sw.js`.
|
||||||
|
- **`dev.html`**: 676-line standalone dev gallery, not referenced by anything.
|
||||||
|
- **Pre-fork version comments**: Stripped all chat-switchboard version
|
||||||
|
annotations across ~140 files — standalone comments, inline suffixes,
|
||||||
|
section headers, and config field docs. Reworded to describe features
|
||||||
|
rather than reference pre-fork version numbers.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Default bundle narrowed** from 10 packages to 5: `notes`, `chat`,
|
||||||
|
`chat-core`, `mermaid-renderer`, `schedules`. All other packages remain
|
||||||
|
available via `BUNDLED_PACKAGES=*` or explicit list.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v0.6.2 — Docs Polish + Dynamic OpenAPI
|
## v0.6.2 — Docs Polish + Dynamic OpenAPI
|
||||||
|
|
||||||
Docs surface polish and dynamic OpenAPI spec with extension route merging.
|
Docs surface polish and dynamic OpenAPI spec with extension route merging.
|
||||||
|
|||||||
22
ROADMAP.md
22
ROADMAP.md
@@ -1,6 +1,6 @@
|
|||||||
# Switchboard Core — Roadmap
|
# Switchboard Core — Roadmap
|
||||||
|
|
||||||
## Current: v0.6.2 — Docs Polish + Dynamic OpenAPI
|
## Current: v0.6.3 — Dead Code Sweep + Registry Fix
|
||||||
|
|
||||||
Self-hosted extensible platform. Auth, identity, packages, Starlark sandbox,
|
Self-hosted extensible platform. Auth, identity, packages, Starlark sandbox,
|
||||||
storage, realtime, and ops are kernel primitives. Everything else is an extension.
|
storage, realtime, and ops are kernel primitives. Everything else is an extension.
|
||||||
@@ -81,16 +81,16 @@ Pure cleanup. No behavior changes except fixing the broken registry install flow
|
|||||||
|
|
||||||
| Step | Status | Description |
|
| Step | Status | Description |
|
||||||
|------|--------|-------------|
|
|------|--------|-------------|
|
||||||
| Fix registry install | ☐ | SDK sends `{ url }`, handler expects `{ download_url }`. Fix `api-domains.js` to send `{ download_url: url }`. Every Install click currently returns 400. |
|
| Fix registry install | ✅ | SDK sends `{ url }`, handler expects `{ download_url }`. Fix `api-domains.js` to send `{ download_url: url }`. Every Install click currently returns 400. |
|
||||||
| Registry settings UI | ☐ | Add "Package Registry" section to admin settings with URL input field. Only way to configure registry today is a raw `PUT /api/v1/admin/settings/package_registry` — no user will find it. |
|
| Registry settings UI | ✅ | Add "Package Registry" section to admin settings with URL input field. Only way to configure registry today is a raw `PUT /api/v1/admin/settings/package_registry` — no user will find it. |
|
||||||
| Registry tooling + docs | ☐ | `scripts/generate-registry.sh` scans a directory of `.pkg` files and emits registry JSON. `docs/PACKAGE-REGISTRY.md` documents the format. |
|
| Registry tooling + docs | ✅ | `scripts/generate-registry.sh` scans a directory of `.pkg` files and emits registry JSON. `docs/PACKAGE-REGISTRY.md` documents the format. |
|
||||||
| Delete dead kernel Go | ☐ | `store/interfaces.go:178–183` — orphaned ChannelListFilter comments. `pages/pages.go:922–930` — `roleFilterType()` + template registration (chat vestige, maps nonexistent roles). `main.go:67` — orphaned provider-type comment. |
|
| Delete dead kernel Go | ✅ | `store/interfaces.go:178–183` — orphaned ChannelListFilter comments. `pages/pages.go:922–930` — `roleFilterType()` + template registration (chat vestige, maps nonexistent roles). `main.go:67` — orphaned provider-type comment. |
|
||||||
| Delete dead vendor JS | ☐ | `vendor/marked.min.js` and `vendor/purify.min.js` — 62KB, zero production imports. Only referenced in test helpers. |
|
| Delete dead vendor JS | ✅ | `vendor/marked.min.js` and `vendor/purify.min.js` — 62KB, zero production imports. Only referenced in test helpers. |
|
||||||
| Delete `dev.html` | ☐ | 676 lines, not imported by anything. Dead. |
|
| Delete `dev.html` | ✅ | 676 lines, not imported by anything. Dead. |
|
||||||
| Remove `dashboard` from default bundle | ☐ | Requires `legacy-sdk` (doesn't exist), auto-installs as dormant on fresh installs. Confusing. Remove from `defaultBundledPackages`. |
|
| Remove `dashboard` from default bundle | ✅ | Requires `legacy-sdk` (doesn't exist), auto-installs as dormant on fresh installs. Confusing. Remove from `defaultBundledPackages`. |
|
||||||
| Remove `hello-dashboard` | ☐ | Proof-of-concept from early development. Move to `examples/` or delete. |
|
| Remove `hello-dashboard` | ✅ | Proof-of-concept from early development. Move to `examples/` or delete. |
|
||||||
| Strip stale version comments | ☐ | 60+ files have `// v0.29.x:`, `// v0.33.x:` pre-fork comments. Single sed pass. |
|
| Strip stale version comments | ✅ | 60+ files have `// v0.29.x:`, `// v0.33.x:` pre-fork comments. Single sed pass. |
|
||||||
| Narrow default bundle | ☐ | Default: `notes`, `chat`, `chat-core`, `mermaid-renderer`, `schedules`. Everything else available via registry or `BUNDLED_PACKAGES=*`. |
|
| Narrow default bundle | ✅ | Default: `notes`, `chat`, `chat-core`, `mermaid-renderer`, `schedules`. Everything else available via registry or `BUNDLED_PACKAGES=*`. |
|
||||||
|
|
||||||
### v0.6.4 — Admin Health/Metrics Tab + Cluster Merge
|
### v0.6.4 — Admin Health/Metrics Tab + Cluster Merge
|
||||||
|
|
||||||
|
|||||||
@@ -21,20 +21,22 @@ The production Docker image ships with pre-built packages. A **curated default s
|
|||||||
| Package | Type | Description |
|
| Package | Type | Description |
|
||||||
|---------|------|-------------|
|
|---------|------|-------------|
|
||||||
| notes | surface | Markdown notes with graph view |
|
| notes | surface | Markdown notes with graph view |
|
||||||
|
| chat | surface | Real-time chat surface |
|
||||||
| chat-core | library | Conversations, messages, read cursors |
|
| chat-core | library | Conversations, messages, read cursors |
|
||||||
| workflow-chat | extension | Chat integration for workflow stages |
|
| mermaid-renderer | extension | Diagram rendering |
|
||||||
| dashboard | surface | Dashboard surface |
|
| schedules | surface | Cron task management UI |
|
||||||
| workflow-demo | surface | Interactive walkthrough with diagrams |
|
|
||||||
| bug-report-triage | workflow | Public entry, severity routing, SLA timers |
|
|
||||||
| content-approval | workflow | Multi-party signoff example |
|
|
||||||
| employee-onboarding | workflow | Automated provisioning + manager signoff |
|
|
||||||
|
|
||||||
#### Opt-in (ship in image, require `BUNDLED_PACKAGES` to enable)
|
#### Opt-in (ship in image, require `BUNDLED_PACKAGES` to enable)
|
||||||
|
|
||||||
| Package | Type | Description |
|
| Package | Type | Description |
|
||||||
|---------|------|-------------|
|
|---------|------|-------------|
|
||||||
| tasks | full | Kanban/list task manager with webhooks |
|
| tasks | full | Kanban/list task manager with webhooks |
|
||||||
| schedules | surface | Cron task management UI |
|
| workflow-chat | extension | Chat integration for workflow stages |
|
||||||
|
| dashboard | surface | Dashboard surface |
|
||||||
|
| workflow-demo | surface | Interactive walkthrough with diagrams |
|
||||||
|
| bug-report-triage | workflow | Public entry, severity routing, SLA timers |
|
||||||
|
| content-approval | workflow | Multi-party signoff example |
|
||||||
|
| employee-onboarding | workflow | Automated provisioning + manager signoff |
|
||||||
| editor | surface | Rich markdown editor |
|
| editor | surface | Rich markdown editor |
|
||||||
| hello-dashboard | surface | Welcome/getting started surface |
|
| hello-dashboard | surface | Welcome/getting started surface |
|
||||||
| team-activity-log | surface | Team activity feed |
|
| team-activity-log | surface | Team activity feed |
|
||||||
@@ -45,7 +47,6 @@ The production Docker image ships with pre-built packages. A **curated default s
|
|||||||
| gitea-client | library | Gitea API integration library |
|
| gitea-client | library | Gitea API integration library |
|
||||||
| js-sandbox | extension | JavaScript sandbox |
|
| js-sandbox | extension | JavaScript sandbox |
|
||||||
| katex-renderer | extension | LaTeX rendering |
|
| katex-renderer | extension | LaTeX rendering |
|
||||||
| mermaid-renderer | extension | Diagram rendering |
|
|
||||||
| regex-tester | extension | Regex testing tool |
|
| regex-tester | extension | Regex testing tool |
|
||||||
| icd-test-runner | surface | E2E API test suite |
|
| icd-test-runner | surface | E2E API test suite |
|
||||||
| sdk-test-runner | surface | SDK feature test suite |
|
| sdk-test-runner | surface | SDK feature test suite |
|
||||||
|
|||||||
94
docs/PACKAGE-REGISTRY.md
Normal file
94
docs/PACKAGE-REGISTRY.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Package Registry
|
||||||
|
|
||||||
|
The package registry lets admins browse and install packages from an external
|
||||||
|
JSON index — a lightweight alternative to manually uploading `.pkg` files.
|
||||||
|
|
||||||
|
## Registry JSON Format
|
||||||
|
|
||||||
|
The registry is a static JSON file matching the `RegistryResponse` struct:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"id": "notes",
|
||||||
|
"title": "Notes",
|
||||||
|
"version": "0.8.0",
|
||||||
|
"description": "Markdown notes with backlinks and graph view",
|
||||||
|
"author": "switchboard",
|
||||||
|
"type": "extension",
|
||||||
|
"tier": "core",
|
||||||
|
"download_url": "https://cdn.example.com/pkg/notes.pkg",
|
||||||
|
"size": 48200,
|
||||||
|
"updated_at": "2026-03-20T00:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Fields
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|----------------|------------------------------------------------|
|
||||||
|
| `id` | Package identifier (matches `manifest.json`) |
|
||||||
|
| `title` | Display name |
|
||||||
|
| `version` | Semver version string |
|
||||||
|
| `description` | Short description |
|
||||||
|
| `download_url` | HTTPS URL to the `.pkg` file (must be HTTPS) |
|
||||||
|
|
||||||
|
### Optional Fields
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|--------------|------------------------------------------|
|
||||||
|
| `author` | Package author |
|
||||||
|
| `type` | `extension` or `library` |
|
||||||
|
| `tier` | `core`, `official`, or `community` |
|
||||||
|
| `size` | File size in bytes |
|
||||||
|
| `updated_at` | ISO 8601 timestamp of last update |
|
||||||
|
|
||||||
|
## Configuring the Registry URL
|
||||||
|
|
||||||
|
### Via Admin UI
|
||||||
|
|
||||||
|
Navigate to **Admin > Settings > Package Registry** and enter the registry URL.
|
||||||
|
|
||||||
|
### Via API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PUT /api/v1/admin/settings/package_registry \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"value": {"url": "https://cdn.example.com/pkg/registry.json"}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generating a Registry
|
||||||
|
|
||||||
|
Use `scripts/generate-registry.sh` to build a `registry.json` from a
|
||||||
|
directory of `.pkg` files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default: reads dist/, uses placeholder base URL
|
||||||
|
./scripts/generate-registry.sh
|
||||||
|
|
||||||
|
# Custom directory and base URL
|
||||||
|
./scripts/generate-registry.sh ./my-packages https://cdn.example.com/pkg > registry.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Requirements: `jq`, `unzip`, and `stat` (GNU or BSD).
|
||||||
|
|
||||||
|
## Self-Hosting
|
||||||
|
|
||||||
|
Any HTTPS-capable file server works. Upload your `.pkg` files and the
|
||||||
|
generated `registry.json` to the same directory, then point the admin
|
||||||
|
setting to the `registry.json` URL.
|
||||||
|
|
||||||
|
Example with a static file server:
|
||||||
|
|
||||||
|
```
|
||||||
|
/var/www/packages/
|
||||||
|
registry.json
|
||||||
|
notes.pkg
|
||||||
|
chat.pkg
|
||||||
|
chat-core.pkg
|
||||||
|
```
|
||||||
|
|
||||||
|
The registry is fetched and cached for 5 minutes on each browse request.
|
||||||
81
scripts/generate-registry.sh
Executable file
81
scripts/generate-registry.sh
Executable file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# generate-registry.sh — Build a registry.json from a directory of .pkg files.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/generate-registry.sh [PKG_DIR] [BASE_URL]
|
||||||
|
#
|
||||||
|
# Arguments:
|
||||||
|
# PKG_DIR Directory containing .pkg files (default: dist/)
|
||||||
|
# BASE_URL HTTPS base URL for download links (default: https://example.com/packages)
|
||||||
|
#
|
||||||
|
# Output:
|
||||||
|
# Writes registry.json to stdout. Redirect to a file:
|
||||||
|
# ./scripts/generate-registry.sh dist/ https://cdn.example.com/pkg > registry.json
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PKG_DIR="${1:-dist}"
|
||||||
|
BASE_URL="${2:-https://example.com/packages}"
|
||||||
|
|
||||||
|
# Strip trailing slash
|
||||||
|
BASE_URL="${BASE_URL%/}"
|
||||||
|
|
||||||
|
if [ ! -d "$PKG_DIR" ]; then
|
||||||
|
echo "Error: directory '$PKG_DIR' not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect entries
|
||||||
|
entries=()
|
||||||
|
|
||||||
|
for pkg in "$PKG_DIR"/*.pkg; do
|
||||||
|
[ -f "$pkg" ] || continue
|
||||||
|
|
||||||
|
# Extract manifest.json from the ZIP archive
|
||||||
|
manifest=$(unzip -p "$pkg" manifest.json 2>/dev/null) || {
|
||||||
|
echo "Warning: skipping $pkg (no manifest.json)" >&2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read fields from manifest
|
||||||
|
id=$(echo "$manifest" | jq -r '.id // empty')
|
||||||
|
title=$(echo "$manifest" | jq -r '.title // .id')
|
||||||
|
version=$(echo "$manifest" | jq -r '.version // "0.0.0"')
|
||||||
|
description=$(echo "$manifest" | jq -r '.description // ""')
|
||||||
|
author=$(echo "$manifest" | jq -r '.author // ""')
|
||||||
|
type=$(echo "$manifest" | jq -r '.type // "extension"')
|
||||||
|
tier=$(echo "$manifest" | jq -r '.tier // "community"')
|
||||||
|
|
||||||
|
if [ -z "$id" ]; then
|
||||||
|
echo "Warning: skipping $pkg (no id in manifest)" >&2
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
filename=$(basename "$pkg")
|
||||||
|
size=$(stat -c%s "$pkg" 2>/dev/null || stat -f%z "$pkg" 2>/dev/null || echo 0)
|
||||||
|
updated_at=$(date -r "$pkg" -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
entry=$(jq -n \
|
||||||
|
--arg id "$id" \
|
||||||
|
--arg title "$title" \
|
||||||
|
--arg version "$version" \
|
||||||
|
--arg description "$description" \
|
||||||
|
--arg author "$author" \
|
||||||
|
--arg type "$type" \
|
||||||
|
--arg tier "$tier" \
|
||||||
|
--arg download_url "$BASE_URL/$filename" \
|
||||||
|
--argjson size "$size" \
|
||||||
|
--arg updated_at "$updated_at" \
|
||||||
|
'{id: $id, title: $title, version: $version, description: $description,
|
||||||
|
author: $author, type: $type, tier: $tier, download_url: $download_url,
|
||||||
|
size: $size, updated_at: $updated_at}')
|
||||||
|
|
||||||
|
entries+=("$entry")
|
||||||
|
done
|
||||||
|
|
||||||
|
# Build final JSON
|
||||||
|
if [ ${#entries[@]} -eq 0 ]; then
|
||||||
|
echo '{"packages": []}' | jq .
|
||||||
|
else
|
||||||
|
printf '%s\n' "${entries[@]}" | jq -s '{packages: .}'
|
||||||
|
fi
|
||||||
@@ -26,18 +26,18 @@ type Config struct {
|
|||||||
// Seed users (dev/test only) — CSV: "user:pass:role,user2:pass2:role2"
|
// Seed users (dev/test only) — CSV: "user:pass:role,user2:pass2:role2"
|
||||||
SeedUsers string
|
SeedUsers string
|
||||||
|
|
||||||
// API key encryption (required for v0.9.4+)
|
// API key encryption (required+)
|
||||||
// Used to derive AES-256 key for global/team provider API keys.
|
// Used to derive AES-256 key for global/team provider API keys.
|
||||||
// Personal keys use per-user UEK (derived from password).
|
// Personal keys use per-user UEK (derived from password).
|
||||||
EncryptionKey string
|
EncryptionKey string
|
||||||
|
|
||||||
// File storage (v0.12.0+)
|
// File storage
|
||||||
// STORAGE_BACKEND: "pvc" or "s3". Empty = auto-detect (pvc if path writable).
|
// STORAGE_BACKEND: "pvc" or "s3". Empty = auto-detect (pvc if path writable).
|
||||||
// STORAGE_PATH: mount point for PVC backend (default /data/storage).
|
// STORAGE_PATH: mount point for PVC backend (default /data/storage).
|
||||||
StorageBackend string
|
StorageBackend string
|
||||||
StoragePath string
|
StoragePath string
|
||||||
|
|
||||||
// S3-compatible storage (v0.12.0+)
|
// S3-compatible storage
|
||||||
// Works with AWS S3, MinIO, Ceph RGW, or any S3-compatible API.
|
// Works with AWS S3, MinIO, Ceph RGW, or any S3-compatible API.
|
||||||
S3Endpoint string // custom endpoint URL (required for MinIO/Ceph, optional for AWS)
|
S3Endpoint string // custom endpoint URL (required for MinIO/Ceph, optional for AWS)
|
||||||
S3Bucket string // bucket name (required when STORAGE_BACKEND=s3)
|
S3Bucket string // bucket name (required when STORAGE_BACKEND=s3)
|
||||||
@@ -47,16 +47,16 @@ type Config struct {
|
|||||||
S3Prefix string // optional key prefix within bucket (e.g. "switchboard/")
|
S3Prefix string // optional key prefix within bucket (e.g. "switchboard/")
|
||||||
S3ForcePathStyle bool // use path-style URLs (required for MinIO, most self-hosted)
|
S3ForcePathStyle bool // use path-style URLs (required for MinIO, most self-hosted)
|
||||||
|
|
||||||
// Structured logging (v0.33.0)
|
// Structured logging
|
||||||
// LOG_FORMAT: "text" (default, backward-compatible) or "json" (structured).
|
// LOG_FORMAT: "text" (default, backward-compatible) or "json" (structured).
|
||||||
// LOG_LEVEL: "debug", "info" (default), "warn", "error".
|
// LOG_LEVEL: "debug", "info" (default), "warn", "error".
|
||||||
LogFormat string
|
LogFormat string
|
||||||
LogLevel string
|
LogLevel string
|
||||||
|
|
||||||
// Auth mode (v0.24.0): "builtin" (default) | "mtls" | "oidc"
|
// Auth mode: "builtin" (default) | "mtls" | "oidc"
|
||||||
AuthMode string
|
AuthMode string
|
||||||
|
|
||||||
// mTLS (v0.24.1)
|
// mTLS
|
||||||
// Headers injected by TLS-terminating reverse proxy.
|
// Headers injected by TLS-terminating reverse proxy.
|
||||||
MTLSHeaderDN string // default "X-SSL-Client-DN"
|
MTLSHeaderDN string // default "X-SSL-Client-DN"
|
||||||
MTLSHeaderVerify string // default "X-SSL-Client-Verify"
|
MTLSHeaderVerify string // default "X-SSL-Client-Verify"
|
||||||
@@ -64,7 +64,7 @@ type Config struct {
|
|||||||
MTLSAutoActivate bool // auto-activate new users (default true)
|
MTLSAutoActivate bool // auto-activate new users (default true)
|
||||||
MTLSDefaultTeam string // team ID for auto-provisioned users (optional)
|
MTLSDefaultTeam string // team ID for auto-provisioned users (optional)
|
||||||
|
|
||||||
// Bundled packages (v0.3.8, curated v0.5.4)
|
// Bundled packages
|
||||||
// SKIP_BUNDLED_PACKAGES: set true to disable auto-install of bundled packages on first run.
|
// SKIP_BUNDLED_PACKAGES: set true to disable auto-install of bundled packages on first run.
|
||||||
// BUNDLED_PACKAGES_DIR: directory containing pre-built .pkg archives (default /app/bundled-packages).
|
// BUNDLED_PACKAGES_DIR: directory containing pre-built .pkg archives (default /app/bundled-packages).
|
||||||
// BUNDLED_PACKAGES: controls which bundled packages are auto-installed:
|
// BUNDLED_PACKAGES: controls which bundled packages are auto-installed:
|
||||||
@@ -75,7 +75,7 @@ type Config struct {
|
|||||||
BundledPackagesDir string
|
BundledPackagesDir string
|
||||||
BundledPackages string
|
BundledPackages string
|
||||||
|
|
||||||
// OIDC (v0.24.1)
|
// OIDC
|
||||||
OIDCIssuerURL string // e.g. "https://keycloak.corp/realms/switchboard"
|
OIDCIssuerURL string // e.g. "https://keycloak.corp/realms/switchboard"
|
||||||
OIDCExternalIssuerURL string // OIDC_EXTERNAL_ISSUER_URL
|
OIDCExternalIssuerURL string // OIDC_EXTERNAL_ISSUER_URL
|
||||||
OIDCClientID string
|
OIDCClientID string
|
||||||
@@ -87,7 +87,7 @@ type Config struct {
|
|||||||
OIDCGroupsClaim string // JWT claim for group sync (default "groups")
|
OIDCGroupsClaim string // JWT claim for group sync (default "groups")
|
||||||
OIDCAdminRole string // IdP role value that maps to admin (default "admin")
|
OIDCAdminRole string // IdP role value that maps to admin (default "admin")
|
||||||
|
|
||||||
// Cluster registry (v0.6.0)
|
// Cluster registry
|
||||||
// PG-backed node registry for horizontal scaling. No-op on SQLite.
|
// PG-backed node registry for horizontal scaling. No-op on SQLite.
|
||||||
// CLUSTER_NODE_ID: override for deterministic identity (default: hostname-PID).
|
// CLUSTER_NODE_ID: override for deterministic identity (default: hostname-PID).
|
||||||
// CLUSTER_HEARTBEAT_INTERVAL: tick frequency (default "10s").
|
// CLUSTER_HEARTBEAT_INTERVAL: tick frequency (default "10s").
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
// provider_configs.api_key_plain using the env-derived key, writing the
|
// provider_configs.api_key_plain using the env-derived key, writing the
|
||||||
// result to api_key_enc (BYTEA) + key_nonce. Clears api_key_plain after.
|
// result to api_key_enc (BYTEA) + key_nonce. Clears api_key_plain after.
|
||||||
//
|
//
|
||||||
// This runs once on upgrade from v0.9.3 → v0.9.4. Subsequent starts are
|
// This runs once on upgrade when ENCRYPTION_KEY is first set. Subsequent starts are
|
||||||
// a no-op (api_key_plain will be NULL for all rows).
|
// a no-op (api_key_plain will be NULL for all rows).
|
||||||
//
|
//
|
||||||
// Personal-scope keys are temporarily encrypted with the env key. They get
|
// Personal-scope keys are temporarily encrypted with the env key. They get
|
||||||
@@ -94,7 +94,7 @@ func EnforceEncryptionKey(db *sql.DB, encryptionKey string) error {
|
|||||||
|
|
||||||
// Check if any plaintext keys exist (pre-backfill)
|
// Check if any plaintext keys exist (pre-backfill)
|
||||||
var hasPlaintext bool
|
var hasPlaintext bool
|
||||||
// api_key_plain column may not exist on fresh v0.9.4+ installs
|
// api_key_plain column may not exist on fresh installs
|
||||||
var colExists bool
|
var colExists bool
|
||||||
db.QueryRow(`
|
db.QueryRow(`
|
||||||
SELECT EXISTS (
|
SELECT EXISTS (
|
||||||
@@ -121,7 +121,7 @@ func EnforceEncryptionKey(db *sql.DB, encryptionKey string) error {
|
|||||||
if hasPlaintext {
|
if hasPlaintext {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"ENCRYPTION_KEY is not set but plaintext API keys need encryption. " +
|
"ENCRYPTION_KEY is not set but plaintext API keys need encryption. " +
|
||||||
"Set ENCRYPTION_KEY before starting (required for v0.9.4+)",
|
"Set ENCRYPTION_KEY before starting (required+)",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
log.Println(" ⚠ ENCRYPTION_KEY not set — API key encryption disabled (OK for fresh install with no providers)")
|
log.Println(" ⚠ ENCRYPTION_KEY not set — API key encryption disabled (OK for fresh install with no providers)")
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func SchemaVersion() string { return schemaVersion }
|
|||||||
// Migrate runs all pending migrations for the current dialect.
|
// Migrate runs all pending migrations for the current dialect.
|
||||||
// Migration files live in migrations/<dialect>/ subdirectories.
|
// Migration files live in migrations/<dialect>/ subdirectories.
|
||||||
// Falls back to the root migrations/ directory if no subdirectory exists
|
// Falls back to the root migrations/ directory if no subdirectory exists
|
||||||
// (backward compatible with pre-v0.17.1 postgres-only layouts).
|
// (backward compatible with postgres-only layouts).
|
||||||
func Migrate() error {
|
func Migrate() error {
|
||||||
if DB == nil {
|
if DB == nil {
|
||||||
return fmt.Errorf("database not connected")
|
return fmt.Errorf("database not connected")
|
||||||
|
|||||||
@@ -113,14 +113,14 @@ func TestRouteFor(t *testing.T) {
|
|||||||
{"pong", DirToClient},
|
{"pong", DirToClient},
|
||||||
// Tool bridge routes
|
// Tool bridge routes
|
||||||
{"tool.call.abc123", DirToClient},
|
{"tool.call.abc123", DirToClient},
|
||||||
{"tool.result.abc123", DirBoth}, // v0.32.0: DirBoth for cross-pod WaitFor
|
{"tool.result.abc123", DirBoth},
|
||||||
// Extension lifecycle
|
// Extension lifecycle
|
||||||
{"extension.loaded", DirLocal},
|
{"extension.loaded", DirLocal},
|
||||||
{"extension.error", DirLocal},
|
{"extension.error", DirLocal},
|
||||||
// Realtime (v0.5.0)
|
// Realtime
|
||||||
{"realtime.chat.message", DirToClient},
|
{"realtime.chat.message", DirToClient},
|
||||||
{"realtime.custom.event", DirToClient},
|
{"realtime.custom.event", DirToClient},
|
||||||
// Room management (v0.5.0)
|
// Room management
|
||||||
{"room.subscribe", DirFromClient},
|
{"room.subscribe", DirFromClient},
|
||||||
{"room.unsubscribe", DirFromClient},
|
{"room.unsubscribe", DirFromClient},
|
||||||
}
|
}
|
||||||
@@ -227,7 +227,6 @@ func TestToolCallRouteToClient(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestToolResultRouteBoth(t *testing.T) {
|
func TestToolResultRouteBoth(t *testing.T) {
|
||||||
// v0.32.0: tool.result is DirBoth so results cross pods for WaitFor.
|
|
||||||
// The WS subscriber explicitly filters out tool.result events to
|
// The WS subscriber explicitly filters out tool.result events to
|
||||||
// prevent re-sending to clients (see subscribeToBus in ws.go).
|
// prevent re-sending to clients (see subscribeToBus in ws.go).
|
||||||
if !ShouldAcceptFromClient("tool.result.abc123") {
|
if !ShouldAcceptFromClient("tool.result.abc123") {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package events
|
package events
|
||||||
|
|
||||||
// v0.32.0: TicketValidatorAdapter bridges the context-aware store.TicketStore
|
|
||||||
// to the middleware.TicketValidator interface (which has no context parameter).
|
// to the middleware.TicketValidator interface (which has no context parameter).
|
||||||
// Replaces the in-memory TicketStore that lived in this file previously.
|
// Replaces the in-memory TicketStore that lived in this file previously.
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ type Event struct {
|
|||||||
Payload json.RawMessage `json:"payload"`
|
Payload json.RawMessage `json:"payload"`
|
||||||
Ts int64 `json:"ts"`
|
Ts int64 `json:"ts"`
|
||||||
|
|
||||||
// v0.32.0: Cross-pod targeted delivery. When set, only connections
|
|
||||||
// belonging to this user receive the event. Serialized for pg_broadcast
|
// belonging to this user receive the event. Serialized for pg_broadcast
|
||||||
// (harmless if clients see it — they ignore unknown fields).
|
// (harmless if clients see it — they ignore unknown fields).
|
||||||
TargetUserID string `json:"target_user_id,omitempty"`
|
TargetUserID string `json:"target_user_id,omitempty"`
|
||||||
@@ -38,8 +37,8 @@ var routeTable = map[string]Direction{
|
|||||||
// User/presence
|
// User/presence
|
||||||
"user.presence": DirToClient,
|
"user.presence": DirToClient,
|
||||||
"user.status": DirToClient,
|
"user.status": DirToClient,
|
||||||
"user.mentioned": DirToClient, // v0.23.2: targeted @mention notification
|
"user.mentioned": DirToClient,
|
||||||
"typing.user": DirToClient, // v0.23.2: human typing in DM/channel
|
"typing.user": DirToClient,
|
||||||
|
|
||||||
// System
|
// System
|
||||||
"system.notify": DirToClient,
|
"system.notify": DirToClient,
|
||||||
@@ -48,16 +47,16 @@ var routeTable = map[string]Direction{
|
|||||||
// Model/provider status
|
// Model/provider status
|
||||||
"model.status": DirToClient,
|
"model.status": DirToClient,
|
||||||
|
|
||||||
// Role alerts (v0.17.0)
|
// Role alerts
|
||||||
"role.fallback": DirToClient,
|
"role.fallback": DirToClient,
|
||||||
|
|
||||||
// Notifications (v0.20.0)
|
// Notifications
|
||||||
"notification.new": DirToClient, // targeted via Hub.PublishToUser, not room-based
|
"notification.new": DirToClient, // targeted via Hub.PublishToUser, not room-based
|
||||||
"notification.read": DirToClient, // badge sync across tabs
|
"notification.read": DirToClient, // badge sync across tabs
|
||||||
|
|
||||||
// Workspace (v0.21.5)
|
// Workspace
|
||||||
|
|
||||||
// Workflow (v0.27.0, v0.3.2, v0.3.3)
|
// Workflow
|
||||||
"workflow.started": DirToClient, // instance started
|
"workflow.started": DirToClient, // instance started
|
||||||
"workflow.assigned": DirToClient, // new assignment → team members
|
"workflow.assigned": DirToClient, // new assignment → team members
|
||||||
"workflow.claimed": DirToClient, // assignment claimed → team + claimer
|
"workflow.claimed": DirToClient, // assignment claimed → team + claimer
|
||||||
@@ -65,10 +64,10 @@ var routeTable = map[string]Direction{
|
|||||||
"workflow.completed": DirToClient, // workflow finished → instance participants
|
"workflow.completed": DirToClient, // workflow finished → instance participants
|
||||||
"workflow.cancelled": DirToClient, // instance cancelled
|
"workflow.cancelled": DirToClient, // instance cancelled
|
||||||
"workflow.error": DirToClient, // engine/hook error
|
"workflow.error": DirToClient, // engine/hook error
|
||||||
"workflow.sla_breach": DirToClient, // SLA exceeded → team + admins (v0.3.3)
|
"workflow.sla_breach": DirToClient, // SLA exceeded → team + admins
|
||||||
"workflow.stale": DirToClient, // instance marked stale (v0.3.3)
|
"workflow.stale": DirToClient, // instance marked stale
|
||||||
"workflow.signoff": DirToClient, // signoff submitted (v0.3.4)
|
"workflow.signoff": DirToClient, // signoff submitted
|
||||||
"workflow.rejected": DirToClient, // rejection triggered cancel/reroute (v0.3.4)
|
"workflow.rejected": DirToClient, // rejection triggered cancel/reroute
|
||||||
|
|
||||||
// Plugin hooks — never cross the wire
|
// Plugin hooks — never cross the wire
|
||||||
"plugin.hook.": DirLocal,
|
"plugin.hook.": DirLocal,
|
||||||
@@ -77,20 +76,20 @@ var routeTable = map[string]Direction{
|
|||||||
|
|
||||||
// Tool execution (browser tools bridge)
|
// Tool execution (browser tools bridge)
|
||||||
"tool.call.": DirToClient, // Server → specific client (browser tool invocation)
|
"tool.call.": DirToClient, // Server → specific client (browser tool invocation)
|
||||||
"tool.result.": DirBoth, // v0.32.0: DirBoth — result must cross pods for WaitFor
|
"tool.result.": DirBoth,
|
||||||
|
|
||||||
// Extension lifecycle
|
// Extension lifecycle
|
||||||
"extension.loaded": DirLocal, // Client-only
|
"extension.loaded": DirLocal, // Client-only
|
||||||
"extension.error": DirLocal,
|
"extension.error": DirLocal,
|
||||||
|
|
||||||
// Trigger system (v0.2.2)
|
// Trigger system
|
||||||
"trigger.fired": DirLocal, // Trigger invocation event (observability)
|
"trigger.fired": DirLocal, // Trigger invocation event (observability)
|
||||||
"trigger.error": DirLocal, // Trigger execution error
|
"trigger.error": DirLocal, // Trigger execution error
|
||||||
|
|
||||||
// Realtime (v0.5.0): extension-published events, room-scoped
|
// Realtime: extension-published events, room-scoped
|
||||||
"realtime.": DirToClient,
|
"realtime.": DirToClient,
|
||||||
|
|
||||||
// Room management (v0.5.0): client room join/leave
|
// Room management: client room join/leave
|
||||||
"room.subscribe": DirFromClient,
|
"room.subscribe": DirFromClient,
|
||||||
"room.unsubscribe": DirFromClient,
|
"room.unsubscribe": DirFromClient,
|
||||||
|
|
||||||
|
|||||||
@@ -174,7 +174,6 @@ func (h *Hub) IsConnected(userID string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PublishToUser sends an event to a specific user via the bus.
|
// PublishToUser sends an event to a specific user via the bus.
|
||||||
// v0.32.0: Cross-pod safe — the bus broadcast hook fans out via pg_notify.
|
|
||||||
// All replicas receive the event; only the one with the user's connection delivers it.
|
// All replicas receive the event; only the one with the user's connection delivers it.
|
||||||
func (h *Hub) PublishToUser(userID string, event Event) {
|
func (h *Hub) PublishToUser(userID string, event Event) {
|
||||||
event.TargetUserID = userID
|
event.TargetUserID = userID
|
||||||
@@ -238,13 +237,11 @@ func (c *Conn) subscribeToBus() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.32.0: Targeted delivery — only deliver to the intended user.
|
|
||||||
// Targeted events skip room filtering (user-scoped, not room-scoped).
|
// Targeted events skip room filtering (user-scoped, not room-scoped).
|
||||||
if e.TargetUserID != "" && e.TargetUserID != c.userID {
|
if e.TargetUserID != "" && e.TargetUserID != c.userID {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.32.0: tool.result travels cross-pod for WaitFor (DirBoth) but
|
|
||||||
// must not be forwarded to WebSocket clients — they sent it.
|
// must not be forwarded to WebSocket clients — they sent it.
|
||||||
if strings.HasPrefix(e.Label, "tool.result.") {
|
if strings.HasPrefix(e.Label, "tool.result.") {
|
||||||
return
|
return
|
||||||
@@ -316,7 +313,6 @@ func (c *Conn) readPump() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.5.0: Room management — intercept before bus publish (like ping)
|
|
||||||
if event.Label == "room.subscribe" {
|
if event.Label == "room.subscribe" {
|
||||||
var req struct {
|
var req struct {
|
||||||
Room string `json:"room"`
|
Room string `json:"room"`
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
// v0.38.1: Extension connection handlers — admin/global scope.
|
|
||||||
// Methods on AdminHandler, mirrors admin provider config pattern.
|
// Methods on AdminHandler, mirrors admin provider config pattern.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -345,7 +345,6 @@ func hashToken(token string) string {
|
|||||||
// - AdminHandler.destroyVault (admin-initiated reset)
|
// - AdminHandler.destroyVault (admin-initiated reset)
|
||||||
//
|
//
|
||||||
// Does NOT evict from UEK cache or write audit logs — callers handle that.
|
// Does NOT evict from UEK cache or write audit logs — callers handle that.
|
||||||
// v0.29.0: accepts stores instead of using database.DB directly.
|
|
||||||
func DestroyVaultDB(ctx context.Context, stores store.Stores, userID string) (deleted int64) {
|
func DestroyVaultDB(ctx context.Context, stores store.Stores, userID string) (deleted int64) {
|
||||||
if err := stores.Users.ClearVaultKeys(ctx, userID); err != nil {
|
if err := stores.Users.ClearVaultKeys(ctx, userID); err != nil {
|
||||||
log.Printf("⚠ DestroyVaultDB: failed to clear vault columns for user %s: %v", userID, err)
|
log.Printf("⚠ DestroyVaultDB: failed to clear vault columns for user %s: %v", userID, err)
|
||||||
@@ -366,8 +365,6 @@ func DestroyVaultDB(ctx context.Context, stores store.Stores, userID string) (de
|
|||||||
// sessions survive server restarts.
|
// sessions survive server restarts.
|
||||||
//
|
//
|
||||||
// Used by BootstrapAdmin and SeedUsers where the password is known at startup.
|
// Used by BootstrapAdmin and SeedUsers where the password is known at startup.
|
||||||
// v0.29.0: accepts stores instead of using database.DB directly.
|
|
||||||
// v0.30.2: accepts optional uekCache to pre-warm vault on restart.
|
|
||||||
func ProbeAndRepairVault(ctx context.Context, stores store.Stores, userID, password string, uekCache ...*crypto.UEKCache) {
|
func ProbeAndRepairVault(ctx context.Context, stores store.Stores, userID, password string, uekCache ...*crypto.UEKCache) {
|
||||||
vaultSet, encryptedUEK, salt, nonce, err := stores.Users.GetVaultKeys(ctx, userID)
|
vaultSet, encryptedUEK, salt, nonce, err := stores.Users.GetVaultKeys(ctx, userID)
|
||||||
if err != nil || !vaultSet {
|
if err != nil || !vaultSet {
|
||||||
@@ -493,7 +490,7 @@ func BootstrapAdmin(cfg *config.Config, s store.Stores, uekCache ...*crypto.UEKC
|
|||||||
})
|
})
|
||||||
// If the actual password changed, the vault seal is stale. Probe it
|
// If the actual password changed, the vault seal is stale. Probe it
|
||||||
// with the current password and destroy only if unwrap fails.
|
// with the current password and destroy only if unwrap fails.
|
||||||
// Ensure handle exists (backfill for pre-v0.24.0 users)
|
// Ensure handle exists (backfill for users)
|
||||||
if existing.Handle == "" {
|
if existing.Handle == "" {
|
||||||
handle := auth.UniqueHandle(ctx, s.Users, models.HandleFromName(cfg.AdminUsername))
|
handle := auth.UniqueHandle(ctx, s.Users, models.HandleFromName(cfg.AdminUsername))
|
||||||
s.Users.Update(ctx, existing.ID, map[string]interface{}{"handle": handle})
|
s.Users.Update(ctx, existing.ID, map[string]interface{}{"handle": handle})
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
// v0.38.1: ConnectionResolver bridges the sandbox connections module
|
|
||||||
// to the store + vault layers. Implements sandbox.ConnectionResolver.
|
// to the store + vault layers. Implements sandbox.ConnectionResolver.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
// v0.38.4: Connection type discovery endpoint.
|
|
||||||
// Returns merged connection types from all active packages.
|
// Returns merged connection types from all active packages.
|
||||||
// Library declarations take precedence over non-library declarations.
|
// Library declarations take precedence over non-library declarations.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
// v0.38.1: Extension connection handlers — personal scope + resolution.
|
|
||||||
// Pattern follows apiconfigs.go (ProviderConfigHandler).
|
// Pattern follows apiconfigs.go (ProviderConfigHandler).
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -28,7 +27,6 @@ func NewConnectionHandler(s store.Stores, vault *crypto.KeyResolver) *Connection
|
|||||||
// ListConnections returns the user's personal connections.
|
// ListConnections returns the user's personal connections.
|
||||||
func (h *ConnectionHandler) ListConnections(c *gin.Context) {
|
func (h *ConnectionHandler) ListConnections(c *gin.Context) {
|
||||||
userID := getUserID(c)
|
userID := getUserID(c)
|
||||||
// v0.38.5: Show all accessible connections (personal + team + global)
|
|
||||||
// so users can see which connections are available to them.
|
// so users can see which connections are available to them.
|
||||||
conns, err := h.stores.Connections.ListAccessible(c.Request.Context(), userID)
|
conns, err := h.stores.Connections.ListAccessible(c.Request.Context(), userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"switchboard-core/models"
|
"switchboard-core/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Extension Dependencies (v0.38.2) ──────────────────
|
// ── Extension Dependencies ──────────────────
|
||||||
// Admin-only read endpoints for dependency graph visibility.
|
// Admin-only read endpoints for dependency graph visibility.
|
||||||
// Dependencies are created/deleted implicitly by InstallPackage/DeletePackage.
|
// Dependencies are created/deleted implicitly by InstallPackage/DeletePackage.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Package handlers — ext_api.go
|
// Package handlers — ext_api.go
|
||||||
//
|
//
|
||||||
// v0.29.1 CS1: Extension API routes. Starlark packages serve custom
|
|
||||||
// JSON endpoints via the existing Gin router. Mounted at
|
// JSON endpoints via the existing Gin router. Mounted at
|
||||||
// /s/:slug/api/*path with JWT auth.
|
// /s/:slug/api/*path with JWT auth.
|
||||||
//
|
//
|
||||||
@@ -332,7 +331,7 @@ func hasPermission(granted []string, perm string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── api_schema support (v0.6.2) ─────────────────────────────────────
|
// ── api_schema support ─────────────────────────────────────
|
||||||
|
|
||||||
// APISchemaEntry represents one route's OpenAPI-level documentation
|
// APISchemaEntry represents one route's OpenAPI-level documentation
|
||||||
// declared via the optional manifest "api_schema" array.
|
// declared via the optional manifest "api_schema" array.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
// ext_db_schema.go — v0.29.2
|
// ext_db_schema.go
|
||||||
//
|
//
|
||||||
// DDL generation and lifecycle management for extension-owned database tables.
|
// DDL generation and lifecycle management for extension-owned database tables.
|
||||||
// Tables are namespaced as ext_{pkg_slug}_{logical_name} and tracked in the
|
// Tables are namespaced as ext_{pkg_slug}_{logical_name} and tracked in the
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ExtPermHandler serves extension permission management endpoints.
|
// ExtPermHandler serves extension permission management endpoints.
|
||||||
// v0.29.0 CS2: Admin reviews and grants/revokes declared permissions.
|
|
||||||
type ExtPermHandler struct {
|
type ExtPermHandler struct {
|
||||||
stores store.Stores
|
stores store.Stores
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ExtSecretsHandler serves extension secret management endpoints.
|
// ExtSecretsHandler serves extension secret management endpoints.
|
||||||
// v0.29.0 CS3: Admin sets key-value secrets that Starlark extensions
|
|
||||||
// can read via the secrets.get() module.
|
// can read via the secrets.get() module.
|
||||||
//
|
//
|
||||||
// Secrets are stored in GlobalConfig under key "ext_secrets:{packageID}"
|
// Secrets are stored in GlobalConfig under key "ext_secrets:{packageID}"
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ExtensionHandler serves extension management endpoints.
|
// ExtensionHandler serves extension management endpoints.
|
||||||
// v0.28.7: Backed by PackageStore (packages table) instead of ExtensionStore.
|
|
||||||
type ExtensionHandler struct {
|
type ExtensionHandler struct {
|
||||||
stores store.Stores
|
stores store.Stores
|
||||||
}
|
}
|
||||||
@@ -94,7 +93,6 @@ func (h *ExtensionHandler) UpdateUserExtensionSettings(c *gin.Context) {
|
|||||||
|
|
||||||
settings := json.RawMessage("{}")
|
settings := json.RawMessage("{}")
|
||||||
if body.Settings != nil {
|
if body.Settings != nil {
|
||||||
// v0.2.0: enforce user_overridable — strip locked keys before saving
|
|
||||||
schema := store.ParseSettingsSchema(pkg.Manifest)
|
schema := store.ParseSettingsSchema(pkg.Manifest)
|
||||||
if len(schema) > 0 {
|
if len(schema) > 0 {
|
||||||
var incoming map[string]any
|
var incoming map[string]any
|
||||||
@@ -229,18 +227,15 @@ func (h *ExtensionHandler) AdminInstallExtension(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.29.0: Parse manifest permissions and declare them.
|
|
||||||
// If permissions are declared, package moves to pending_review.
|
// If permissions are declared, package moves to pending_review.
|
||||||
SyncManifestPermissions(c, h.stores, pkg.ID, manifestMap)
|
SyncManifestPermissions(c, h.stores, pkg.ID, manifestMap)
|
||||||
|
|
||||||
// v0.29.2: Create namespaced DB tables declared in the manifest.
|
|
||||||
if tables, ok := ParseDBTables(manifestMap); ok {
|
if tables, ok := ParseDBTables(manifestMap); ok {
|
||||||
if err := CreateExtTables(c.Request.Context(), database.DB, database.IsPostgres(), h.stores, pkg.ID, tables); err != nil {
|
if err := CreateExtTables(c.Request.Context(), database.DB, database.IsPostgres(), h.stores, pkg.ID, tables); err != nil {
|
||||||
log.Printf("[ext-db] schema create failed for %s: %v", pkg.ID, err)
|
log.Printf("[ext-db] schema create failed for %s: %v", pkg.ID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.2.2: Sync triggers from manifest
|
|
||||||
SyncManifestTriggers(c.Request.Context(), h.stores, triggers.GlobalEngine(), pkg.ID, manifestMap)
|
SyncManifestTriggers(c.Request.Context(), h.stores, triggers.GlobalEngine(), pkg.ID, manifestMap)
|
||||||
|
|
||||||
c.JSON(201, gin.H{"data": pkg})
|
c.JSON(201, gin.H{"data": pkg})
|
||||||
@@ -403,7 +398,6 @@ func (h *ExtensionHandler) AdminUninstallExtension(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.29.2: Drop namespaced DB tables before removing the package record.
|
|
||||||
if err := DropExtTables(c.Request.Context(), database.DB, h.stores, id); err != nil {
|
if err := DropExtTables(c.Request.Context(), database.DB, h.stores, id); err != nil {
|
||||||
log.Printf("[ext-db] schema drop failed for %s: %v", id, err)
|
log.Printf("[ext-db] schema drop failed for %s: %v", id, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ func (h *GroupHandler) AddMember(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the added user (v0.20.0)
|
// Notify the added user
|
||||||
if svc := notifications.Default(); svc != nil {
|
if svc := notifications.Default(); svc != nil {
|
||||||
notifications.NotifyGroupMemberAdded(svc, req.UserID, groupID, group.Name)
|
notifications.NotifyGroupMemberAdded(svc, req.UserID, groupID, group.Name)
|
||||||
}
|
}
|
||||||
@@ -273,7 +273,7 @@ func (h *GroupHandler) RemoveMember(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the removed user (v0.20.0)
|
// Notify the removed user
|
||||||
if svc := notifications.Default(); svc != nil && group != nil {
|
if svc := notifications.Default(); svc != nil && group != nil {
|
||||||
notifications.NotifyGroupMemberRemoved(svc, userID, groupID, group.Name)
|
notifications.NotifyGroupMemberRemoved(svc, userID, groupID, group.Name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func setupNotifHarness(t *testing.T) *notifHarness {
|
|||||||
protected.PUT("/notifications/preferences/:type", notifH.SetPreference)
|
protected.PUT("/notifications/preferences/:type", notifH.SetPreference)
|
||||||
protected.DELETE("/notifications/preferences/:type", notifH.DeletePreference)
|
protected.DELETE("/notifications/preferences/:type", notifH.DeletePreference)
|
||||||
|
|
||||||
// Admin broadcast route (v0.28.6)
|
// Admin broadcast route
|
||||||
admin := api.Group("/admin")
|
admin := api.Group("/admin")
|
||||||
admin.Use(middleware.Auth(cfg, stores.Users, userCache))
|
admin.Use(middleware.Auth(cfg, stores.Users, userCache))
|
||||||
admin.Use(middleware.RequireAdmin(stores))
|
admin.Use(middleware.RequireAdmin(stores))
|
||||||
@@ -560,7 +560,7 @@ func TestNotifications_Unauthenticated(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Admin Broadcast (v0.28.6) ────────────
|
// ── Admin Broadcast ────────────
|
||||||
|
|
||||||
func TestBroadcast_Success(t *testing.T) {
|
func TestBroadcast_Success(t *testing.T) {
|
||||||
h := setupNotifHarness(t)
|
h := setupNotifHarness(t)
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ func (h *NotificationHandler) Delete(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Notification Preferences (v0.20.0 Phase 3) ──
|
// ── Notification Preferences ──
|
||||||
|
|
||||||
// GET /api/v1/notifications/preferences
|
// GET /api/v1/notifications/preferences
|
||||||
func (h *NotificationHandler) ListPreferences(c *gin.Context) {
|
func (h *NotificationHandler) ListPreferences(c *gin.Context) {
|
||||||
@@ -282,7 +282,7 @@ func (h *NotificationHandler) DeletePreference(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Admin Broadcast (v0.28.6) ───────────────
|
// ── Admin Broadcast ───────────────
|
||||||
|
|
||||||
// POST /api/v1/admin/notifications/broadcast
|
// POST /api/v1/admin/notifications/broadcast
|
||||||
// Admin-only: sends a system.announcement notification to all active users.
|
// Admin-only: sends a system.announcement notification to all active users.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Package handlers — openapi.go
|
// Package handlers — openapi.go
|
||||||
//
|
//
|
||||||
// v0.6.2: Dynamic OpenAPI spec generation. Merges the static kernel spec
|
|
||||||
// with extension-declared API routes. Extensions with api_schema get rich
|
// with extension-declared API routes. Extensions with api_schema get rich
|
||||||
// path items; others get auto-generated stubs.
|
// path items; others get auto-generated stubs.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
// package_registry.go — v0.30.0 CS4
|
// package_registry.go
|
||||||
//
|
//
|
||||||
// Package registry (marketplace discovery). Admin browses available
|
// Package registry (marketplace discovery). Admin browses available
|
||||||
// packages from an external JSON registry and installs them by URL.
|
// packages from an external JSON registry and installs them by URL.
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ import (
|
|||||||
var validPackageID = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$`)
|
var validPackageID = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$`)
|
||||||
|
|
||||||
// PackageHandler manages unified package lifecycle (admin-only).
|
// PackageHandler manages unified package lifecycle (admin-only).
|
||||||
// Replaces SurfaceHandler (v0.28.7).
|
// Replaces SurfaceHandler.
|
||||||
type PackageHandler struct {
|
type PackageHandler struct {
|
||||||
stores store.Stores
|
stores store.Stores
|
||||||
packagesDir string // e.g. /data/packages — where archives are extracted
|
packagesDir string // e.g. /data/packages — where archives are extracted
|
||||||
sandbox *sandbox.Sandbox // v0.30.0: for schema migration scripts
|
sandbox *sandbox.Sandbox
|
||||||
runner *sandbox.Runner // v0.38.2: for test-tool execution
|
runner *sandbox.Runner
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPackageHandler(s store.Stores, packagesDir ...string) *PackageHandler {
|
func NewPackageHandler(s store.Stores, packagesDir ...string) *PackageHandler {
|
||||||
@@ -43,12 +43,12 @@ func NewPackageHandler(s store.Stores, packagesDir ...string) *PackageHandler {
|
|||||||
return &PackageHandler{stores: s, packagesDir: dir}
|
return &PackageHandler{stores: s, packagesDir: dir}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSandbox attaches a Starlark sandbox for schema migrations (v0.30.0).
|
// SetSandbox attaches a Starlark sandbox for schema migrations.
|
||||||
func (h *PackageHandler) SetSandbox(sb *sandbox.Sandbox) {
|
func (h *PackageHandler) SetSandbox(sb *sandbox.Sandbox) {
|
||||||
h.sandbox = sb
|
h.sandbox = sb
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRunner attaches the Starlark runner for test-tool execution (v0.38.2).
|
// SetRunner attaches the Starlark runner for test-tool execution.
|
||||||
func (h *PackageHandler) SetRunner(r *sandbox.Runner) {
|
func (h *PackageHandler) SetRunner(r *sandbox.Runner) {
|
||||||
h.runner = r
|
h.runner = r
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,6 @@ func (h *PackageHandler) GetPackage(c *gin.Context) {
|
|||||||
func (h *PackageHandler) EnablePackage(c *gin.Context) {
|
func (h *PackageHandler) EnablePackage(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
|
|
||||||
// v0.3.7: Block enabling dormant packages (unmet requires).
|
|
||||||
pkg, err := h.stores.Packages.Get(c.Request.Context(), id)
|
pkg, err := h.stores.Packages.Get(c.Request.Context(), id)
|
||||||
if err != nil || pkg == nil {
|
if err != nil || pkg == nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "package not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "package not found"})
|
||||||
@@ -149,7 +148,6 @@ func (h *PackageHandler) DeletePackage(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.38.2: Libraries with active consumers cannot be uninstalled.
|
|
||||||
if pkg.Type == "library" && h.stores.Dependencies != nil {
|
if pkg.Type == "library" && h.stores.Dependencies != nil {
|
||||||
hasConsumers, _ := h.stores.Dependencies.HasConsumers(c.Request.Context(), id)
|
hasConsumers, _ := h.stores.Dependencies.HasConsumers(c.Request.Context(), id)
|
||||||
if hasConsumers {
|
if hasConsumers {
|
||||||
@@ -158,12 +156,10 @@ func (h *PackageHandler) DeletePackage(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.38.2: Clean up dependency records when uninstalling a consumer.
|
|
||||||
if h.stores.Dependencies != nil {
|
if h.stores.Dependencies != nil {
|
||||||
h.stores.Dependencies.DeleteAllForConsumer(c.Request.Context(), id)
|
h.stores.Dependencies.DeleteAllForConsumer(c.Request.Context(), id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.29.2: Drop namespaced DB tables before removing the package record.
|
|
||||||
if err := DropExtTables(c.Request.Context(), database.DB, h.stores, id); err != nil {
|
if err := DropExtTables(c.Request.Context(), database.DB, h.stores, id); err != nil {
|
||||||
log.Printf("[ext-db] schema drop failed for %s: %v", id, err)
|
log.Printf("[ext-db] schema drop failed for %s: %v", id, err)
|
||||||
}
|
}
|
||||||
@@ -195,7 +191,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
|||||||
var tmpPath string
|
var tmpPath string
|
||||||
var cleanupTmp bool
|
var cleanupTmp bool
|
||||||
|
|
||||||
// v0.30.0: Check for pre-downloaded file from registry install
|
|
||||||
if regFile, ok := c.Get("_registry_file"); ok {
|
if regFile, ok := c.Get("_registry_file"); ok {
|
||||||
tmpPath = regFile.(string)
|
tmpPath = regFile.(string)
|
||||||
// Don't remove — caller manages lifecycle
|
// Don't remove — caller manages lifecycle
|
||||||
@@ -278,7 +273,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.38.0: Entry point validation for starlark-tier packages.
|
|
||||||
// Scripts are loaded from disk at runtime — no _starlark_script injection.
|
// Scripts are loaded from disk at runtime — no _starlark_script injection.
|
||||||
if manifestTier, _ := manifest["tier"].(string); manifestTier == "starlark" {
|
if manifestTier, _ := manifest["tier"].(string); manifestTier == "starlark" {
|
||||||
entryPoint := "script.star"
|
entryPoint := "script.star"
|
||||||
@@ -380,7 +374,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
|||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "full packages require a route"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "full packages require a route"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// v0.31.0: full packages need either server-side behavior (tools/pipes/hooks)
|
|
||||||
// or a settings schema. A surface-with-settings is a valid "full" package
|
// or a settings schema. A surface-with-settings is a valid "full" package
|
||||||
// (e.g. editor: browser-tier surface + admin-configurable settings).
|
// (e.g. editor: browser-tier surface + admin-configurable settings).
|
||||||
if !hasExtBehavior && !hasSettings {
|
if !hasExtBehavior && !hasSettings {
|
||||||
@@ -393,7 +386,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "library":
|
case "library":
|
||||||
// v0.38.2: Libraries must declare exports, cannot have tools/pipes/route.
|
|
||||||
exports, _ := manifest["exports"].([]any)
|
exports, _ := manifest["exports"].([]any)
|
||||||
if len(exports) == 0 {
|
if len(exports) == 0 {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "library packages require an 'exports' array"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "library packages require an 'exports' array"})
|
||||||
@@ -471,7 +463,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
|||||||
|
|
||||||
userID := c.GetString("user_id")
|
userID := c.GetString("user_id")
|
||||||
|
|
||||||
// v0.30.0: use registry source if set by registry install handler
|
|
||||||
pkgSource := "extension"
|
pkgSource := "extension"
|
||||||
if src, ok := c.Get("_registry_source"); ok {
|
if src, ok := c.Get("_registry_source"); ok {
|
||||||
pkgSource = src.(string)
|
pkgSource = src.(string)
|
||||||
@@ -509,22 +500,18 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
|||||||
log.Printf("[packages] Failed to update package metadata for %s: %v", pkgID, err)
|
log.Printf("[packages] Failed to update package metadata for %s: %v", pkgID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.29.2: Create namespaced DB tables declared in the manifest.
|
|
||||||
if tables, ok := ParseDBTables(manifest); ok {
|
if tables, ok := ParseDBTables(manifest); ok {
|
||||||
if err := CreateExtTables(c.Request.Context(), database.DB, database.IsPostgres(), h.stores, pkgID, tables); err != nil {
|
if err := CreateExtTables(c.Request.Context(), database.DB, database.IsPostgres(), h.stores, pkgID, tables); err != nil {
|
||||||
log.Printf("[ext-db] schema create failed for %s: %v", pkgID, err)
|
log.Printf("[ext-db] schema create failed for %s: %v", pkgID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.38.5: Declare manifest permissions (same as AdminInstallExtension).
|
|
||||||
// This creates the permission rows and sets status to pending_review
|
// This creates the permission rows and sets status to pending_review
|
||||||
// if the package declares permissions.
|
// if the package declares permissions.
|
||||||
SyncManifestPermissions(c, h.stores, pkgID, manifest)
|
SyncManifestPermissions(c, h.stores, pkgID, manifest)
|
||||||
|
|
||||||
// v0.2.2: Sync triggers from manifest
|
|
||||||
SyncManifestTriggers(c.Request.Context(), h.stores, triggers.GlobalEngine(), pkgID, manifest)
|
SyncManifestTriggers(c.Request.Context(), h.stores, triggers.GlobalEngine(), pkgID, manifest)
|
||||||
|
|
||||||
// v0.30.0: Run schema migrations if declared.
|
|
||||||
newSchemaVersion := ParseSchemaVersion(manifest)
|
newSchemaVersion := ParseSchemaVersion(manifest)
|
||||||
if newSchemaVersion > 0 && h.sandbox != nil {
|
if newSchemaVersion > 0 && h.sandbox != nil {
|
||||||
oldSchemaVersion := 0
|
oldSchemaVersion := 0
|
||||||
@@ -553,7 +540,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.30.2: Install workflow definition from package manifest.
|
|
||||||
if pkgType == "workflow" {
|
if pkgType == "workflow" {
|
||||||
if err := InstallWorkflowFromManifest(c, h.stores, pkgID, manifest); err != nil {
|
if err := InstallWorkflowFromManifest(c, h.stores, pkgID, manifest); err != nil {
|
||||||
log.Printf("[packages] workflow install failed for %s: %v", pkgID, err)
|
log.Printf("[packages] workflow install failed for %s: %v", pkgID, err)
|
||||||
@@ -562,7 +548,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.38.2: Create dependency records from manifest "dependencies" map.
|
|
||||||
// Libraries must be installed first; consumers declare them.
|
// Libraries must be installed first; consumers declare them.
|
||||||
if deps, ok := manifest["dependencies"].(map[string]any); ok && len(deps) > 0 {
|
if deps, ok := manifest["dependencies"].(map[string]any); ok && len(deps) > 0 {
|
||||||
// Clear stale dependency records from a previous install of the same consumer.
|
// Clear stale dependency records from a previous install of the same consumer.
|
||||||
@@ -603,7 +588,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
|||||||
log.Printf("[packages] %s: %d dependencies recorded", pkgID, len(deps))
|
log.Printf("[packages] %s: %d dependencies recorded", pkgID, len(deps))
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.2.1: Auto-set default_surface when the first extension surface is installed.
|
|
||||||
if (pkgType == "surface" || pkgType == "full") && pkgSource != "core" {
|
if (pkgType == "surface" || pkgType == "full") && pkgSource != "core" {
|
||||||
if raw, err := h.stores.GlobalConfig.Get(c.Request.Context(), "default_surface"); err != nil || raw == nil {
|
if raw, err := h.stores.GlobalConfig.Get(c.Request.Context(), "default_surface"); err != nil || raw == nil {
|
||||||
dflt := models.JSONMap{"id": pkgID}
|
dflt := models.JSONMap{"id": pkgID}
|
||||||
@@ -615,7 +599,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.3.7: Auto-set dormant for packages with unmet requires.
|
|
||||||
// Known capabilities: (none yet — chat and legacy-sdk are future/removed).
|
// Known capabilities: (none yet — chat and legacy-sdk are future/removed).
|
||||||
knownCaps := map[string]bool{}
|
knownCaps := map[string]bool{}
|
||||||
var unmetReqs []string
|
var unmetReqs []string
|
||||||
@@ -689,7 +672,7 @@ func extractableRelPath(name string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Package settings (v0.30.0 CS2) ──────────────
|
// ── Package settings ──────────────
|
||||||
|
|
||||||
// GetPackageSettings returns the settings schema from the manifest merged
|
// GetPackageSettings returns the settings schema from the manifest merged
|
||||||
// with current admin-configured values.
|
// with current admin-configured values.
|
||||||
@@ -823,7 +806,7 @@ func (h *PackageHandler) ListEnabledSurfaces(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"data": enabled})
|
c.JSON(http.StatusOK, gin.H{"data": enabled})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Test Tool (v0.38.2) ─────────────────────────
|
// ── Test Tool ─────────────────────────
|
||||||
// POST /admin/packages/:id/test-tool
|
// POST /admin/packages/:id/test-tool
|
||||||
// Invokes a starlark extension's on_tool_call entry point directly,
|
// Invokes a starlark extension's on_tool_call entry point directly,
|
||||||
// without going through the AI chat loop. Admin-only test harness.
|
// without going through the AI chat loop. Admin-only test harness.
|
||||||
@@ -898,7 +881,7 @@ func (h *PackageHandler) TestTool(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Package update (v0.5.4) ──────────────────────
|
// ── Package update ──────────────────────
|
||||||
|
|
||||||
// UpdatePackage applies an in-place update to an existing package.
|
// UpdatePackage applies an in-place update to an existing package.
|
||||||
// POST /api/v1/admin/packages/:id/update
|
// POST /api/v1/admin/packages/:id/update
|
||||||
@@ -1175,7 +1158,7 @@ func mergePackageSettings(existing *store.PackageRegistration, manifest map[stri
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Package export (v0.5.4) ──────────────────────
|
// ── Package export ──────────────────────
|
||||||
|
|
||||||
// ExportPackage exports an installed package as a downloadable .pkg archive.
|
// ExportPackage exports an installed package as a downloadable .pkg archive.
|
||||||
// GET /api/v1/admin/packages/:id/export
|
// GET /api/v1/admin/packages/:id/export
|
||||||
|
|||||||
@@ -23,23 +23,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// defaultBundledPackages is the curated set of packages installed by default.
|
// defaultBundledPackages is the curated set of packages installed by default.
|
||||||
// This is the dev/test bundle — includes demos and workflow examples.
|
|
||||||
// Other packages still ship in the Docker image but require BUNDLED_PACKAGES
|
// Other packages still ship in the Docker image but require BUNDLED_PACKAGES
|
||||||
// to be set explicitly (or "*" for all).
|
// to be set explicitly (or "*" for all).
|
||||||
//
|
//
|
||||||
// Recommended production override (lean):
|
// Recommended production override: BUNDLED_PACKAGES=notes,chat,chat-core,mermaid-renderer,schedules
|
||||||
// BUNDLED_PACKAGES=notes,chat,chat-core,cluster-dashboard
|
|
||||||
var defaultBundledPackages = map[string]bool{
|
var defaultBundledPackages = map[string]bool{
|
||||||
"notes": true,
|
"notes": true,
|
||||||
"chat": true,
|
"chat": true,
|
||||||
"chat-core": true,
|
"chat-core": true,
|
||||||
"workflow-chat": true,
|
"mermaid-renderer": true,
|
||||||
"dashboard": true,
|
"schedules": true,
|
||||||
"cluster-dashboard": true,
|
|
||||||
"workflow-demo": true,
|
|
||||||
"bug-report-triage": true,
|
|
||||||
"content-approval": true,
|
|
||||||
"employee-onboarding": true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallBundledPackages scans bundledDir for .pkg archives and installs
|
// InstallBundledPackages scans bundledDir for .pkg archives and installs
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
// presence.go — Heartbeat upsert and status query (v0.23.1)
|
// presence.go — Heartbeat upsert and status query
|
||||||
//
|
//
|
||||||
// Clients POST /api/v1/presence/heartbeat every 30s while active.
|
// Clients POST /api/v1/presence/heartbeat every 30s while active.
|
||||||
// GET /api/v1/presence?users=id1,id2 returns current status.
|
// GET /api/v1/presence?users=id1,id2 returns current status.
|
||||||
// Online = last_seen within 90s.
|
// Online = last_seen within 90s.
|
||||||
//
|
//
|
||||||
// v0.29.0: Raw SQL replaced with PresenceStore + UserStore methods.
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ package handlers
|
|||||||
// (profile, permissions, teams/mine, settings) into one call.
|
// (profile, permissions, teams/mine, settings) into one call.
|
||||||
// The SDK calls this at startup and on token refresh.
|
// The SDK calls this at startup and on token refresh.
|
||||||
//
|
//
|
||||||
// v0.37.15
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func (s *jsonScanner) Scan(src interface{}) error {
|
|||||||
// memory on the next rows.Scan() call, corrupting our data after the
|
// memory on the next rows.Scan() call, corrupting our data after the
|
||||||
// fact. This caused intermittent SafeJSON 500s on paginated list
|
// fact. This caused intermittent SafeJSON 500s on paginated list
|
||||||
// endpoints where row N's Settings pointed to row N+1's overwritten
|
// endpoints where row N's Settings pointed to row N+1's overwritten
|
||||||
// buffer. Fixed in v0.21.7.
|
// buffer. Fixed.
|
||||||
cp := make([]byte, len(v))
|
cp := make([]byte, len(v))
|
||||||
copy(cp, v)
|
copy(cp, v)
|
||||||
*s.dest = json.RawMessage(cp)
|
*s.dest = json.RawMessage(cp)
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ func TestSafeJSON_PaginatedWithCorrupt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Driver buffer aliasing tests ────────────
|
// ── Driver buffer aliasing tests ────────────
|
||||||
// These verify the fix for the v0.21.7 corruption bug: scanJSON must
|
// These verify the fix for the buffer-aliasing corruption bug: scanJSON must
|
||||||
// copy []byte from the database driver, not alias it. Without the copy,
|
// copy []byte from the database driver, not alias it. Without the copy,
|
||||||
// rows.Next() can overwrite the buffer and corrupt previously-scanned
|
// rows.Next() can overwrite the buffer and corrupt previously-scanned
|
||||||
// json.RawMessage values in a paginated list.
|
// json.RawMessage values in a paginated list.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package handlers
|
|||||||
|
|
||||||
// settings.go — User profile, preferences, and password management.
|
// settings.go — User profile, preferences, and password management.
|
||||||
//
|
//
|
||||||
// v0.29.0: Raw SQL replaced with UserStore methods.
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
// team_package_settings.go — v0.2.0
|
// team_package_settings.go
|
||||||
//
|
//
|
||||||
// Team-admin endpoints for managing team-scoped package settings.
|
// Team-admin endpoints for managing team-scoped package settings.
|
||||||
// These sit in the settings cascade between global (admin) and user.
|
// These sit in the settings cascade between global (admin) and user.
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ func (h *TeamHandler) ListTeamAuditActions(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"actions": actions})
|
c.JSON(http.StatusOK, gin.H{"actions": actions})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Team Roles API (v0.3.4) ─────────────────
|
// ── Team Roles API ─────────────────
|
||||||
|
|
||||||
// builtinRoles are always present in every team's role list.
|
// builtinRoles are always present in every team's role list.
|
||||||
var builtinRoles = []string{"admin", "member"}
|
var builtinRoles = []string{"admin", "member"}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
// Upgrade Tests — v0.5.5
|
// Upgrade Tests
|
||||||
//
|
//
|
||||||
// Schema edge cases, settings migration, and
|
// Schema edge cases, settings migration, and
|
||||||
// package compatibility across kernel upgrades.
|
// package compatibility across kernel upgrades.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
// user_packages.go — v0.30.0 CS5
|
// user_packages.go
|
||||||
//
|
//
|
||||||
// Non-admin package management. Users install personal-scoped packages;
|
// Non-admin package management. Users install personal-scoped packages;
|
||||||
// team admins install team-scoped packages.
|
// team admins install team-scoped packages.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
// workflow_packages.go — v0.30.2 CS1
|
// workflow_packages.go
|
||||||
//
|
//
|
||||||
// Handles workflow-specific package operations:
|
// Handles workflow-specific package operations:
|
||||||
// - ExportWorkflowPackage: bundles a workflow definition + stages into a .pkg
|
// - ExportWorkflowPackage: bundles a workflow definition + stages into a .pkg
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"switchboard-core/workflow"
|
"switchboard-core/workflow"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Public Workflow Handlers (v0.3.3) ───────
|
// ── Public Workflow Handlers ───────
|
||||||
|
|
||||||
// WorkflowPublicHandler manages unauthenticated workflow entry endpoints.
|
// WorkflowPublicHandler manages unauthenticated workflow entry endpoints.
|
||||||
type WorkflowPublicHandler struct {
|
type WorkflowPublicHandler struct {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"switchboard-core/workflow"
|
"switchboard-core/workflow"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Signoff Handlers (v0.3.4) ─────────────────
|
// ── Signoff Handlers ─────────────────
|
||||||
|
|
||||||
// WorkflowSignoffHandler manages signoff HTTP endpoints.
|
// WorkflowSignoffHandler manages signoff HTTP endpoints.
|
||||||
type WorkflowSignoffHandler struct {
|
type WorkflowSignoffHandler struct {
|
||||||
|
|||||||
@@ -603,7 +603,7 @@ func TestWorkflowAssignment_Cancel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── v0.3.3 Store Tests ──────────────────────
|
// ── Instance Lifecycle Store Tests ──────────────────────
|
||||||
|
|
||||||
func TestWorkflowInstance_MarkStale(t *testing.T) {
|
func TestWorkflowInstance_MarkStale(t *testing.T) {
|
||||||
database.RequireTestDB(t)
|
database.RequireTestDB(t)
|
||||||
@@ -779,7 +779,7 @@ func TestWorkflowAssignment_ListByTeam(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Signoff store tests (v0.3.4) ──────────────
|
// ── Signoff store tests ──────────────
|
||||||
|
|
||||||
func TestWorkflowSignoff_CreateAndList(t *testing.T) {
|
func TestWorkflowSignoff_CreateAndList(t *testing.T) {
|
||||||
database.RequireTestDB(t)
|
database.RequireTestDB(t)
|
||||||
@@ -862,7 +862,7 @@ func TestWorkflowSignoff_ListEmpty(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Clone tests (v0.3.5) ──────────────────
|
// ── Clone tests ──────────────────
|
||||||
|
|
||||||
func TestWorkflow_Clone(t *testing.T) {
|
func TestWorkflow_Clone(t *testing.T) {
|
||||||
database.RequireTestDB(t)
|
database.RequireTestDB(t)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"switchboard-core/models"
|
"switchboard-core/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Team-Scoped Workflow Wrappers (v0.31.2) ────────────────
|
// ── Team-Scoped Workflow Wrappers ────────────────
|
||||||
//
|
//
|
||||||
// Thin wrappers that enforce team ownership before delegating
|
// Thin wrappers that enforce team ownership before delegating
|
||||||
// to the existing WorkflowHandler methods. Follows the same
|
// to the existing WorkflowHandler methods. Follows the same
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ func (h *WorkflowHandler) Create(c *gin.Context) {
|
|||||||
|
|
||||||
w.CreatedBy = c.GetString("user_id")
|
w.CreatedBy = c.GetString("user_id")
|
||||||
|
|
||||||
// v0.31.2: team-scoped route injects force_team_id via CreateTeamWorkflow
|
|
||||||
if ftid, ok := c.Get("force_team_id"); ok {
|
if ftid, ok := c.Get("force_team_id"); ok {
|
||||||
tid := ftid.(string)
|
tid := ftid.(string)
|
||||||
w.TeamID = &tid
|
w.TeamID = &tid
|
||||||
@@ -417,7 +416,6 @@ func (h *WorkflowHandler) Clone(c *gin.Context) {
|
|||||||
CreatedBy: c.GetString("user_id"),
|
CreatedBy: c.GetString("user_id"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.31.2: team-scoped route injects force_team_id
|
|
||||||
if ftid, ok := c.Get("force_team_id"); ok {
|
if ftid, ok := c.Get("force_team_id"); ok {
|
||||||
tid := ftid.(string)
|
tid := ftid.(string)
|
||||||
clone.TeamID = &tid
|
clone.TeamID = &tid
|
||||||
|
|||||||
134
server/main.go
134
server/main.go
@@ -36,7 +36,6 @@ import (
|
|||||||
sqliteStore "switchboard-core/store/sqlite"
|
sqliteStore "switchboard-core/store/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
// v0.33.0: Embedded OpenAPI spec and Swagger UI for /api/docs.
|
|
||||||
//
|
//
|
||||||
//go:embed static/openapi.yaml
|
//go:embed static/openapi.yaml
|
||||||
var openapiSpec []byte
|
var openapiSpec []byte
|
||||||
@@ -59,13 +58,10 @@ func main() {
|
|||||||
// ── Server startup ──────────────────────
|
// ── Server startup ──────────────────────
|
||||||
cfg := config.Load()
|
cfg := config.Load()
|
||||||
|
|
||||||
// v0.33.0: Structured logging — must be first so all subsequent
|
// Structured logging — must be first so all subsequent
|
||||||
// log output goes through slog.
|
// log output goes through slog.
|
||||||
logging.Init(cfg.LogFormat, cfg.LogLevel)
|
logging.Init(cfg.LogFormat, cfg.LogLevel)
|
||||||
|
|
||||||
|
|
||||||
// v0.29.1: Config-file provider types (Ollama, LiteLLM, vLLM, etc.)
|
|
||||||
|
|
||||||
var stores store.Stores
|
var stores store.Stores
|
||||||
uekCache := crypto.NewUEKCache()
|
uekCache := crypto.NewUEKCache()
|
||||||
var keyResolver *crypto.KeyResolver
|
var keyResolver *crypto.KeyResolver
|
||||||
@@ -109,7 +105,6 @@ func main() {
|
|||||||
stores = postgres.NewStores(database.DB)
|
stores = postgres.NewStores(database.DB)
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.33.0: Start Prometheus DB pool collector
|
|
||||||
metrics.StartDBCollector(database.DB, 15*time.Second)
|
metrics.StartDBCollector(database.DB, 15*time.Second)
|
||||||
|
|
||||||
// Bootstrap admin from env (K8s secret) — upserts on every restart
|
// Bootstrap admin from env (K8s secret) — upserts on every restart
|
||||||
@@ -161,23 +156,18 @@ func main() {
|
|||||||
sandbox.New(sandbox.DefaultConfig()),
|
sandbox.New(sandbox.DefaultConfig()),
|
||||||
stores,
|
stores,
|
||||||
)
|
)
|
||||||
// v0.38.1: connections module — extension connection resolution
|
|
||||||
starlarkRunner.SetConnectionResolver(handlers.NewConnectionResolverAdapter(stores, keyResolver))
|
starlarkRunner.SetConnectionResolver(handlers.NewConnectionResolverAdapter(stores, keyResolver))
|
||||||
// v0.29.2: db module — extension namespaced table access
|
|
||||||
starlarkRunner.SetDB(database.DB, database.IsPostgres())
|
starlarkRunner.SetDB(database.DB, database.IsPostgres())
|
||||||
// v0.38.0: disk-based script loading + load() support
|
|
||||||
if cfg.StoragePath != "" {
|
if cfg.StoragePath != "" {
|
||||||
starlarkRunner.SetPackagesDir(cfg.StoragePath + "/packages")
|
starlarkRunner.SetPackagesDir(cfg.StoragePath + "/packages")
|
||||||
}
|
}
|
||||||
// v0.5.0: realtime module — extension publish to WebSocket channels
|
|
||||||
starlarkRunner.SetBus(bus)
|
starlarkRunner.SetBus(bus)
|
||||||
// v0.38.5: allow extensions to reach private IPs (self-hosted Gitea, etc.)
|
|
||||||
if os.Getenv("EXT_ALLOW_PRIVATE_IPS") == "true" {
|
if os.Getenv("EXT_ALLOW_PRIVATE_IPS") == "true" {
|
||||||
starlarkRunner.SetAllowPrivateIPs(true)
|
starlarkRunner.SetAllowPrivateIPs(true)
|
||||||
log.Printf(" ⚠️ Extension SSRF protection relaxed: private IPs allowed")
|
log.Printf(" ⚠️ Extension SSRF protection relaxed: private IPs allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Bundled Packages (v0.3.8) ───────────────
|
// ── Bundled Packages ───────────────
|
||||||
// Auto-install bundled .pkg archives on first run.
|
// Auto-install bundled .pkg archives on first run.
|
||||||
// Skipped if SKIP_BUNDLED_PACKAGES=true or packages already exist.
|
// Skipped if SKIP_BUNDLED_PACKAGES=true or packages already exist.
|
||||||
if !cfg.SkipBundledPackages && stores.Packages != nil {
|
if !cfg.SkipBundledPackages && stores.Packages != nil {
|
||||||
@@ -188,7 +178,7 @@ func main() {
|
|||||||
handlers.InstallBundledPackages(cfg.BundledPackagesDir, bundledPkgDir, cfg.BundledPackages, stores, starlarkRunner)
|
handlers.InstallBundledPackages(cfg.BundledPackagesDir, bundledPkgDir, cfg.BundledPackages, stores, starlarkRunner)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Trigger Engine (v0.2.2) ─────────────────
|
// ── Trigger Engine ─────────────────
|
||||||
triggerEngine := triggers.New(stores, starlarkRunner, bus)
|
triggerEngine := triggers.New(stores, starlarkRunner, bus)
|
||||||
if err := triggerEngine.Start(context.Background()); err != nil {
|
if err := triggerEngine.Start(context.Background()); err != nil {
|
||||||
log.Printf(" ⚠️ Trigger engine start: %v", err)
|
log.Printf(" ⚠️ Trigger engine start: %v", err)
|
||||||
@@ -210,7 +200,7 @@ func main() {
|
|||||||
// ── WebSocket Hub ─────────────────────────
|
// ── WebSocket Hub ─────────────────────────
|
||||||
hub := events.NewHub(bus, middleware.GetAllowedOrigins(cfg))
|
hub := events.NewHub(bus, middleware.GetAllowedOrigins(cfg))
|
||||||
|
|
||||||
// ── Cluster Registry (v0.6.0) ────────────
|
// ── Cluster Registry ────────────
|
||||||
// PG-backed node self-registration + heartbeat. No-op on SQLite.
|
// PG-backed node self-registration + heartbeat. No-op on SQLite.
|
||||||
var clusterReg *cluster.Registry
|
var clusterReg *cluster.Registry
|
||||||
if database.IsPostgres() && stores.Cluster != nil {
|
if database.IsPostgres() && stores.Cluster != nil {
|
||||||
@@ -231,10 +221,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── WebSocket Ticket Store (v0.32.0: PG-backed for cross-pod) ─────
|
// ── WebSocket Ticket Store ─────
|
||||||
ticketAdapter := &events.TicketValidatorAdapter{Store: stores.Tickets}
|
ticketAdapter := &events.TicketValidatorAdapter{Store: stores.Tickets}
|
||||||
|
|
||||||
// ── Notification Service (v0.20.0) ───────
|
// ── Notification Service ───────
|
||||||
var notifSvc *notifications.Service
|
var notifSvc *notifications.Service
|
||||||
if stores.Notifications != nil {
|
if stores.Notifications != nil {
|
||||||
notifSvc = notifications.NewService(stores.Notifications, hub).
|
notifSvc = notifications.NewService(stores.Notifications, hub).
|
||||||
@@ -277,7 +267,6 @@ func main() {
|
|||||||
c.JSON(200, info)
|
c.JSON(200, info)
|
||||||
})
|
})
|
||||||
|
|
||||||
// v0.32.0: Kubernetes probe endpoints
|
|
||||||
// Liveness: process is alive and serving (no dependency checks).
|
// Liveness: process is alive and serving (no dependency checks).
|
||||||
base.GET("/healthz/live", func(c *gin.Context) {
|
base.GET("/healthz/live", func(c *gin.Context) {
|
||||||
c.JSON(200, gin.H{"status": "ok"})
|
c.JSON(200, gin.H{"status": "ok"})
|
||||||
@@ -299,10 +288,8 @@ func main() {
|
|||||||
c.JSON(200, gin.H{"status": "ok"})
|
c.JSON(200, gin.H{"status": "ok"})
|
||||||
})
|
})
|
||||||
|
|
||||||
// v0.33.0: Prometheus metrics endpoint (no auth — Prometheus scrapes directly)
|
|
||||||
base.GET("/metrics", gin.WrapH(promhttp.Handler()))
|
base.GET("/metrics", gin.WrapH(promhttp.Handler()))
|
||||||
|
|
||||||
// v0.33.0: OpenAPI spec + Swagger UI (no auth — documentation)
|
|
||||||
base.GET("/api/docs", func(c *gin.Context) {
|
base.GET("/api/docs", func(c *gin.Context) {
|
||||||
c.Data(http.StatusOK, "text/html; charset=utf-8", swaggerHTML)
|
c.Data(http.StatusOK, "text/html; charset=utf-8", swaggerHTML)
|
||||||
})
|
})
|
||||||
@@ -311,7 +298,6 @@ func main() {
|
|||||||
patched := bytes.Replace(openapiSpec, []byte("${VERSION}"), []byte(Version), 1)
|
patched := bytes.Replace(openapiSpec, []byte("${VERSION}"), []byte(Version), 1)
|
||||||
c.Data(http.StatusOK, "application/yaml", patched)
|
c.Data(http.StatusOK, "application/yaml", patched)
|
||||||
})
|
})
|
||||||
// v0.6.2: Dynamic OpenAPI spec with extension routes merged in
|
|
||||||
base.GET("/api/docs/openapi.json", func(c *gin.Context) {
|
base.GET("/api/docs/openapi.json", func(c *gin.Context) {
|
||||||
spec, err := handlers.BuildOpenAPISpec(stores, openapiSpec, Version)
|
spec, err := handlers.BuildOpenAPISpec(stores, openapiSpec, Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -401,7 +387,7 @@ func main() {
|
|||||||
extH := handlers.NewExtensionHandler(stores)
|
extH := handlers.NewExtensionHandler(stores)
|
||||||
api.GET("/extensions/:id/assets/*path", extH.ServeExtensionAsset)
|
api.GET("/extensions/:id/assets/*path", extH.ServeExtensionAsset)
|
||||||
|
|
||||||
// ── Webhook Inbound (v0.2.2) ──────────────
|
// ── Webhook Inbound ──────────────
|
||||||
// Public routes — HMAC-verified in handler, no auth middleware.
|
// Public routes — HMAC-verified in handler, no auth middleware.
|
||||||
api.POST("/hooks/:package_id/:slug", triggerEngine.HandleWebhook)
|
api.POST("/hooks/:package_id/:slug", triggerEngine.HandleWebhook)
|
||||||
api.GET("/hooks/:package_id/:slug", triggerEngine.HandleWebhook)
|
api.GET("/hooks/:package_id/:slug", triggerEngine.HandleWebhook)
|
||||||
@@ -409,7 +395,7 @@ func main() {
|
|||||||
// ── Workflow Engine (shared by public + protected routes) ──
|
// ── Workflow Engine (shared by public + protected routes) ──
|
||||||
wfEngine := workflow.NewEngine(stores, bus, starlarkRunner)
|
wfEngine := workflow.NewEngine(stores, bus, starlarkRunner)
|
||||||
|
|
||||||
// ── Public Workflow Entry (v0.3.3) ─────
|
// ── Public Workflow Entry ─────
|
||||||
publicWfH := handlers.NewWorkflowPublicHandler(wfEngine, stores)
|
publicWfH := handlers.NewWorkflowPublicHandler(wfEngine, stores)
|
||||||
publicWf := api.Group("/public/workflows")
|
publicWf := api.Group("/public/workflows")
|
||||||
publicWf.Use(authLimiter.Limit())
|
publicWf.Use(authLimiter.Limit())
|
||||||
@@ -419,7 +405,7 @@ func main() {
|
|||||||
publicWf.POST("/advance/:token", publicWfH.AdvancePublic)
|
publicWf.POST("/advance/:token", publicWfH.AdvancePublic)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Workflow Scanner (v0.3.3) ──────────
|
// ── Workflow Scanner ──────────
|
||||||
wfScanner := workflow.NewScanner(stores, bus)
|
wfScanner := workflow.NewScanner(stores, bus)
|
||||||
wfScanner.Start()
|
wfScanner.Start()
|
||||||
defer wfScanner.Stop()
|
defer wfScanner.Stop()
|
||||||
@@ -429,7 +415,7 @@ func main() {
|
|||||||
protected.Use(middleware.Auth(cfg, stores.Users, userCache))
|
protected.Use(middleware.Auth(cfg, stores.Users, userCache))
|
||||||
protected.Use(middleware.ValidatePathParams())
|
protected.Use(middleware.ValidatePathParams())
|
||||||
{
|
{
|
||||||
// ── WebSocket Ticket (v0.28.8) ───────────
|
// ── WebSocket Ticket ───────────
|
||||||
// Issue a single-use ticket for WebSocket auth.
|
// Issue a single-use ticket for WebSocket auth.
|
||||||
// Client fetches this, then connects with ?ticket=<opaque>.
|
// Client fetches this, then connects with ?ticket=<opaque>.
|
||||||
protected.POST("/ws/ticket", func(c *gin.Context) {
|
protected.POST("/ws/ticket", func(c *gin.Context) {
|
||||||
@@ -442,15 +428,15 @@ func main() {
|
|||||||
c.JSON(http.StatusOK, gin.H{"ticket": ticket})
|
c.JSON(http.StatusOK, gin.H{"ticket": ticket})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Presence (v0.23.1)
|
// Presence
|
||||||
presence := handlers.NewPresenceHandler(stores)
|
presence := handlers.NewPresenceHandler(stores)
|
||||||
protected.POST("/presence/heartbeat", presence.Heartbeat)
|
protected.POST("/presence/heartbeat", presence.Heartbeat)
|
||||||
protected.GET("/presence", presence.Query)
|
protected.GET("/presence", presence.Query)
|
||||||
|
|
||||||
// User search (v0.23.2 — DM user picker)
|
// User search
|
||||||
protected.GET("/users/search", presence.SearchUsers)
|
protected.GET("/users/search", presence.SearchUsers)
|
||||||
|
|
||||||
// Workflows (v0.26.1 — team-owned staged processes)
|
// Workflows
|
||||||
wfH := handlers.NewWorkflowHandler(stores)
|
wfH := handlers.NewWorkflowHandler(stores)
|
||||||
protected.GET("/workflows", wfH.List)
|
protected.GET("/workflows", wfH.List)
|
||||||
protected.POST("/workflows", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfH.Create)
|
protected.POST("/workflows", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfH.Create)
|
||||||
@@ -466,7 +452,7 @@ func main() {
|
|||||||
protected.POST("/workflows/:id/clone", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfH.Clone)
|
protected.POST("/workflows/:id/clone", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfH.Clone)
|
||||||
protected.GET("/workflows/:id/versions/:version", wfH.GetVersion)
|
protected.GET("/workflows/:id/versions/:version", wfH.GetVersion)
|
||||||
|
|
||||||
// Workflow instances + assignments (v0.3.2)
|
// Workflow instances + assignments
|
||||||
wfInstH := handlers.NewWorkflowInstanceHandler(wfEngine, stores)
|
wfInstH := handlers.NewWorkflowInstanceHandler(wfEngine, stores)
|
||||||
protected.POST("/workflows/:id/instances", middleware.RequirePermission(auth.PermWorkflowSubmit, stores), wfInstH.Start)
|
protected.POST("/workflows/:id/instances", middleware.RequirePermission(auth.PermWorkflowSubmit, stores), wfInstH.Start)
|
||||||
protected.GET("/workflows/:id/instances", wfInstH.ListInstances)
|
protected.GET("/workflows/:id/instances", wfInstH.ListInstances)
|
||||||
@@ -481,16 +467,16 @@ func main() {
|
|||||||
protected.POST("/assignments/:id/cancel", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfAssignH.Cancel)
|
protected.POST("/assignments/:id/cancel", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfAssignH.Cancel)
|
||||||
protected.GET("/assignments/mine", wfAssignH.ListMine)
|
protected.GET("/assignments/mine", wfAssignH.ListMine)
|
||||||
|
|
||||||
// Workflow signoffs (v0.3.4)
|
// Workflow signoffs
|
||||||
wfSignoffH := handlers.NewWorkflowSignoffHandler(wfEngine, stores)
|
wfSignoffH := handlers.NewWorkflowSignoffHandler(wfEngine, stores)
|
||||||
protected.POST("/instances/:iid/signoffs", wfSignoffH.Submit)
|
protected.POST("/instances/:iid/signoffs", wfSignoffH.Submit)
|
||||||
protected.GET("/instances/:iid/signoffs", wfSignoffH.List)
|
protected.GET("/instances/:iid/signoffs", wfSignoffH.List)
|
||||||
|
|
||||||
// Surface discovery (v0.25.0, v0.28.7: unified packages)
|
// Surface discovery
|
||||||
pkgH := handlers.NewPackageHandler(stores)
|
pkgH := handlers.NewPackageHandler(stores)
|
||||||
protected.GET("/surfaces", pkgH.ListEnabledSurfaces)
|
protected.GET("/surfaces", pkgH.ListEnabledSurfaces)
|
||||||
|
|
||||||
// User package management (v0.30.0)
|
// User package management
|
||||||
userPkgDir := ""
|
userPkgDir := ""
|
||||||
if cfg.StoragePath != "" {
|
if cfg.StoragePath != "" {
|
||||||
userPkgDir = cfg.StoragePath + "/packages"
|
userPkgDir = cfg.StoragePath + "/packages"
|
||||||
@@ -500,11 +486,11 @@ func main() {
|
|||||||
protected.POST("/packages/install", userPkgH.InstallPersonalPackage)
|
protected.POST("/packages/install", userPkgH.InstallPersonalPackage)
|
||||||
protected.DELETE("/packages/:id", userPkgH.DeletePersonalPackage)
|
protected.DELETE("/packages/:id", userPkgH.DeletePersonalPackage)
|
||||||
|
|
||||||
// Connection Type Discovery (v0.38.4)
|
// Connection Type Discovery
|
||||||
connTypeH := handlers.NewConnectionTypeHandler(stores)
|
connTypeH := handlers.NewConnectionTypeHandler(stores)
|
||||||
protected.GET("/connection-types", connTypeH.ListConnectionTypes)
|
protected.GET("/connection-types", connTypeH.ListConnectionTypes)
|
||||||
|
|
||||||
// Extension Connections (personal scope — v0.38.1)
|
// Extension Connections (personal scope
|
||||||
connH := handlers.NewConnectionHandler(stores, keyResolver)
|
connH := handlers.NewConnectionHandler(stores, keyResolver)
|
||||||
protected.GET("/connections", connH.ListConnections)
|
protected.GET("/connections", connH.ListConnections)
|
||||||
protected.POST("/connections", connH.CreateConnection)
|
protected.POST("/connections", connH.CreateConnection)
|
||||||
@@ -521,21 +507,21 @@ func main() {
|
|||||||
protected.GET("/settings", settings.GetSettings)
|
protected.GET("/settings", settings.GetSettings)
|
||||||
protected.PUT("/settings", settings.UpdateSettings)
|
protected.PUT("/settings", settings.UpdateSettings)
|
||||||
|
|
||||||
// Documentation (v0.6.1)
|
// Documentation
|
||||||
docsDir := findDocsDir()
|
docsDir := findDocsDir()
|
||||||
docsH := handlers.NewDocsHandler(docsDir)
|
docsH := handlers.NewDocsHandler(docsDir)
|
||||||
protected.GET("/docs", docsH.ListDocs)
|
protected.GET("/docs", docsH.ListDocs)
|
||||||
protected.GET("/docs/:name", docsH.GetDoc)
|
protected.GET("/docs/:name", docsH.GetDoc)
|
||||||
|
|
||||||
// Permission bootstrap (v0.37.1) — self-service resolved permissions
|
// Permission bootstrap — self-service resolved permissions
|
||||||
permH := handlers.NewProfilePermissionsHandler(stores)
|
permH := handlers.NewProfilePermissionsHandler(stores)
|
||||||
protected.GET("/profile/permissions", permH.GetMyPermissions)
|
protected.GET("/profile/permissions", permH.GetMyPermissions)
|
||||||
|
|
||||||
// Boot payload (v0.37.15) — single-call SDK bootstrap
|
// Boot payload — single-call SDK bootstrap
|
||||||
bootH := handlers.NewProfileBootstrapHandler(stores)
|
bootH := handlers.NewProfileBootstrapHandler(stores)
|
||||||
protected.GET("/profile/bootstrap", bootH.GetBootstrap)
|
protected.GET("/profile/bootstrap", bootH.GetBootstrap)
|
||||||
|
|
||||||
// Notifications (v0.20.0)
|
// Notifications
|
||||||
notifH := handlers.NewNotificationHandler(stores, hub)
|
notifH := handlers.NewNotificationHandler(stores, hub)
|
||||||
protected.GET("/notifications", notifH.List)
|
protected.GET("/notifications", notifH.List)
|
||||||
protected.GET("/notifications/unread-count", notifH.UnreadCount)
|
protected.GET("/notifications/unread-count", notifH.UnreadCount)
|
||||||
@@ -543,12 +529,12 @@ func main() {
|
|||||||
protected.POST("/notifications/mark-all-read", notifH.MarkAllRead)
|
protected.POST("/notifications/mark-all-read", notifH.MarkAllRead)
|
||||||
protected.DELETE("/notifications/:id", notifH.Delete)
|
protected.DELETE("/notifications/:id", notifH.Delete)
|
||||||
|
|
||||||
// Notification preferences (v0.20.0 Phase 3)
|
// Notification preferences
|
||||||
protected.GET("/notifications/preferences", notifH.ListPreferences)
|
protected.GET("/notifications/preferences", notifH.ListPreferences)
|
||||||
protected.PUT("/notifications/preferences/:type", notifH.SetPreference)
|
protected.PUT("/notifications/preferences/:type", notifH.SetPreference)
|
||||||
protected.DELETE("/notifications/preferences/:type", notifH.DeletePreference)
|
protected.DELETE("/notifications/preferences/:type", notifH.DeletePreference)
|
||||||
|
|
||||||
// ── Scheduled Tasks — User (v0.2.2) ────
|
// ── Scheduled Tasks — User ────
|
||||||
userSchedH := handlers.NewScheduleHandler(stores, triggerEngine)
|
userSchedH := handlers.NewScheduleHandler(stores, triggerEngine)
|
||||||
protected.GET("/schedules", userSchedH.ListMySchedules)
|
protected.GET("/schedules", userSchedH.ListMySchedules)
|
||||||
protected.POST("/schedules", userSchedH.CreateSchedule)
|
protected.POST("/schedules", userSchedH.CreateSchedule)
|
||||||
@@ -562,7 +548,7 @@ func main() {
|
|||||||
teams := handlers.NewTeamHandler(stores, keyResolver)
|
teams := handlers.NewTeamHandler(stores, keyResolver)
|
||||||
protected.GET("/teams/mine", teams.MyTeams)
|
protected.GET("/teams/mine", teams.MyTeams)
|
||||||
|
|
||||||
// Groups (user: my groups — v0.16.0)
|
// Groups (user: my groups
|
||||||
groupH := handlers.NewGroupHandler(stores)
|
groupH := handlers.NewGroupHandler(stores)
|
||||||
protected.GET("/groups/mine", groupH.MyGroups)
|
protected.GET("/groups/mine", groupH.MyGroups)
|
||||||
|
|
||||||
@@ -580,18 +566,18 @@ func main() {
|
|||||||
// Team groups (team admins manage team-scoped groups)
|
// Team groups (team admins manage team-scoped groups)
|
||||||
teamScoped.GET("/groups", groupH.ListTeamGroups)
|
teamScoped.GET("/groups", groupH.ListTeamGroups)
|
||||||
|
|
||||||
// Team connections (v0.38.1)
|
// Team connections
|
||||||
teamScoped.GET("/connections", teams.ListTeamConnections)
|
teamScoped.GET("/connections", teams.ListTeamConnections)
|
||||||
teamScoped.POST("/connections", teams.CreateTeamConnection)
|
teamScoped.POST("/connections", teams.CreateTeamConnection)
|
||||||
teamScoped.PUT("/connections/:id", teams.UpdateTeamConnection)
|
teamScoped.PUT("/connections/:id", teams.UpdateTeamConnection)
|
||||||
teamScoped.DELETE("/connections/:id", teams.DeleteTeamConnection)
|
teamScoped.DELETE("/connections/:id", teams.DeleteTeamConnection)
|
||||||
|
|
||||||
// Team package management (v0.30.0)
|
// Team package management
|
||||||
teamPkgH := handlers.NewUserPackageHandler(stores, userPkgDir)
|
teamPkgH := handlers.NewUserPackageHandler(stores, userPkgDir)
|
||||||
teamScoped.POST("/packages/install", teamPkgH.InstallTeamPackage)
|
teamScoped.POST("/packages/install", teamPkgH.InstallTeamPackage)
|
||||||
teamScoped.DELETE("/packages/:id", teamPkgH.DeleteTeamPackage)
|
teamScoped.DELETE("/packages/:id", teamPkgH.DeleteTeamPackage)
|
||||||
|
|
||||||
// Team package settings — cascade overrides (v0.2.0)
|
// Team package settings — cascade overrides
|
||||||
teamPkgSettingsH := handlers.NewTeamPackageSettingsHandler(stores)
|
teamPkgSettingsH := handlers.NewTeamPackageSettingsHandler(stores)
|
||||||
teamScoped.GET("/packages/:id/settings", teamPkgSettingsH.GetTeamPackageSettings)
|
teamScoped.GET("/packages/:id/settings", teamPkgSettingsH.GetTeamPackageSettings)
|
||||||
teamScoped.PUT("/packages/:id/settings", teamPkgSettingsH.UpdateTeamPackageSettings)
|
teamScoped.PUT("/packages/:id/settings", teamPkgSettingsH.UpdateTeamPackageSettings)
|
||||||
@@ -601,7 +587,7 @@ func main() {
|
|||||||
teamScoped.GET("/audit", teams.ListTeamAuditLog)
|
teamScoped.GET("/audit", teams.ListTeamAuditLog)
|
||||||
teamScoped.GET("/audit/actions", teams.ListTeamAuditActions)
|
teamScoped.GET("/audit/actions", teams.ListTeamAuditActions)
|
||||||
|
|
||||||
// Team workflows — self-service (v0.31.2)
|
// Team workflows — self-service
|
||||||
teamWfH := handlers.NewWorkflowHandler(stores)
|
teamWfH := handlers.NewWorkflowHandler(stores)
|
||||||
teamScoped.GET("/workflows", teamWfH.ListTeamWorkflows)
|
teamScoped.GET("/workflows", teamWfH.ListTeamWorkflows)
|
||||||
teamScoped.POST("/workflows", teamWfH.CreateTeamWorkflow)
|
teamScoped.POST("/workflows", teamWfH.CreateTeamWorkflow)
|
||||||
@@ -619,7 +605,7 @@ func main() {
|
|||||||
teamScoped.GET("/workflows/available", teamWfH.ListGlobalWorkflows)
|
teamScoped.GET("/workflows/available", teamWfH.ListGlobalWorkflows)
|
||||||
teamScoped.GET("/workflows/:id/versions/:version", teamWfH.GetTeamWorkflowVersion)
|
teamScoped.GET("/workflows/:id/versions/:version", teamWfH.GetTeamWorkflowVersion)
|
||||||
|
|
||||||
// Team workflow instances + assignments (v0.3.2)
|
// Team workflow instances + assignments
|
||||||
teamWfInstH := handlers.NewWorkflowInstanceHandler(wfEngine, stores)
|
teamWfInstH := handlers.NewWorkflowInstanceHandler(wfEngine, stores)
|
||||||
teamScoped.POST("/workflows/:id/instances", teamWfInstH.Start)
|
teamScoped.POST("/workflows/:id/instances", teamWfInstH.Start)
|
||||||
teamScoped.GET("/workflows/:id/instances", teamWfInstH.ListInstances)
|
teamScoped.GET("/workflows/:id/instances", teamWfInstH.ListInstances)
|
||||||
@@ -630,7 +616,7 @@ func main() {
|
|||||||
teamWfAssignH := handlers.NewWorkflowAssignmentHandler(wfEngine, stores)
|
teamWfAssignH := handlers.NewWorkflowAssignmentHandler(wfEngine, stores)
|
||||||
teamScoped.GET("/assignments", teamWfAssignH.ListByTeam)
|
teamScoped.GET("/assignments", teamWfAssignH.ListByTeam)
|
||||||
|
|
||||||
// Team workflow signoffs (v0.3.4)
|
// Team workflow signoffs
|
||||||
teamWfSignoffH := handlers.NewWorkflowSignoffHandler(wfEngine, stores)
|
teamWfSignoffH := handlers.NewWorkflowSignoffHandler(wfEngine, stores)
|
||||||
teamScoped.POST("/instances/:iid/signoffs", teamWfSignoffH.Submit)
|
teamScoped.POST("/instances/:iid/signoffs", teamWfSignoffH.Submit)
|
||||||
teamScoped.GET("/instances/:iid/signoffs", teamWfSignoffH.List)
|
teamScoped.GET("/instances/:iid/signoffs", teamWfSignoffH.List)
|
||||||
@@ -674,7 +660,7 @@ func main() {
|
|||||||
// Stats
|
// Stats
|
||||||
admin.GET("/stats", adm.GetStats)
|
admin.GET("/stats", adm.GetStats)
|
||||||
|
|
||||||
// Global Connections (v0.38.1)
|
// Global Connections
|
||||||
admin.GET("/connections", adm.ListGlobalConnections)
|
admin.GET("/connections", adm.ListGlobalConnections)
|
||||||
admin.POST("/connections", adm.CreateGlobalConnection)
|
admin.POST("/connections", adm.CreateGlobalConnection)
|
||||||
admin.PUT("/connections/:id", adm.UpdateGlobalConnection)
|
admin.PUT("/connections/:id", adm.UpdateGlobalConnection)
|
||||||
@@ -692,7 +678,7 @@ func main() {
|
|||||||
admin.PUT("/teams/:id/members/:memberId", teamAdm.UpdateMember)
|
admin.PUT("/teams/:id/members/:memberId", teamAdm.UpdateMember)
|
||||||
admin.DELETE("/teams/:id/members/:memberId", teamAdm.RemoveMember)
|
admin.DELETE("/teams/:id/members/:memberId", teamAdm.RemoveMember)
|
||||||
|
|
||||||
// Admin broadcast (v0.28.6)
|
// Admin broadcast
|
||||||
adminNotifH := handlers.NewNotificationHandler(stores, hub)
|
adminNotifH := handlers.NewNotificationHandler(stores, hub)
|
||||||
admin.POST("/notifications/broadcast", adminNotifH.Broadcast)
|
admin.POST("/notifications/broadcast", adminNotifH.Broadcast)
|
||||||
|
|
||||||
@@ -700,7 +686,7 @@ func main() {
|
|||||||
admin.GET("/audit", adm.ListAuditLog)
|
admin.GET("/audit", adm.ListAuditLog)
|
||||||
admin.GET("/audit/actions", adm.ListAuditActions)
|
admin.GET("/audit/actions", adm.ListAuditActions)
|
||||||
|
|
||||||
// Groups (admin — v0.16.0)
|
// Groups (admin
|
||||||
groupAdm := handlers.NewGroupHandler(stores)
|
groupAdm := handlers.NewGroupHandler(stores)
|
||||||
admin.GET("/groups", groupAdm.ListGroups)
|
admin.GET("/groups", groupAdm.ListGroups)
|
||||||
admin.POST("/groups", groupAdm.CreateGroup)
|
admin.POST("/groups", groupAdm.CreateGroup)
|
||||||
@@ -711,11 +697,11 @@ func main() {
|
|||||||
admin.POST("/groups/:id/members", groupAdm.AddMember)
|
admin.POST("/groups/:id/members", groupAdm.AddMember)
|
||||||
admin.DELETE("/groups/:id/members/:userId", groupAdm.RemoveMember)
|
admin.DELETE("/groups/:id/members/:userId", groupAdm.RemoveMember)
|
||||||
|
|
||||||
// Permissions (v0.24.2)
|
// Permissions
|
||||||
admin.GET("/permissions", groupAdm.ListPermissions)
|
admin.GET("/permissions", groupAdm.ListPermissions)
|
||||||
admin.GET("/users/:id/permissions", groupAdm.GetUserPermissions)
|
admin.GET("/users/:id/permissions", groupAdm.GetUserPermissions)
|
||||||
|
|
||||||
// Resource Grants (admin — v0.16.0)
|
// Resource Grants (admin
|
||||||
admin.GET("/grants/:type/:id", groupAdm.GetResourceGrant)
|
admin.GET("/grants/:type/:id", groupAdm.GetResourceGrant)
|
||||||
admin.PUT("/grants/:type/:id", groupAdm.SetResourceGrant)
|
admin.PUT("/grants/:type/:id", groupAdm.SetResourceGrant)
|
||||||
|
|
||||||
@@ -728,7 +714,7 @@ func main() {
|
|||||||
// Vault
|
// Vault
|
||||||
admin.GET("/vault/status", adm.VaultStatus)
|
admin.GET("/vault/status", adm.VaultStatus)
|
||||||
|
|
||||||
// Email / SMTP test (v0.20.0 Phase 3)
|
// Email / SMTP test
|
||||||
emailAdm := handlers.NewAdminEmailHandler(stores)
|
emailAdm := handlers.NewAdminEmailHandler(stores)
|
||||||
admin.POST("/notifications/test-email", emailAdm.TestEmail)
|
admin.POST("/notifications/test-email", emailAdm.TestEmail)
|
||||||
|
|
||||||
@@ -739,7 +725,7 @@ func main() {
|
|||||||
admin.PUT("/extensions/:id", extAdm.AdminUpdateExtension)
|
admin.PUT("/extensions/:id", extAdm.AdminUpdateExtension)
|
||||||
admin.DELETE("/extensions/:id", extAdm.AdminUninstallExtension)
|
admin.DELETE("/extensions/:id", extAdm.AdminUninstallExtension)
|
||||||
|
|
||||||
// Extension permissions (admin — v0.29.0)
|
// Extension permissions (admin
|
||||||
extPermH := handlers.NewExtPermHandler(stores)
|
extPermH := handlers.NewExtPermHandler(stores)
|
||||||
admin.GET("/extensions/:id/permissions", extPermH.ListPackagePermissions)
|
admin.GET("/extensions/:id/permissions", extPermH.ListPackagePermissions)
|
||||||
admin.GET("/extensions/:id/review", extPermH.ReviewPackage)
|
admin.GET("/extensions/:id/review", extPermH.ReviewPackage)
|
||||||
@@ -747,21 +733,21 @@ func main() {
|
|||||||
admin.POST("/extensions/:id/permissions/:perm/revoke", extPermH.RevokePermission)
|
admin.POST("/extensions/:id/permissions/:perm/revoke", extPermH.RevokePermission)
|
||||||
admin.POST("/extensions/:id/permissions/grant-all", extPermH.GrantAllPermissions)
|
admin.POST("/extensions/:id/permissions/grant-all", extPermH.GrantAllPermissions)
|
||||||
|
|
||||||
// Extension secrets (admin — v0.29.0 CS3)
|
// Extension secrets (admin
|
||||||
extSecH := handlers.NewExtSecretsHandler(stores)
|
extSecH := handlers.NewExtSecretsHandler(stores)
|
||||||
admin.GET("/extensions/:id/secrets", extSecH.GetSecrets)
|
admin.GET("/extensions/:id/secrets", extSecH.GetSecrets)
|
||||||
admin.PUT("/extensions/:id/secrets", extSecH.SetSecrets)
|
admin.PUT("/extensions/:id/secrets", extSecH.SetSecrets)
|
||||||
admin.DELETE("/extensions/:id/secrets", extSecH.DeleteSecrets)
|
admin.DELETE("/extensions/:id/secrets", extSecH.DeleteSecrets)
|
||||||
|
|
||||||
// Package lifecycle management (v0.28.7 — replaces surfaces + extensions admin)
|
// Package lifecycle management
|
||||||
packagesDir := ""
|
packagesDir := ""
|
||||||
if cfg.StoragePath != "" {
|
if cfg.StoragePath != "" {
|
||||||
packagesDir = cfg.StoragePath + "/packages"
|
packagesDir = cfg.StoragePath + "/packages"
|
||||||
}
|
}
|
||||||
pkgAdm := handlers.NewPackageHandler(stores, packagesDir)
|
pkgAdm := handlers.NewPackageHandler(stores, packagesDir)
|
||||||
pkgAdm.SetSandbox(sandbox.New(sandbox.DefaultConfig())) // v0.30.0: schema migrations
|
pkgAdm.SetSandbox(sandbox.New(sandbox.DefaultConfig()))
|
||||||
|
|
||||||
// Package registry — must be registered before /packages/:id (v0.30.0)
|
// Package registry — must be registered before /packages/:id
|
||||||
registryH := handlers.NewRegistryHandler(stores, packagesDir, pkgAdm)
|
registryH := handlers.NewRegistryHandler(stores, packagesDir, pkgAdm)
|
||||||
admin.GET("/packages/registry", registryH.BrowseRegistry)
|
admin.GET("/packages/registry", registryH.BrowseRegistry)
|
||||||
admin.POST("/packages/registry/install", registryH.InstallFromRegistry)
|
admin.POST("/packages/registry/install", registryH.InstallFromRegistry)
|
||||||
@@ -772,20 +758,20 @@ func main() {
|
|||||||
admin.PUT("/packages/:id/enable", pkgAdm.EnablePackage)
|
admin.PUT("/packages/:id/enable", pkgAdm.EnablePackage)
|
||||||
admin.PUT("/packages/:id/disable", pkgAdm.DisablePackage)
|
admin.PUT("/packages/:id/disable", pkgAdm.DisablePackage)
|
||||||
admin.DELETE("/packages/:id", pkgAdm.DeletePackage)
|
admin.DELETE("/packages/:id", pkgAdm.DeletePackage)
|
||||||
admin.GET("/packages/:id/settings", pkgAdm.GetPackageSettings) // v0.30.0
|
admin.GET("/packages/:id/settings", pkgAdm.GetPackageSettings)
|
||||||
admin.PUT("/packages/:id/settings", pkgAdm.UpdatePackageSettings) // v0.30.0
|
admin.PUT("/packages/:id/settings", pkgAdm.UpdatePackageSettings)
|
||||||
admin.GET("/packages/:id/dependencies", pkgAdm.ListDependencies) // v0.38.2
|
admin.GET("/packages/:id/dependencies", pkgAdm.ListDependencies)
|
||||||
admin.GET("/packages/:id/consumers", pkgAdm.ListConsumers) // v0.38.2
|
admin.GET("/packages/:id/consumers", pkgAdm.ListConsumers)
|
||||||
admin.POST("/packages/:id/test-tool", pkgAdm.TestTool) // v0.38.2
|
admin.POST("/packages/:id/test-tool", pkgAdm.TestTool)
|
||||||
admin.POST("/packages/:id/update", pkgAdm.UpdatePackage) // v0.5.4
|
admin.POST("/packages/:id/update", pkgAdm.UpdatePackage)
|
||||||
admin.GET("/packages/:id/export", pkgAdm.ExportPackage) // v0.5.4
|
admin.GET("/packages/:id/export", pkgAdm.ExportPackage)
|
||||||
admin.GET("/dependencies", pkgAdm.ListAllDependencies) // v0.38.2
|
admin.GET("/dependencies", pkgAdm.ListAllDependencies)
|
||||||
|
|
||||||
// Workflow package export (v0.30.2)
|
// Workflow package export
|
||||||
wfPkgH := handlers.NewWorkflowPackageHandler(stores)
|
wfPkgH := handlers.NewWorkflowPackageHandler(stores)
|
||||||
admin.GET("/workflows/:id/export", wfPkgH.ExportWorkflowPackage)
|
admin.GET("/workflows/:id/export", wfPkgH.ExportWorkflowPackage)
|
||||||
|
|
||||||
// ── Triggers (v0.2.2) ─────────────────
|
// ── Triggers ─────────────────
|
||||||
trigH := handlers.NewTriggerHandler(stores, triggerEngine)
|
trigH := handlers.NewTriggerHandler(stores, triggerEngine)
|
||||||
admin.GET("/triggers", trigH.ListTriggers)
|
admin.GET("/triggers", trigH.ListTriggers)
|
||||||
admin.GET("/triggers/:id", trigH.GetTrigger)
|
admin.GET("/triggers/:id", trigH.GetTrigger)
|
||||||
@@ -795,20 +781,20 @@ func main() {
|
|||||||
admin.GET("/triggers/:id/logs", trigH.ListTriggerLogs)
|
admin.GET("/triggers/:id/logs", trigH.ListTriggerLogs)
|
||||||
admin.GET("/packages/:id/triggers", trigH.ListPackageTriggers)
|
admin.GET("/packages/:id/triggers", trigH.ListPackageTriggers)
|
||||||
|
|
||||||
// ── Scheduled Tasks — Admin (v0.2.2) ──
|
// ── Scheduled Tasks — Admin ──
|
||||||
schedH := handlers.NewScheduleHandler(stores, triggerEngine)
|
schedH := handlers.NewScheduleHandler(stores, triggerEngine)
|
||||||
admin.GET("/schedules", schedH.AdminListSchedules)
|
admin.GET("/schedules", schedH.AdminListSchedules)
|
||||||
admin.PUT("/schedules/:id/enable", schedH.AdminEnableSchedule)
|
admin.PUT("/schedules/:id/enable", schedH.AdminEnableSchedule)
|
||||||
admin.PUT("/schedules/:id/disable", schedH.AdminDisableSchedule)
|
admin.PUT("/schedules/:id/disable", schedH.AdminDisableSchedule)
|
||||||
admin.DELETE("/schedules/:id", schedH.AdminDeleteSchedule)
|
admin.DELETE("/schedules/:id", schedH.AdminDeleteSchedule)
|
||||||
|
|
||||||
// ── Cluster (v0.6.0) ─────────────────
|
// ── Cluster ─────────────────
|
||||||
if stores.Cluster != nil {
|
if stores.Cluster != nil {
|
||||||
clusterH := handlers.NewClusterHandler(stores)
|
clusterH := handlers.NewClusterHandler(stores)
|
||||||
admin.GET("/cluster", clusterH.ListNodes)
|
admin.GET("/cluster", clusterH.ListNodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Backup/Restore (v0.6.1) ─────────
|
// ── Backup/Restore ─────────
|
||||||
backupH := handlers.NewBackupHandler(stores, packagesDir, cfg.StoragePath)
|
backupH := handlers.NewBackupHandler(stores, packagesDir, cfg.StoragePath)
|
||||||
admin.POST("/backup", backupH.CreateBackup)
|
admin.POST("/backup", backupH.CreateBackup)
|
||||||
admin.GET("/backups", backupH.ListBackups)
|
admin.GET("/backups", backupH.ListBackups)
|
||||||
@@ -828,9 +814,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Page Routes ──────────────────────────
|
// ── Page Routes ──────────────────────────
|
||||||
// v0.27.0: Extension surface static assets (JS, CSS, images).
|
|
||||||
// Served without auth — same rationale as extension assets (script tags can't send headers).
|
// Served without auth — same rationale as extension assets (script tags can't send headers).
|
||||||
// v0.28.7: on-disk path moved to /data/packages/, URL stays /surfaces/:id/ for stability.
|
|
||||||
// In split deployment, nginx serves these from /data/packages/ directly.
|
// In split deployment, nginx serves these from /data/packages/ directly.
|
||||||
if cfg.StoragePath != "" {
|
if cfg.StoragePath != "" {
|
||||||
packagesStaticDir := cfg.StoragePath + "/packages"
|
packagesStaticDir := cfg.StoragePath + "/packages"
|
||||||
@@ -850,13 +834,12 @@ func main() {
|
|||||||
pages.SetVersion(Version)
|
pages.SetVersion(Version)
|
||||||
pageEngine := pages.New(cfg, stores)
|
pageEngine := pages.New(cfg, stores)
|
||||||
|
|
||||||
// Root redirect → configurable default surface (v0.2.1)
|
// Root redirect → configurable default surface
|
||||||
base.GET("/", pageEngine.DefaultSurfaceRedirect())
|
base.GET("/", pageEngine.DefaultSurfaceRedirect())
|
||||||
|
|
||||||
// Login page — no auth required
|
// Login page — no auth required
|
||||||
base.GET("/login", pageEngine.RenderLogin())
|
base.GET("/login", pageEngine.RenderLogin())
|
||||||
|
|
||||||
// v0.25.0: Surface routes generated from manifest registry.
|
|
||||||
// Replaces manual per-surface route blocks.
|
// Replaces manual per-surface route blocks.
|
||||||
pageEngine.RegisterPageRoutes(base, pages.PageRouteMiddleware{
|
pageEngine.RegisterPageRoutes(base, pages.PageRouteMiddleware{
|
||||||
Authenticated: middleware.AuthOrRedirect(cfg, stores.Users, userCache),
|
Authenticated: middleware.AuthOrRedirect(cfg, stores.Users, userCache),
|
||||||
@@ -864,10 +847,9 @@ func main() {
|
|||||||
middleware.AuthOrRedirect(cfg, stores.Users, userCache),
|
middleware.AuthOrRedirect(cfg, stores.Users, userCache),
|
||||||
middleware.RequireAdminPage(stores),
|
middleware.RequireAdminPage(stores),
|
||||||
},
|
},
|
||||||
Session: middleware.AuthOrRedirect(cfg, stores.Users, userCache), // TODO: session middleware for workflow visitors (v0.2.0)
|
Session: middleware.AuthOrRedirect(cfg, stores.Users, userCache), // TODO: session middleware for workflow visitors
|
||||||
})
|
})
|
||||||
|
|
||||||
// v0.29.1: Extension API routes — Starlark packages serve JSON endpoints.
|
|
||||||
// Mounted at /s/:slug/api/* with JWT auth (returns 401, not redirect).
|
// Mounted at /s/:slug/api/* with JWT auth (returns 401, not redirect).
|
||||||
{
|
{
|
||||||
extAPIH := handlers.NewExtAPIHandler(stores, starlarkRunner)
|
extAPIH := handlers.NewExtAPIHandler(stores, starlarkRunner)
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ type TicketValidator interface {
|
|||||||
// WsAuth returns a Gin middleware for the WebSocket endpoint.
|
// WsAuth returns a Gin middleware for the WebSocket endpoint.
|
||||||
// It authenticates via three methods in priority order:
|
// It authenticates via three methods in priority order:
|
||||||
//
|
//
|
||||||
// 1. ?ticket=<opaque> — single-use ticket (preferred, v0.28.8+)
|
// 1. ?ticket=<opaque> — single-use ticket (preferred)
|
||||||
// 2. ?token=<jwt> — legacy JWT in query param (deprecated)
|
// 2. ?token=<jwt> — legacy JWT in query param (deprecated)
|
||||||
// 3. Authorization header — standard Bearer JWT
|
// 3. Authorization header — standard Bearer JWT
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// RateLimiter implements per-IP rate limiting backed by a shared store.
|
// RateLimiter implements per-IP rate limiting backed by a shared store.
|
||||||
// v0.32.0: replaces in-memory token bucket for cross-pod consistency.
|
|
||||||
//
|
//
|
||||||
// Fail-open: if the store is unavailable (PG down), requests are allowed.
|
// Fail-open: if the store is unavailable (PG down), requests are allowed.
|
||||||
// Auth endpoints have their own protections (bcrypt, lockout); blocking
|
// Auth endpoints have their own protections (bcrypt, lockout); blocking
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
|
|
||||||
// RequireTeamAdmin returns middleware that restricts access to team admins.
|
// RequireTeamAdmin returns middleware that restricts access to team admins.
|
||||||
// System admins are always allowed through.
|
// System admins are always allowed through.
|
||||||
// v0.29.0: accepts TeamStore instead of using database.DB directly.
|
|
||||||
func RequireTeamAdmin(teams store.TeamStore) gin.HandlerFunc {
|
func RequireTeamAdmin(teams store.TeamStore) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
role, _ := c.Get("role")
|
role, _ := c.Get("role")
|
||||||
@@ -43,7 +42,6 @@ func RequireTeamAdmin(teams store.TeamStore) gin.HandlerFunc {
|
|||||||
// RequireTeamMember returns middleware that restricts access to users who
|
// RequireTeamMember returns middleware that restricts access to users who
|
||||||
// belong to the team identified by :teamId (any role).
|
// belong to the team identified by :teamId (any role).
|
||||||
// System admins are always allowed through.
|
// System admins are always allowed through.
|
||||||
// v0.29.0: accepts TeamStore instead of using database.DB directly.
|
|
||||||
func RequireTeamMember(teams store.TeamStore) gin.HandlerFunc {
|
func RequireTeamMember(teams store.TeamStore) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
role, _ := c.Get("role")
|
role, _ := c.Get("role")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
// ── Extension Connections (v0.38.1) ──────────────────
|
// ── Extension Connections ──────────────────
|
||||||
|
|
||||||
// ExtConnection represents a scoped credential/endpoint configuration
|
// ExtConnection represents a scoped credential/endpoint configuration
|
||||||
// for extensions that integrate with external services. Same scope
|
// for extensions that integrate with external services. Same scope
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
// ── Extension Dependencies (v0.38.2) ──────────────────
|
// ── Extension Dependencies ──────────────────
|
||||||
|
|
||||||
// ExtDependency records that a consumer package depends on a library package.
|
// ExtDependency records that a consumer package depends on a library package.
|
||||||
// Created at consumer install time from the manifest's "dependencies" map.
|
// Created at consumer install time from the manifest's "dependencies" map.
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ func IntPtr(i int) *int { return &i }
|
|||||||
func BoolPtr(b bool) *bool { return &b }
|
func BoolPtr(b bool) *bool { return &b }
|
||||||
|
|
||||||
// =========================================
|
// =========================================
|
||||||
// GROUPS (v0.16.0)
|
// GROUPS
|
||||||
// =========================================
|
// =========================================
|
||||||
|
|
||||||
// Group scope constants (reuses ScopeGlobal, ScopeTeam from above)
|
// Group scope constants (reuses ScopeGlobal, ScopeTeam from above)
|
||||||
@@ -286,7 +286,7 @@ type ResourceGrant struct {
|
|||||||
// KBChunk is a text segment from a document with its embedding vector.
|
// KBChunk is a text segment from a document with its embedding vector.
|
||||||
// KBSearchResult is a single result from similarity search.
|
// KBSearchResult is a single result from similarity search.
|
||||||
// ChannelKB represents a knowledge base linked to a channel.
|
// ChannelKB represents a knowledge base linked to a channel.
|
||||||
// PROVIDER HEALTH (v0.22.0)
|
// PROVIDER HEALTH
|
||||||
|
|
||||||
// ProviderStatus represents the derived health state.
|
// ProviderStatus represents the derived health state.
|
||||||
|
|
||||||
@@ -294,10 +294,10 @@ type ResourceGrant struct {
|
|||||||
|
|
||||||
// ErrorRate returns the error fraction, or 0 if no requests.
|
// ErrorRate returns the error fraction, or 0 if no requests.
|
||||||
|
|
||||||
// CAPABILITY OVERRIDES (v0.22.0)
|
// CAPABILITY OVERRIDES
|
||||||
|
|
||||||
// CapabilityOverride is an admin correction for a model's capabilities.
|
// CapabilityOverride is an admin correction for a model's capabilities.
|
||||||
// ROUTING POLICIES (v0.22.2)
|
// ROUTING POLICIES
|
||||||
|
|
||||||
// RoutingPolicy is one routing rule that controls how requests are
|
// RoutingPolicy is one routing rule that controls how requests are
|
||||||
// dispatched to provider configs. Stored in the routing_policies table.
|
// dispatched to provider configs. Stored in the routing_policies table.
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ const (
|
|||||||
ExtPermDBRead = "db.read"
|
ExtPermDBRead = "db.read"
|
||||||
ExtPermDBWrite = "db.write"
|
ExtPermDBWrite = "db.write"
|
||||||
ExtPermAPIHTTP = "api.http"
|
ExtPermAPIHTTP = "api.http"
|
||||||
ExtPermFormValidate = "forms.validate" // v0.29.3: form validation hooks
|
ExtPermFormValidate = "forms.validate"
|
||||||
ExtPermWorkflowAccess = "workflow.access" // v0.30.2: workflow definition + stage data access
|
ExtPermWorkflowAccess = "workflow.access"
|
||||||
ExtPermConnectionsRead = "connections.read" // v0.38.1: extension connection resolution
|
ExtPermConnectionsRead = "connections.read"
|
||||||
ExtPermTriggersRegister = "triggers.register" // v0.2.2: event/webhook trigger registration
|
ExtPermTriggersRegister = "triggers.register"
|
||||||
ExtPermRealtimePublish = "realtime.publish" // v0.5.0: publish events to realtime channels
|
ExtPermRealtimePublish = "realtime.publish"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ValidExtensionPermissions is the set of recognized permission keys.
|
// ValidExtensionPermissions is the set of recognized permission keys.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package models
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// =========================================
|
// =========================================
|
||||||
// NOTIFICATIONS (v0.20.0)
|
// NOTIFICATIONS
|
||||||
// =========================================
|
// =========================================
|
||||||
|
|
||||||
// Notification represents an in-app notification for a user.
|
// Notification represents an in-app notification for a user.
|
||||||
|
|||||||
@@ -120,22 +120,21 @@ var ValidAudiences = map[string]bool{
|
|||||||
|
|
||||||
// ── Typed Form Template ─────────────────────
|
// ── Typed Form Template ─────────────────────
|
||||||
|
|
||||||
// TypedFormTemplate is the structured form_template schema (v0.29.3).
|
// TypedFormTemplate is the structured form_template schema.
|
||||||
// v0.35.0: added Fieldsets for progressive multi-step forms.
|
|
||||||
type TypedFormTemplate struct {
|
type TypedFormTemplate struct {
|
||||||
Fields []FormField `json:"fields"`
|
Fields []FormField `json:"fields"`
|
||||||
Fieldsets []FormFieldset `json:"fieldsets,omitempty"` // v0.35.0: multi-step form groups
|
Fieldsets []FormFieldset `json:"fieldsets,omitempty"`
|
||||||
Hooks *FormHooks `json:"hooks,omitempty"`
|
Hooks *FormHooks `json:"hooks,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormFieldset groups fields into a labelled step for progressive forms (v0.35.0).
|
// FormFieldset groups fields into a labelled step for progressive forms.
|
||||||
// When fieldsets is present, top-level fields is ignored.
|
// When fieldsets is present, top-level fields is ignored.
|
||||||
type FormFieldset struct {
|
type FormFieldset struct {
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
Fields []FormField `json:"fields"`
|
Fields []FormField `json:"fields"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FieldCondition controls conditional visibility of a form field (v0.35.0).
|
// FieldCondition controls conditional visibility of a form field.
|
||||||
// When set, the field is shown/validated only if the condition is met.
|
// When set, the field is shown/validated only if the condition is met.
|
||||||
type FieldCondition struct {
|
type FieldCondition struct {
|
||||||
When string `json:"when"` // key of the field to check
|
When string `json:"when"` // key of the field to check
|
||||||
@@ -153,7 +152,7 @@ type FormField struct {
|
|||||||
Default interface{} `json:"default,omitempty"`
|
Default interface{} `json:"default,omitempty"`
|
||||||
Options []FormOption `json:"options,omitempty"`
|
Options []FormOption `json:"options,omitempty"`
|
||||||
Validation *FormValidation `json:"validation,omitempty"`
|
Validation *FormValidation `json:"validation,omitempty"`
|
||||||
Condition *FieldCondition `json:"condition,omitempty"` // v0.35.0: conditional visibility
|
Condition *FieldCondition `json:"condition,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormOption is a choice for select fields.
|
// FormOption is a choice for select fields.
|
||||||
@@ -200,7 +199,6 @@ func ParseTypedFormTemplate(raw json.RawMessage) *TypedFormTemplate {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.35.0: If fieldsets present, flatten into fields for backward compat
|
|
||||||
if len(tpl.Fieldsets) > 0 {
|
if len(tpl.Fieldsets) > 0 {
|
||||||
var allFields []FormField
|
var allFields []FormField
|
||||||
for _, fs := range tpl.Fieldsets {
|
for _, fs := range tpl.Fieldsets {
|
||||||
@@ -237,7 +235,6 @@ type FieldError struct {
|
|||||||
func ValidateFormData(tpl *TypedFormTemplate, data map[string]interface{}) []FieldError {
|
func ValidateFormData(tpl *TypedFormTemplate, data map[string]interface{}) []FieldError {
|
||||||
var errs []FieldError
|
var errs []FieldError
|
||||||
for _, f := range tpl.Fields {
|
for _, f := range tpl.Fields {
|
||||||
// v0.35.0: Skip fields whose condition is not met
|
|
||||||
if f.Condition != nil && !evaluateFieldCondition(f.Condition, data) {
|
if f.Condition != nil && !evaluateFieldCondition(f.Condition, data) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -392,7 +389,7 @@ func validateSelect(f FormField, val interface{}) []FieldError {
|
|||||||
return []FieldError{{Key: f.Key, Message: f.Label + " is not a valid option"}}
|
return []FieldError{{Key: f.Key, Message: f.Label + " is not a valid option"}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Field Condition Evaluator (v0.35.0) ─────
|
// ── Field Condition Evaluator ─────
|
||||||
|
|
||||||
// evaluateFieldCondition checks if a conditional field should be visible/validated.
|
// evaluateFieldCondition checks if a conditional field should be visible/validated.
|
||||||
func evaluateFieldCondition(cond *FieldCondition, data map[string]interface{}) bool {
|
func evaluateFieldCondition(cond *FieldCondition, data map[string]interface{}) bool {
|
||||||
@@ -505,7 +502,7 @@ type WorkflowAssignment struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Workflow Signoff (v0.3.4) ───────────────
|
// ── Workflow Signoff ───────────────
|
||||||
|
|
||||||
// WorkflowSignoff records a user's approval or rejection at a stage boundary.
|
// WorkflowSignoff records a user's approval or rejection at a stage boundary.
|
||||||
type WorkflowSignoff struct {
|
type WorkflowSignoff struct {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
// ── Role Fallback ───────────────────────────
|
// ── Role Fallback ───────────────────────────
|
||||||
// Subscribes to the existing "role.fallback" EventBus event (emitted by
|
// Subscribes to the existing "role.fallback" EventBus event (emitted by
|
||||||
// capabilities/resolver.go since v0.17.0) and creates notifications for
|
// capabilities/resolver.go) and creates notifications for
|
||||||
// all admin users.
|
// all admin users.
|
||||||
|
|
||||||
// roleFallbackPayload matches the payload emitted by the role resolver.
|
// roleFallbackPayload matches the payload emitted by the role resolver.
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ type AdminPageData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SettingsPageData is what the settings surface templates receive.
|
// SettingsPageData is what the settings surface templates receive.
|
||||||
// Feature gates (BYOK, personas) moved to sw.auth.policies in v0.37.19.
|
// Feature gates (BYOK, personas) moved to sw.auth.policies.
|
||||||
type SettingsPageData struct {
|
type SettingsPageData struct {
|
||||||
Section string `json:"section"`
|
Section string `json:"section"`
|
||||||
ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"` // v0.38.3
|
ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Loader registration ──────────────────────
|
// ── Loader registration ──────────────────────
|
||||||
@@ -39,7 +39,7 @@ type SettingsPageData struct {
|
|||||||
// TeamAdminPageData is what the team-admin surface receives.
|
// TeamAdminPageData is what the team-admin surface receives.
|
||||||
type TeamAdminPageData struct {
|
type TeamAdminPageData struct {
|
||||||
Section string `json:"section"`
|
Section string `json:"section"`
|
||||||
ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"` // v0.38.3
|
ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) registerLoaders() {
|
func (e *Engine) registerLoaders() {
|
||||||
@@ -91,7 +91,6 @@ func sectionCategory(section string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Settings loader ──────────────────────────
|
// ── Settings loader ──────────────────────────
|
||||||
// v0.22.7: Reads feature gates from GlobalConfig to control
|
|
||||||
// which nav links/tabs are visible (BYOK, User Personas).
|
// which nav links/tabs are visible (BYOK, User Personas).
|
||||||
|
|
||||||
func (e *Engine) teamAdminLoader(c *gin.Context, s store.Stores) (any, error) {
|
func (e *Engine) teamAdminLoader(c *gin.Context, s store.Stores) (any, error) {
|
||||||
@@ -117,7 +116,7 @@ func (e *Engine) settingsLoader(c *gin.Context, s store.Stores) (any, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Config section discovery (v0.38.3) ───────
|
// ── Config section discovery ───────
|
||||||
//
|
//
|
||||||
// Packages declare a config_section in their manifest. This helper scans
|
// Packages declare a config_section in their manifest. This helper scans
|
||||||
// enabled packages and returns entries whose surfaces array includes the
|
// enabled packages and returns entries whose surfaces array includes the
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ type Engine struct {
|
|||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
stores store.Stores
|
stores store.Stores
|
||||||
loaders map[string]DataLoaderFunc
|
loaders map[string]DataLoaderFunc
|
||||||
surfaces []SurfaceManifest // v0.25.0: registered surface definitions
|
surfaces []SurfaceManifest
|
||||||
devMode bool
|
devMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,25 +95,20 @@ type PageData struct {
|
|||||||
User *UserContext
|
User *UserContext
|
||||||
Data any // surface-specific data from loader
|
Data any // surface-specific data from loader
|
||||||
|
|
||||||
// v0.22.7: Theme + settings injection
|
|
||||||
Theme string // "dark", "light", or "" (= use default)
|
Theme string // "dark", "light", or "" (= use default)
|
||||||
SurfaceSettings any // JSON-serialized to window.__SETTINGS__
|
SurfaceSettings any // JSON-serialized to window.__SETTINGS__
|
||||||
|
|
||||||
// v0.25.0: Surface manifest — layout preset, components, etc.
|
|
||||||
Manifest *SurfaceManifest `json:"manifest,omitempty"`
|
Manifest *SurfaceManifest `json:"manifest,omitempty"`
|
||||||
|
|
||||||
// v0.25.0: Enabled surface IDs for conditional nav rendering.
|
|
||||||
EnabledSurfaces []string `json:"-"`
|
EnabledSurfaces []string `json:"-"`
|
||||||
|
|
||||||
// v0.27.0: Extension surfaces for sidebar nav rendering.
|
|
||||||
ExtensionSurfaces []ExtensionNavItem `json:"-"`
|
ExtensionSurfaces []ExtensionNavItem `json:"-"`
|
||||||
|
|
||||||
// v0.22.7: Login/splash page fields
|
|
||||||
InstanceName string // branding: instance display name
|
InstanceName string // branding: instance display name
|
||||||
LogoURL string // branding: custom logo URL
|
LogoURL string // branding: custom logo URL
|
||||||
Tagline string // branding: tagline under instance name
|
Tagline string // branding: tagline under instance name
|
||||||
RegistrationOpen bool // whether self-registration is enabled
|
RegistrationOpen bool // whether self-registration is enabled
|
||||||
AuthMode string // v0.24.1: "builtin", "mtls", "oidc"
|
AuthMode string // "builtin", "mtls", "oidc"
|
||||||
}
|
}
|
||||||
|
|
||||||
// SurfaceEnabled returns true if the given surface ID is in the EnabledSurfaces list.
|
// SurfaceEnabled returns true if the given surface ID is in the EnabledSurfaces list.
|
||||||
@@ -137,7 +132,6 @@ type ExtensionNavItem struct {
|
|||||||
|
|
||||||
// ConfigSectionEntry describes a package-declared config section for
|
// ConfigSectionEntry describes a package-declared config section for
|
||||||
// lazy-loading in Settings, Admin, or Team Admin surfaces.
|
// lazy-loading in Settings, Admin, or Team Admin surfaces.
|
||||||
// v0.38.3: manifest-driven discovery — no new tables or endpoints.
|
|
||||||
type ConfigSectionEntry struct {
|
type ConfigSectionEntry struct {
|
||||||
PackageID string `json:"package_id"` // section key & asset path prefix
|
PackageID string `json:"package_id"` // section key & asset path prefix
|
||||||
Label string `json:"label"` // nav link text
|
Label string `json:"label"` // nav link text
|
||||||
@@ -207,7 +201,7 @@ func New(cfg *config.Config, stores store.Stores) *Engine {
|
|||||||
e.parseTemplates()
|
e.parseTemplates()
|
||||||
e.registerLoaders()
|
e.registerLoaders()
|
||||||
e.registerCoreSurfaces()
|
e.registerCoreSurfaces()
|
||||||
e.SeedSurfaces() // v0.25.0: Write core manifests to DB (preserves admin toggle state)
|
e.SeedSurfaces()
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +272,6 @@ func (e *Engine) parseTemplates() {
|
|||||||
"toJSON": toJSON,
|
"toJSON": toJSON,
|
||||||
"eq": func(a, b string) bool { return a == b },
|
"eq": func(a, b string) bool { return a == b },
|
||||||
"contains": stringContains,
|
"contains": stringContains,
|
||||||
"roleFilterType": roleFilterType,
|
|
||||||
"esc": template.HTMLEscapeString,
|
"esc": template.HTMLEscapeString,
|
||||||
"safeHTML": func(s string) template.HTML { return template.HTML(s) },
|
"safeHTML": func(s string) template.HTML { return template.HTML(s) },
|
||||||
"join": strings.Join,
|
"join": strings.Join,
|
||||||
@@ -323,7 +316,6 @@ func (e *Engine) Render(c *gin.Context, name string, data PageData) {
|
|||||||
data.Message = e.loadMessage()
|
data.Message = e.loadMessage()
|
||||||
data.Footer = e.loadFooter()
|
data.Footer = e.loadFooter()
|
||||||
|
|
||||||
// v0.22.7: Default theme if not set
|
|
||||||
if data.Theme == "" {
|
if data.Theme == "" {
|
||||||
data.Theme = "dark"
|
data.Theme = "dark"
|
||||||
}
|
}
|
||||||
@@ -478,7 +470,6 @@ func (e *Engine) RegisterPageRoutes(base *gin.RouterGroup, mw PageRouteMiddlewar
|
|||||||
registerRoutes(group, s, handler)
|
registerRoutes(group, s, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.27.0: Extension surface catch-all — DB lookup at request time.
|
|
||||||
// No restart needed after installing new surfaces via admin API.
|
// No restart needed after installing new surfaces via admin API.
|
||||||
group.GET("/s/:slug", e.RenderExtensionSurface())
|
group.GET("/s/:slug", e.RenderExtensionSurface())
|
||||||
}
|
}
|
||||||
@@ -593,7 +584,6 @@ func stripPrefix(route, prefix string) string {
|
|||||||
// RenderLogin serves the standalone login page.
|
// RenderLogin serves the standalone login page.
|
||||||
func (e *Engine) RenderLogin() gin.HandlerFunc {
|
func (e *Engine) RenderLogin() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
// v0.22.7: Populate splash/login fields from global config
|
|
||||||
instanceName, logoURL, tagline := e.loadBranding()
|
instanceName, logoURL, tagline := e.loadBranding()
|
||||||
regOpen := e.isRegistrationOpen()
|
regOpen := e.isRegistrationOpen()
|
||||||
|
|
||||||
@@ -620,8 +610,8 @@ type WorkflowPageData struct {
|
|||||||
FormTemplateJSON string // typed form template JSON (empty if custom)
|
FormTemplateJSON string // typed form template JSON (empty if custom)
|
||||||
TotalStages int
|
TotalStages int
|
||||||
CurrentStage int
|
CurrentStage int
|
||||||
SurfacePkgID string // v0.30.2: custom package surface override (empty = use StageMode)
|
SurfacePkgID string
|
||||||
BrandingJSON string // v0.35.0: workflow branding JSON (accent_color, logo_url, tagline)
|
BrandingJSON string
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorkflowLandingPageData is passed to workflow-landing.html.
|
// WorkflowLandingPageData is passed to workflow-landing.html.
|
||||||
@@ -639,7 +629,7 @@ type WorkflowLandingPageData struct {
|
|||||||
PersonaName string
|
PersonaName string
|
||||||
PersonaIcon string
|
PersonaIcon string
|
||||||
StageCount int
|
StageCount int
|
||||||
FirstStageMode string // custom | form_only | form_chat (v0.29.3)
|
FirstStageMode string // custom | form_only | form_chat
|
||||||
ResumeURL string // non-empty if visitor has an active session
|
ResumeURL string // non-empty if visitor has an active session
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,7 +649,7 @@ func (e *Engine) RenderWorkflow() gin.HandlerFunc {
|
|||||||
// Load session display name
|
// Load session display name
|
||||||
sessionName := "Visitor"
|
sessionName := "Visitor"
|
||||||
|
|
||||||
// Load workflow stage info for form rendering (v0.29.3)
|
// Load workflow stage info for form rendering
|
||||||
var stageMode, stageName, formTplJSON, surfacePkgID, brandingJSON string
|
var stageMode, stageName, formTplJSON, surfacePkgID, brandingJSON string
|
||||||
var totalStages, currentStage int
|
var totalStages, currentStage int
|
||||||
stageMode = "custom" // default
|
stageMode = "custom" // default
|
||||||
@@ -919,18 +909,6 @@ func stringContains(haystack []string, needle string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// roleFilterType returns the model_type filter for a given role name.
|
|
||||||
func roleFilterType(role string) string {
|
|
||||||
switch role {
|
|
||||||
case "embedding":
|
|
||||||
return "embedding"
|
|
||||||
case "utility":
|
|
||||||
return "chat"
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// dict creates a map from alternating key/value pairs for template use.
|
// dict creates a map from alternating key/value pairs for template use.
|
||||||
func dict(pairs ...any) map[string]any {
|
func dict(pairs ...any) map[string]any {
|
||||||
m := make(map[string]any, len(pairs)/2)
|
m := make(map[string]any, len(pairs)/2)
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ func (e *Engine) SeedSurfaces() {
|
|||||||
}
|
}
|
||||||
log.Printf("[pages] Seeded %d core surfaces into registry", len(e.surfaces))
|
log.Printf("[pages] Seeded %d core surfaces into registry", len(e.surfaces))
|
||||||
|
|
||||||
// v0.31.0: Clean up old "editor" core surface row.
|
|
||||||
// Editor is now an installable package — the old seed row would show a
|
// Editor is now an installable package — the old seed row would show a
|
||||||
// broken nav link until the package .pkg is uploaded via admin UI.
|
// broken nav link until the package .pkg is uploaded via admin UI.
|
||||||
if sr, err := e.stores.Packages.Get(ctx, "editor"); err == nil && sr != nil && sr.Source == "extension" {
|
if sr, err := e.stores.Packages.Get(ctx, "editor"); err == nil && sr != nil && sr.Source == "extension" {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Package sandbox — connections_module.go
|
// Package sandbox — connections_module.go
|
||||||
//
|
//
|
||||||
// v0.38.1: Connections module for Starlark extensions.
|
|
||||||
// Requires permission: connections.read
|
// Requires permission: connections.read
|
||||||
//
|
//
|
||||||
// Starlark API:
|
// Starlark API:
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Package sandbox — db_module.go
|
// Package sandbox — db_module.go
|
||||||
//
|
//
|
||||||
// v0.29.2 CS1: Structured db module for Starlark extensions.
|
|
||||||
//
|
//
|
||||||
// Permissions:
|
// Permissions:
|
||||||
// db.read → db.query(), db.view(), db.list_tables()
|
// db.read → db.query(), db.view(), db.list_tables()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Package sandbox — http_module.go
|
// Package sandbox — http_module.go
|
||||||
//
|
//
|
||||||
// v0.29.1 CS0: HTTP outbound module for Starlark extensions.
|
|
||||||
// Requires permission: api.http
|
// Requires permission: api.http
|
||||||
//
|
//
|
||||||
// Starlark API:
|
// Starlark API:
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Package sandbox — lib_module.go
|
// Package sandbox — lib_module.go
|
||||||
//
|
//
|
||||||
// v0.38.2: Library loading module for Starlark extensions.
|
|
||||||
// No permission required — any starlark package can use lib.require().
|
// No permission required — any starlark package can use lib.require().
|
||||||
//
|
//
|
||||||
// Starlark API:
|
// Starlark API:
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Package sandbox — modules.go
|
// Package sandbox — modules.go
|
||||||
//
|
//
|
||||||
// v0.29.0 CS3: Module factories for Starlark sandbox.
|
|
||||||
// Each factory returns a starlarkstruct.Module that the runner
|
// Each factory returns a starlarkstruct.Module that the runner
|
||||||
// injects into the script namespace based on granted permissions.
|
// injects into the script namespace based on granted permissions.
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Package sandbox — realtime_module.go
|
// Package sandbox — realtime_module.go
|
||||||
//
|
//
|
||||||
// v0.5.0: Starlark realtime.publish() module.
|
|
||||||
//
|
//
|
||||||
// Allows extensions to publish events to WebSocket channels.
|
// Allows extensions to publish events to WebSocket channels.
|
||||||
// Events are scoped to rooms — only clients subscribed to the
|
// Events are scoped to rooms — only clients subscribed to the
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
// Package sandbox — runner.go
|
// Package sandbox — runner.go
|
||||||
//
|
//
|
||||||
// v0.29.0 CS3: Runner loads a package's Starlark script, assembles
|
|
||||||
// the module set based on granted permissions, and executes it.
|
// the module set based on granted permissions, and executes it.
|
||||||
//
|
//
|
||||||
// v0.29.1 CS0: Wires api.http permission to BuildHTTPModule with
|
|
||||||
// network_access config from package manifest.
|
// network_access config from package manifest.
|
||||||
//
|
//
|
||||||
// v0.29.1 CS2: Adds RunContext for per-invocation state (user_id),
|
|
||||||
// vault for provider key decryption, and provider.complete module.
|
// vault for provider key decryption, and provider.complete module.
|
||||||
//
|
//
|
||||||
// v0.29.2 CS1: Adds db *sql.DB for the db module. SetDB() wires it
|
|
||||||
// at startup. db.read grants query/view/list_tables; db.write adds
|
// at startup. db.read grants query/view/list_tables; db.write adds
|
||||||
// insert/update/delete. If both are granted, db.write wins (superset).
|
// insert/update/delete. If both are granted, db.write wins (superset).
|
||||||
//
|
//
|
||||||
// v0.38.2: Adds lib.load() for library packages. Libraries run with
|
|
||||||
// their own permission context. Per-invocation lib cache + cycle detection.
|
// their own permission context. Per-invocation lib cache + cycle detection.
|
||||||
//
|
//
|
||||||
// The runner is the bridge between the package/permission system
|
// The runner is the bridge between the package/permission system
|
||||||
@@ -48,7 +43,7 @@ type RunContext struct {
|
|||||||
// ChannelID is the context identifier, if any.
|
// ChannelID is the context identifier, if any.
|
||||||
ChannelID string
|
ChannelID string
|
||||||
|
|
||||||
// TeamID is the team context for settings cascade resolution (v0.2.0).
|
// TeamID is the team context for settings cascade resolution.
|
||||||
// When set, team-scoped package settings are included in the cascade.
|
// When set, team-scoped package settings are included in the cascade.
|
||||||
TeamID string
|
TeamID string
|
||||||
}
|
}
|
||||||
@@ -57,13 +52,13 @@ type RunContext struct {
|
|||||||
type Runner struct {
|
type Runner struct {
|
||||||
sandbox *Sandbox
|
sandbox *Sandbox
|
||||||
stores store.Stores
|
stores store.Stores
|
||||||
packagesDir string // v0.38.0: disk path for load() support
|
packagesDir string
|
||||||
notifier NotificationSender // nil = notifications module unavailable
|
notifier NotificationSender // nil = notifications module unavailable
|
||||||
connResolver ConnectionResolver // nil = connections module unavailable (v0.38.1)
|
connResolver ConnectionResolver // nil = connections module unavailable
|
||||||
db *sql.DB // nil = db module unavailable
|
db *sql.DB // nil = db module unavailable
|
||||||
dbPostgres bool // true = use $N placeholders; false = use ?
|
dbPostgres bool // true = use $N placeholders; false = use ?
|
||||||
allowPrivateIPs bool // v0.38.5: disable SSRF private IP check
|
allowPrivateIPs bool
|
||||||
bus *events.Bus // v0.5.0: event bus for realtime module
|
bus *events.Bus
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRunner creates a runner with the given sandbox and dependencies.
|
// NewRunner creates a runner with the given sandbox and dependencies.
|
||||||
@@ -79,7 +74,7 @@ func (r *Runner) SetNotifier(n NotificationSender) {
|
|||||||
r.notifier = n
|
r.notifier = n
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConnectionResolver attaches the connection resolver for the connections module (v0.38.1).
|
// SetConnectionResolver attaches the connection resolver for the connections module.
|
||||||
func (r *Runner) SetConnectionResolver(cr ConnectionResolver) {
|
func (r *Runner) SetConnectionResolver(cr ConnectionResolver) {
|
||||||
r.connResolver = cr
|
r.connResolver = cr
|
||||||
}
|
}
|
||||||
@@ -94,12 +89,11 @@ func (r *Runner) SetDB(db *sql.DB, isPostgres bool) {
|
|||||||
|
|
||||||
// SetPackagesDir sets the disk path where package archives are extracted.
|
// SetPackagesDir sets the disk path where package archives are extracted.
|
||||||
// Required for load() support — scripts are read from disk at runtime.
|
// Required for load() support — scripts are read from disk at runtime.
|
||||||
// v0.38.0.
|
|
||||||
func (r *Runner) SetPackagesDir(dir string) {
|
func (r *Runner) SetPackagesDir(dir string) {
|
||||||
r.packagesDir = dir
|
r.packagesDir = dir
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetBus attaches the event bus for the realtime module (v0.5.0).
|
// SetBus attaches the event bus for the realtime module.
|
||||||
func (r *Runner) SetBus(bus *events.Bus) {
|
func (r *Runner) SetBus(bus *events.Bus) {
|
||||||
r.bus = bus
|
r.bus = bus
|
||||||
}
|
}
|
||||||
@@ -114,7 +108,6 @@ func (r *Runner) SetAllowPrivateIPs(allow bool) {
|
|||||||
// ExecPackage loads a package's script from disk (primary) or manifest
|
// ExecPackage loads a package's script from disk (primary) or manifest
|
||||||
// (legacy) and executes it with modules gated by granted permissions.
|
// (legacy) and executes it with modules gated by granted permissions.
|
||||||
//
|
//
|
||||||
// v0.38.0: Disk-based loading + package-scoped load() support.
|
|
||||||
//
|
//
|
||||||
// The RunContext carries per-invocation state (user_id for provider
|
// The RunContext carries per-invocation state (user_id for provider
|
||||||
// resolution). Pass nil if no per-invocation context is needed.
|
// resolution). Pass nil if no per-invocation context is needed.
|
||||||
@@ -136,7 +129,6 @@ func (r *Runner) ExecPackage(ctx context.Context, pkg *store.PackageRegistration
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.38.2: per-invocation lib context for lib.load() caching + cycle detection
|
|
||||||
lc := newLibContext()
|
lc := newLibContext()
|
||||||
|
|
||||||
// Build modules based on granted permissions
|
// Build modules based on granted permissions
|
||||||
@@ -288,7 +280,7 @@ func (r *Runner) buildModules(ctx context.Context, packageID string, manifest ma
|
|||||||
// The manifest is passed through so modules can read their config
|
// The manifest is passed through so modules can read their config
|
||||||
// (e.g., http module reads network_access, provider reads requires_provider).
|
// (e.g., http module reads network_access, provider reads requires_provider).
|
||||||
// The RunContext carries per-invocation state for user-scoped modules.
|
// The RunContext carries per-invocation state for user-scoped modules.
|
||||||
// The libContext enables lib.load() caching and cycle detection (v0.38.2).
|
// The libContext enables lib.load() caching and cycle detection.
|
||||||
func (r *Runner) buildModulesWithLibCtx(ctx context.Context, packageID string, manifest map[string]any, rc *RunContext, lc *libContext) (map[string]starlark.Value, error) {
|
func (r *Runner) buildModulesWithLibCtx(ctx context.Context, packageID string, manifest map[string]any, rc *RunContext, lc *libContext) (map[string]starlark.Value, error) {
|
||||||
if r.stores.ExtPermissions == nil {
|
if r.stores.ExtPermissions == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -352,8 +344,7 @@ func (r *Runner) buildModulesWithLibCtx(ctx context.Context, packageID string, m
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.30.0: settings module — always injected, no permission required.
|
// A package reads its own admin + team + user settings.
|
||||||
// A package reads its own admin + team + user settings (v0.2.0 cascade).
|
|
||||||
userID := ""
|
userID := ""
|
||||||
teamID := ""
|
teamID := ""
|
||||||
if rc != nil {
|
if rc != nil {
|
||||||
@@ -362,7 +353,6 @@ func (r *Runner) buildModulesWithLibCtx(ctx context.Context, packageID string, m
|
|||||||
}
|
}
|
||||||
modules["settings"] = BuildSettingsModule(ctx, r.stores, packageID, userID, teamID)
|
modules["settings"] = BuildSettingsModule(ctx, r.stores, packageID, userID, teamID)
|
||||||
|
|
||||||
// v0.38.2: lib module — always injected, no permission required.
|
|
||||||
// Allows any starlark package to load declared library dependencies.
|
// Allows any starlark package to load declared library dependencies.
|
||||||
if lc != nil {
|
if lc != nil {
|
||||||
modules["lib"] = BuildLibModule(ctx, r, packageID, rc, lc)
|
modules["lib"] = BuildLibModule(ctx, r, packageID, rc, lc)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Package sandbox — sandboxed Starlark interpreter.
|
// Package sandbox — sandboxed Starlark interpreter.
|
||||||
//
|
//
|
||||||
// v0.29.0 CS1: Provides execution with timeout, step limits,
|
|
||||||
// captured output, and injectable predeclared modules.
|
// captured output, and injectable predeclared modules.
|
||||||
//
|
//
|
||||||
// The sandbox is intentionally restrictive:
|
// The sandbox is intentionally restrictive:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
// settings_module.go — v0.2.0
|
// settings_module.go
|
||||||
//
|
//
|
||||||
// The settings module lets extensions read their resolved settings via
|
// The settings module lets extensions read their resolved settings via
|
||||||
// the three-tier cascade: global → team → user, respecting the
|
// the three-tier cascade: global → team → user, respecting the
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Package sandbox — unicode_scan.go
|
// Package sandbox — unicode_scan.go
|
||||||
//
|
//
|
||||||
// v0.5.1: Invisible Unicode scanning gate.
|
|
||||||
//
|
//
|
||||||
// Defends against GlassWorm (variation selector payloads), Trojan Source
|
// Defends against GlassWorm (variation selector payloads), Trojan Source
|
||||||
// (bidi override attacks / CVE-2021-42574), and other invisible Unicode
|
// (bidi override attacks / CVE-2021-42574), and other invisible Unicode
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
// workflow_module.go — v0.30.2 CS1, v0.3.2 instance API
|
// workflow_module.go
|
||||||
//
|
//
|
||||||
// The workflow module lets extensions read workflow definitions,
|
// The workflow module lets extensions read workflow definitions,
|
||||||
// manage instances, and programmatically advance workflow stages.
|
// manage instances, and programmatically advance workflow stages.
|
||||||
@@ -27,7 +27,6 @@ import (
|
|||||||
// BuildWorkflowModule creates the "workflow" Starlark module for a package.
|
// BuildWorkflowModule creates the "workflow" Starlark module for a package.
|
||||||
// Requires the workflow.access permission.
|
// Requires the workflow.access permission.
|
||||||
//
|
//
|
||||||
// v0.3.2: added get_instance and list_instances for read-only instance access.
|
|
||||||
// Mutating operations (start/advance/cancel) are available via HTTP API;
|
// Mutating operations (start/advance/cancel) are available via HTTP API;
|
||||||
// Starlark builtins for them will be added when the engine interface is
|
// Starlark builtins for them will be added when the engine interface is
|
||||||
// extracted to a shared package to avoid circular imports.
|
// extracted to a shared package to avoid circular imports.
|
||||||
@@ -87,7 +86,7 @@ func workflowGetDef(ctx context.Context, stores store.Stores) func(*starlark.Thr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Instance Read API (v0.3.2) ──────────────
|
// ── Instance Read API ──────────────
|
||||||
|
|
||||||
func workflowGetInstance(ctx context.Context, stores store.Stores) func(*starlark.Thread, *starlark.Builtin, starlark.Tuple, []starlark.Tuple) (starlark.Value, error) {
|
func workflowGetInstance(ctx context.Context, stores store.Stores) func(*starlark.Thread, *starlark.Builtin, starlark.Tuple, []starlark.Tuple) (starlark.Value, error) {
|
||||||
return func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
return func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||||
@@ -187,5 +186,5 @@ func goValToStarlark(v interface{}) starlark.Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// workflowRoute routes the workflow to a named stage (v0.35.0).
|
// workflowRoute routes the workflow to a named stage.
|
||||||
// Starlark: workflow.route(channel_id, target_stage, reason)
|
// Starlark: workflow.route(channel_id, target_stage, reason)
|
||||||
|
|||||||
@@ -42,19 +42,19 @@ type Stores struct {
|
|||||||
ResourceGrants ResourceGrantStore
|
ResourceGrants ResourceGrantStore
|
||||||
Notifications NotificationStore
|
Notifications NotificationStore
|
||||||
NotifPrefs NotificationPreferenceStore
|
NotifPrefs NotificationPreferenceStore
|
||||||
Presence PresenceStore // v0.29.0: User online/offline status
|
Presence PresenceStore
|
||||||
Health HealthStore // v0.29.0: Provider health window management
|
Health HealthStore
|
||||||
Connections ConnectionStore // v0.38.1: Extension connection credentials
|
Connections ConnectionStore
|
||||||
Dependencies DependencyStore // v0.38.2: Library package dependency graph
|
Dependencies DependencyStore
|
||||||
Packages PackageStore // v0.28.7: Unified package registry (surfaces + extensions)
|
Packages PackageStore
|
||||||
Workflows WorkflowStore // v0.26.1: Workflow definitions + stages
|
Workflows WorkflowStore
|
||||||
ExtPermissions ExtensionPermissionStore // v0.29.0: Extension declared/granted capabilities
|
ExtPermissions ExtensionPermissionStore
|
||||||
ExtData ExtDataStore // v0.29.2: Extension namespaced table catalog
|
ExtData ExtDataStore
|
||||||
Tickets TicketStore // v0.32.0: WS auth tickets (PG-backed for cross-pod)
|
Tickets TicketStore
|
||||||
RateLimits RateLimitStore // v0.32.0: Distributed rate limiting
|
RateLimits RateLimitStore
|
||||||
Triggers TriggerStore // v0.2.2: Extension event/webhook triggers
|
Triggers TriggerStore
|
||||||
ScheduledTasks ScheduledTaskStore // v0.2.2: User-created cron tasks
|
ScheduledTasks ScheduledTaskStore
|
||||||
Cluster ClusterStore // v0.6.0: PG-backed cluster node registry (nil on SQLite)
|
Cluster ClusterStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// TeamAvailableModel is returned by CatalogStore.ListTeamAvailable.
|
// TeamAvailableModel is returned by CatalogStore.ListTeamAvailable.
|
||||||
@@ -85,7 +85,7 @@ type UserStore interface {
|
|||||||
RevokeAllRefreshTokens(ctx context.Context, userID string) error
|
RevokeAllRefreshTokens(ctx context.Context, userID string) error
|
||||||
CleanExpiredTokens(ctx context.Context) error
|
CleanExpiredTokens(ctx context.Context) error
|
||||||
|
|
||||||
// ── CS1 additions (v0.29.0) ──
|
// ── CS1 additions ──
|
||||||
|
|
||||||
// Exists returns true if a user with the given ID exists and is active.
|
// Exists returns true if a user with the given ID exists and is active.
|
||||||
Exists(ctx context.Context, userID string) (bool, error)
|
Exists(ctx context.Context, userID string) (bool, error)
|
||||||
@@ -94,7 +94,7 @@ type UserStore interface {
|
|||||||
// Excludes the calling user. Max 20 results.
|
// Excludes the calling user. Max 20 results.
|
||||||
SearchActive(ctx context.Context, excludeUserID, query string) ([]UserSearchResult, error)
|
SearchActive(ctx context.Context, excludeUserID, query string) ([]UserSearchResult, error)
|
||||||
|
|
||||||
// ── CS2 additions (v0.29.0) ──
|
// ── CS2 additions ──
|
||||||
|
|
||||||
// CountAll returns the total number of users.
|
// CountAll returns the total number of users.
|
||||||
CountAll(ctx context.Context) (int, error)
|
CountAll(ctx context.Context) (int, error)
|
||||||
@@ -108,7 +108,7 @@ type UserStore interface {
|
|||||||
// UpdateVaultKeys re-wraps the user's vault keys (password change).
|
// UpdateVaultKeys re-wraps the user's vault keys (password change).
|
||||||
UpdateVaultKeys(ctx context.Context, userID string, encUEK, salt, nonce []byte) error
|
UpdateVaultKeys(ctx context.Context, userID string, encUEK, salt, nonce []byte) error
|
||||||
|
|
||||||
// ── CS4 additions (v0.29.0) ──
|
// ── CS4 additions ──
|
||||||
|
|
||||||
// ClearVaultKeys nulls out vault columns and sets vault_set = false.
|
// ClearVaultKeys nulls out vault columns and sets vault_set = false.
|
||||||
ClearVaultKeys(ctx context.Context, userID string) error
|
ClearVaultKeys(ctx context.Context, userID string) error
|
||||||
@@ -147,7 +147,7 @@ type TeamStore interface {
|
|||||||
IsTeamAdmin(ctx context.Context, teamID, userID string) (bool, error)
|
IsTeamAdmin(ctx context.Context, teamID, userID string) (bool, error)
|
||||||
IsMember(ctx context.Context, teamID, userID string) (bool, error)
|
IsMember(ctx context.Context, teamID, userID string) (bool, error)
|
||||||
|
|
||||||
// ── CS1 additions (v0.29.0) ──
|
// ── CS1 additions ──
|
||||||
|
|
||||||
// Exists returns true if a team with the given ID exists.
|
// Exists returns true if a team with the given ID exists.
|
||||||
Exists(ctx context.Context, teamID string) (bool, error)
|
Exists(ctx context.Context, teamID string) (bool, error)
|
||||||
@@ -161,12 +161,12 @@ type TeamStore interface {
|
|||||||
// ListTeamAuditActions returns distinct audit actions for a team's members.
|
// ListTeamAuditActions returns distinct audit actions for a team's members.
|
||||||
ListTeamAuditActions(ctx context.Context, teamID string) ([]string, error)
|
ListTeamAuditActions(ctx context.Context, teamID string) ([]string, error)
|
||||||
|
|
||||||
// ── CS5b additions (v0.29.0) ──
|
// ── CS5b additions ──
|
||||||
|
|
||||||
// GetFirstTeamIDForUser returns the first team_id the user belongs to, or "" if none.
|
// GetFirstTeamIDForUser returns the first team_id the user belongs to, or "" if none.
|
||||||
GetFirstTeamIDForUser(ctx context.Context, userID string) (string, error)
|
GetFirstTeamIDForUser(ctx context.Context, userID string) (string, error)
|
||||||
|
|
||||||
// ── CS6 additions (v0.29.0) ──
|
// ── CS6 additions ──
|
||||||
|
|
||||||
// AddMemberReturningID inserts a team member and returns the row ID.
|
// AddMemberReturningID inserts a team member and returns the row ID.
|
||||||
AddMemberReturningID(ctx context.Context, teamID, userID, role string) (string, error)
|
AddMemberReturningID(ctx context.Context, teamID, userID, role string) (string, error)
|
||||||
@@ -175,11 +175,6 @@ type TeamStore interface {
|
|||||||
MergeSettings(ctx context.Context, teamID, settingsJSON string) error
|
MergeSettings(ctx context.Context, teamID, settingsJSON string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChannelListFilter holds filter options for ChannelStore.ListFiltered.
|
|
||||||
// ChannelListItem is returned by ListFiltered and GetForUser.
|
|
||||||
// ArchivedChannel is a view model for the admin archived channels list.
|
|
||||||
// MessageWithSender is returned by MessageStore.ListWithSenderInfo.
|
|
||||||
// ChannelSearchResult is returned by MessageStore.SearchInChannel.
|
|
||||||
// =========================================
|
// =========================================
|
||||||
// AUDIT STORE
|
// AUDIT STORE
|
||||||
// =========================================
|
// =========================================
|
||||||
@@ -212,7 +207,7 @@ type GlobalConfigStore interface {
|
|||||||
Set(ctx context.Context, key string, value models.JSONMap, updatedBy string) error
|
Set(ctx context.Context, key string, value models.JSONMap, updatedBy string) error
|
||||||
GetAll(ctx context.Context) (map[string]models.JSONMap, error)
|
GetAll(ctx context.Context) (map[string]models.JSONMap, error)
|
||||||
|
|
||||||
// ── OIDC state (v0.29.0-cs4) ──
|
// ── OIDC state ──
|
||||||
|
|
||||||
// SaveOIDCState stores a state+nonce pair for OIDC callback verification.
|
// SaveOIDCState stores a state+nonce pair for OIDC callback verification.
|
||||||
SaveOIDCState(ctx context.Context, state, nonce, redirectTo string) error
|
SaveOIDCState(ctx context.Context, state, nonce, redirectTo string) error
|
||||||
@@ -224,7 +219,7 @@ type GlobalConfigStore interface {
|
|||||||
// CleanupOIDCState removes stale OIDC states older than 10 minutes.
|
// CleanupOIDCState removes stale OIDC states older than 10 minutes.
|
||||||
CleanupOIDCState(ctx context.Context) error
|
CleanupOIDCState(ctx context.Context) error
|
||||||
|
|
||||||
// ── CS6 additions (v0.29.0) ──
|
// ── CS6 additions ──
|
||||||
|
|
||||||
// GetString returns the raw value column as a string for simple settings
|
// GetString returns the raw value column as a string for simple settings
|
||||||
// (e.g. bare JSON booleans like "true"/"false").
|
// (e.g. bare JSON booleans like "true"/"false").
|
||||||
@@ -232,7 +227,7 @@ type GlobalConfigStore interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================================
|
// =========================================
|
||||||
// GROUP STORE (v0.16.0)
|
// GROUP STORE
|
||||||
// =========================================
|
// =========================================
|
||||||
|
|
||||||
type GroupStore interface {
|
type GroupStore interface {
|
||||||
@@ -257,7 +252,7 @@ type GroupStore interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================================
|
// =========================================
|
||||||
// RESOURCE GRANT STORE (v0.16.0)
|
// RESOURCE GRANT STORE
|
||||||
// =========================================
|
// =========================================
|
||||||
|
|
||||||
type ResourceGrantStore interface {
|
type ResourceGrantStore interface {
|
||||||
@@ -270,7 +265,7 @@ type ResourceGrantStore interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================================
|
// =========================================
|
||||||
// NOTIFICATION STORE (v0.20.0)
|
// NOTIFICATION STORE
|
||||||
// =========================================
|
// =========================================
|
||||||
|
|
||||||
type NotificationStore interface {
|
type NotificationStore interface {
|
||||||
@@ -284,7 +279,7 @@ type NotificationStore interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================================
|
// =========================================
|
||||||
// NOTIFICATION PREFERENCES STORE (v0.20.0)
|
// NOTIFICATION PREFERENCES STORE
|
||||||
// =========================================
|
// =========================================
|
||||||
|
|
||||||
type NotificationPreferenceStore interface {
|
type NotificationPreferenceStore interface {
|
||||||
@@ -299,12 +294,12 @@ type NotificationPreferenceStore interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================================
|
// =========================================
|
||||||
// SESSION STORE (v0.24.3)
|
// SESSION STORE
|
||||||
// =========================================
|
// =========================================
|
||||||
|
|
||||||
|
|
||||||
// =========================================
|
// =========================================
|
||||||
// HEALTH STORE (v0.29.0)
|
// HEALTH STORE
|
||||||
// =========================================
|
// =========================================
|
||||||
|
|
||||||
// HealthStore manages provider health windows. The full interface is used
|
// HealthStore manages provider health windows. The full interface is used
|
||||||
@@ -315,7 +310,7 @@ type HealthStore interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================================
|
// =========================================
|
||||||
// PRESENCE STORE (v0.29.0)
|
// PRESENCE STORE
|
||||||
// =========================================
|
// =========================================
|
||||||
|
|
||||||
// PresenceStore manages user online/offline status.
|
// PresenceStore manages user online/offline status.
|
||||||
@@ -334,7 +329,7 @@ type PresenceStore interface {
|
|||||||
// =========================================
|
// =========================================
|
||||||
// SHARED TYPES
|
// SHARED TYPES
|
||||||
// =========================================
|
// =========================================
|
||||||
// CONNECTION STORE (v0.38.1)
|
// CONNECTION STORE
|
||||||
// =========================================
|
// =========================================
|
||||||
|
|
||||||
type ConnectionStore interface {
|
type ConnectionStore interface {
|
||||||
@@ -367,7 +362,7 @@ type ConnectionStore interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================================
|
// =========================================
|
||||||
// DEPENDENCY STORE (v0.38.2)
|
// DEPENDENCY STORE
|
||||||
// =========================================
|
// =========================================
|
||||||
|
|
||||||
type DependencyStore interface {
|
type DependencyStore interface {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// PackageStore manages the unified package registry (surfaces + extensions).
|
// PackageStore manages the unified package registry (surfaces + extensions).
|
||||||
// Replaces SurfaceRegistryStore and ExtensionStore (v0.28.7).
|
// Replaces SurfaceRegistryStore and ExtensionStore.
|
||||||
type PackageStore interface {
|
type PackageStore interface {
|
||||||
// ── Lifecycle (from SurfaceRegistryStore) ────────────
|
// ── Lifecycle (from SurfaceRegistryStore) ────────────
|
||||||
|
|
||||||
@@ -66,13 +66,13 @@ type PackageStore interface {
|
|||||||
// DeleteUserSettings removes per-user settings, reverting to defaults.
|
// DeleteUserSettings removes per-user settings, reverting to defaults.
|
||||||
DeleteUserSettings(ctx context.Context, pkgID, userID string) error
|
DeleteUserSettings(ctx context.Context, pkgID, userID string) error
|
||||||
|
|
||||||
// ── Scoped visibility (v0.30.0) ────────────────
|
// ── Scoped visibility ────────────────
|
||||||
|
|
||||||
// ListVisiblePackages returns packages visible to the given user:
|
// ListVisiblePackages returns packages visible to the given user:
|
||||||
// global packages + team packages for user's teams + personal packages.
|
// global packages + team packages for user's teams + personal packages.
|
||||||
ListVisiblePackages(ctx context.Context, userID string) ([]PackageRegistration, error)
|
ListVisiblePackages(ctx context.Context, userID string) ([]PackageRegistration, error)
|
||||||
|
|
||||||
// ── Package lifecycle (v0.30.0) ─────────────────
|
// ── Package lifecycle ─────────────────
|
||||||
|
|
||||||
// SetSchemaVersion updates the current schema version for a package.
|
// SetSchemaVersion updates the current schema version for a package.
|
||||||
SetSchemaVersion(ctx context.Context, id string, version int) error
|
SetSchemaVersion(ctx context.Context, id string, version int) error
|
||||||
@@ -83,7 +83,7 @@ type PackageStore interface {
|
|||||||
// SetPackageSettings stores admin-configured package-level settings.
|
// SetPackageSettings stores admin-configured package-level settings.
|
||||||
SetPackageSettings(ctx context.Context, id string, settings json.RawMessage) error
|
SetPackageSettings(ctx context.Context, id string, settings json.RawMessage) error
|
||||||
|
|
||||||
// ── Team-level settings (v0.2.0) ─────────────────
|
// ── Team-level settings ─────────────────
|
||||||
|
|
||||||
// GetTeamSettings returns team-scoped overrides for a package.
|
// GetTeamSettings returns team-scoped overrides for a package.
|
||||||
GetTeamSettings(ctx context.Context, pkgID, teamID string) (json.RawMessage, error)
|
GetTeamSettings(ctx context.Context, pkgID, teamID string) (json.RawMessage, error)
|
||||||
@@ -134,7 +134,7 @@ type PackageUserSettings struct {
|
|||||||
IsEnabled bool `json:"is_enabled" db:"is_enabled"`
|
IsEnabled bool `json:"is_enabled" db:"is_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PackageTeamSettings stores team-scoped overrides for a package (v0.2.0).
|
// PackageTeamSettings stores team-scoped overrides for a package.
|
||||||
type PackageTeamSettings struct {
|
type PackageTeamSettings struct {
|
||||||
PackageID string `json:"package_id" db:"package_id"`
|
PackageID string `json:"package_id" db:"package_id"`
|
||||||
TeamID string `json:"team_id" db:"team_id"`
|
TeamID string `json:"team_id" db:"team_id"`
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"switchboard-core/models"
|
"switchboard-core/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── ConnectionStore (v0.38.1) ────────────────────────
|
// ── ConnectionStore ────────────────────────
|
||||||
|
|
||||||
type ConnectionStore struct{}
|
type ConnectionStore struct{}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"switchboard-core/models"
|
"switchboard-core/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── DependencyStore (v0.38.2) ────────────────────────
|
// ── DependencyStore ────────────────────────
|
||||||
|
|
||||||
type DependencyStore struct{}
|
type DependencyStore struct{}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func (s *GlobalConfigStore) GetAll(ctx context.Context) (map[string]models.JSONM
|
|||||||
return result, rows.Err()
|
return result, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── OIDC state (v0.29.0-cs4) ────────────────────────────────────────────
|
// ── OIDC state ────────────────────────────────────────────
|
||||||
|
|
||||||
func (s *GlobalConfigStore) SaveOIDCState(ctx context.Context, state, nonce, redirectTo string) error {
|
func (s *GlobalConfigStore) SaveOIDCState(ctx context.Context, state, nonce, redirectTo string) error {
|
||||||
_, err := DB.ExecContext(ctx, `
|
_, err := DB.ExecContext(ctx, `
|
||||||
@@ -86,7 +86,7 @@ func (s *GlobalConfigStore) CleanupOIDCState(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CS6 additions (v0.29.0) ─────────────────────────────────────────────
|
// ── CS6 additions ─────────────────────────────────────────────
|
||||||
|
|
||||||
func (s *GlobalConfigStore) GetString(ctx context.Context, key string) (string, error) {
|
func (s *GlobalConfigStore) GetString(ctx context.Context, key string) (string, error) {
|
||||||
var val string
|
var val string
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ func (s *PackageStore) scanMany(ctx context.Context, query string, args ...inter
|
|||||||
return result, rows.Err()
|
return result, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Scoped visibility (v0.30.0) ──────────────────
|
// ── Scoped visibility ──────────────────
|
||||||
|
|
||||||
func (s *PackageStore) ListVisiblePackages(ctx context.Context, userID string) ([]store.PackageRegistration, error) {
|
func (s *PackageStore) ListVisiblePackages(ctx context.Context, userID string) ([]store.PackageRegistration, error) {
|
||||||
return s.scanMany(ctx, `
|
return s.scanMany(ctx, `
|
||||||
@@ -327,7 +327,7 @@ func (s *PackageStore) ListVisiblePackages(ctx context.Context, userID string) (
|
|||||||
ORDER BY p.source, p.title`, userID)
|
ORDER BY p.source, p.title`, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Package lifecycle (v0.30.0) ──────────────────
|
// ── Package lifecycle ──────────────────
|
||||||
|
|
||||||
func (s *PackageStore) SetSchemaVersion(ctx context.Context, id string, version int) error {
|
func (s *PackageStore) SetSchemaVersion(ctx context.Context, id string, version int) error {
|
||||||
result, err := DB.ExecContext(ctx,
|
result, err := DB.ExecContext(ctx,
|
||||||
@@ -367,7 +367,7 @@ func (s *PackageStore) SetPackageSettings(ctx context.Context, id string, settin
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Team-level settings (v0.2.0) ─────────────────
|
// ── Team-level settings ─────────────────
|
||||||
|
|
||||||
func (s *PackageStore) GetTeamSettings(ctx context.Context, pkgID, teamID string) (json.RawMessage, error) {
|
func (s *PackageStore) GetTeamSettings(ctx context.Context, pkgID, teamID string) (json.RawMessage, error) {
|
||||||
var settings []byte
|
var settings []byte
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// RateLimitStore manages distributed rate limit counters in Postgres.
|
// RateLimitStore manages distributed rate limit counters in Postgres.
|
||||||
// v0.32.0: fixed-window counter — upsert per-second bucket, check burst.
|
|
||||||
type RateLimitStore struct{}
|
type RateLimitStore struct{}
|
||||||
|
|
||||||
func NewRateLimitStore() *RateLimitStore { return &RateLimitStore{} }
|
func NewRateLimitStore() *RateLimitStore { return &RateLimitStore{} }
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ func (s *TeamStore) IsMember(ctx context.Context, teamID, userID string) (bool,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── CS1 additions (v0.29.0) ─────────────────────────────────────────────
|
// ── CS1 additions ─────────────────────────────────────────────
|
||||||
|
|
||||||
func (s *TeamStore) Exists(ctx context.Context, teamID string) (bool, error) {
|
func (s *TeamStore) Exists(ctx context.Context, teamID string) (bool, error) {
|
||||||
var exists bool
|
var exists bool
|
||||||
@@ -279,7 +279,7 @@ func (s *TeamStore) ListTeamAuditActions(ctx context.Context, teamID string) ([]
|
|||||||
return actions, rows.Err()
|
return actions, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CS5b additions (v0.29.0) ────────────────────────────────────────────
|
// ── CS5b additions ────────────────────────────────────────────
|
||||||
|
|
||||||
func (s *TeamStore) GetFirstTeamIDForUser(ctx context.Context, userID string) (string, error) {
|
func (s *TeamStore) GetFirstTeamIDForUser(ctx context.Context, userID string) (string, error) {
|
||||||
var teamID string
|
var teamID string
|
||||||
@@ -291,7 +291,7 @@ func (s *TeamStore) GetFirstTeamIDForUser(ctx context.Context, userID string) (s
|
|||||||
return teamID, nil
|
return teamID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CS6 additions (v0.29.0) ────────────────────────────────────────────
|
// ── CS6 additions ────────────────────────────────────────────
|
||||||
|
|
||||||
func (s *TeamStore) AddMemberReturningID(ctx context.Context, teamID, userID, role string) (string, error) {
|
func (s *TeamStore) AddMemberReturningID(ctx context.Context, teamID, userID, role string) (string, error) {
|
||||||
var id string
|
var id string
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// TicketStore manages WS auth tickets in Postgres.
|
// TicketStore manages WS auth tickets in Postgres.
|
||||||
// v0.32.0: replaces in-memory sync.Map for cross-pod validation.
|
|
||||||
type TicketStore struct{}
|
type TicketStore struct{}
|
||||||
|
|
||||||
func NewTicketStore() *TicketStore { return &TicketStore{} }
|
func NewTicketStore() *TicketStore { return &TicketStore{} }
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ func scanOneUser(ctx context.Context, query string, args ...interface{}) (*model
|
|||||||
ScanJSON(sj, &u.Settings)
|
ScanJSON(sj, &u.Settings)
|
||||||
return &u, nil
|
return &u, nil
|
||||||
}
|
}
|
||||||
// ── CS1 additions (v0.29.0) ─────────────────────────────────────────────
|
// ── CS1 additions ─────────────────────────────────────────────
|
||||||
|
|
||||||
func (s *UserStore) Exists(ctx context.Context, userID string) (bool, error) {
|
func (s *UserStore) Exists(ctx context.Context, userID string) (bool, error) {
|
||||||
var exists bool
|
var exists bool
|
||||||
@@ -231,7 +231,7 @@ func (s *UserStore) SearchActive(ctx context.Context, excludeUserID, query strin
|
|||||||
return results, rows.Err()
|
return results, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CS2 additions (v0.29.0) ─────────────────────────────────────────────
|
// ── CS2 additions ─────────────────────────────────────────────
|
||||||
|
|
||||||
func (s *UserStore) MergeSettings(ctx context.Context, userID string, patch []byte) error {
|
func (s *UserStore) MergeSettings(ctx context.Context, userID string, patch []byte) error {
|
||||||
_, err := DB.ExecContext(ctx, `
|
_, err := DB.ExecContext(ctx, `
|
||||||
@@ -268,7 +268,7 @@ func (s *UserStore) CountAll(ctx context.Context) (int, error) {
|
|||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CS4 additions (v0.29.0) ─────────────────────────────────────────────
|
// ── CS4 additions ─────────────────────────────────────────────
|
||||||
|
|
||||||
func (s *UserStore) ClearVaultKeys(ctx context.Context, userID string) error {
|
func (s *UserStore) ClearVaultKeys(ctx context.Context, userID string) error {
|
||||||
_, err := DB.ExecContext(ctx, `
|
_, err := DB.ExecContext(ctx, `
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ func nullIfEmpty(s string) interface{} {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Instances (v0.3.1) ──────────────────────
|
// ── Instances ──────────────────────
|
||||||
|
|
||||||
func (s *WorkflowStore) CreateInstance(ctx context.Context, inst *models.WorkflowInstance) error {
|
func (s *WorkflowStore) CreateInstance(ctx context.Context, inst *models.WorkflowInstance) error {
|
||||||
stageData := jsonOrEmpty(inst.StageData)
|
stageData := jsonOrEmpty(inst.StageData)
|
||||||
@@ -573,7 +573,7 @@ func (s *WorkflowStore) ListActiveInstances(ctx context.Context) ([]models.Workf
|
|||||||
return result, rows.Err()
|
return result, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Assignments (v0.3.1) ────────────────────
|
// ── Assignments ────────────────────
|
||||||
|
|
||||||
func (s *WorkflowStore) CreateAssignment(ctx context.Context, a *models.WorkflowAssignment) error {
|
func (s *WorkflowStore) CreateAssignment(ctx context.Context, a *models.WorkflowAssignment) error {
|
||||||
reviewData := jsonOrEmpty(a.ReviewData)
|
reviewData := jsonOrEmpty(a.ReviewData)
|
||||||
@@ -661,7 +661,7 @@ func (s *WorkflowStore) ListAssignmentsByUser(ctx context.Context, userID string
|
|||||||
return s.queryAssignments(ctx, q, args...)
|
return s.queryAssignments(ctx, q, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Signoffs (v0.3.4) ─────────────────────────
|
// ── Signoffs ─────────────────────────
|
||||||
|
|
||||||
func (s *WorkflowStore) CreateSignoff(ctx context.Context, so *models.WorkflowSignoff) error {
|
func (s *WorkflowStore) CreateSignoff(ctx context.Context, so *models.WorkflowSignoff) error {
|
||||||
return DB.QueryRowContext(ctx, `
|
return DB.QueryRowContext(ctx, `
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// RateLimitStore manages distributed rate limit counters.
|
// RateLimitStore manages distributed rate limit counters.
|
||||||
// v0.32.0: replaces in-memory token bucket for cross-pod rate limiting.
|
|
||||||
type RateLimitStore interface {
|
type RateLimitStore interface {
|
||||||
// Allow checks if a request is within the rate limit.
|
// Allow checks if a request is within the rate limit.
|
||||||
// key is "{scope}:{identifier}" e.g. "auth:192.168.1.1".
|
// key is "{scope}:{identifier}" e.g. "auth:192.168.1.1".
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package store
|
package store
|
||||||
|
|
||||||
// settings_cascade.go — v0.2.0
|
// settings_cascade.go
|
||||||
//
|
//
|
||||||
// Pure functions for three-tier settings resolution:
|
// Pure functions for three-tier settings resolution:
|
||||||
// global (admin) → team → user
|
// global (admin) → team → user
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"switchboard-core/store"
|
"switchboard-core/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── ConnectionStore (v0.38.1) ────────────────────────
|
// ── ConnectionStore ────────────────────────
|
||||||
|
|
||||||
type ConnectionStore struct{}
|
type ConnectionStore struct{}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"switchboard-core/models"
|
"switchboard-core/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── DependencyStore (v0.38.2) ────────────────────────
|
// ── DependencyStore ────────────────────────
|
||||||
|
|
||||||
type DependencyStore struct{}
|
type DependencyStore struct{}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func (s *GlobalConfigStore) GetAll(ctx context.Context) (map[string]models.JSONM
|
|||||||
return result, rows.Err()
|
return result, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── OIDC state (v0.29.0-cs4) ────────────────────────────────────────────
|
// ── OIDC state ────────────────────────────────────────────
|
||||||
|
|
||||||
func (s *GlobalConfigStore) SaveOIDCState(ctx context.Context, state, nonce, redirectTo string) error {
|
func (s *GlobalConfigStore) SaveOIDCState(ctx context.Context, state, nonce, redirectTo string) error {
|
||||||
_, err := DB.ExecContext(ctx, `
|
_, err := DB.ExecContext(ctx, `
|
||||||
@@ -84,7 +84,7 @@ func (s *GlobalConfigStore) CleanupOIDCState(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CS6 additions (v0.29.0) ─────────────────────────────────────────────
|
// ── CS6 additions ─────────────────────────────────────────────
|
||||||
|
|
||||||
func (s *GlobalConfigStore) GetString(ctx context.Context, key string) (string, error) {
|
func (s *GlobalConfigStore) GetString(ctx context.Context, key string) (string, error) {
|
||||||
var val string
|
var val string
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ func (s *PackageStore) scanMany(ctx context.Context, query string, args ...inter
|
|||||||
return result, rows.Err()
|
return result, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Scoped visibility (v0.30.0) ──────────────────
|
// ── Scoped visibility ──────────────────
|
||||||
|
|
||||||
func (s *PackageStore) ListVisiblePackages(ctx context.Context, userID string) ([]store.PackageRegistration, error) {
|
func (s *PackageStore) ListVisiblePackages(ctx context.Context, userID string) ([]store.PackageRegistration, error) {
|
||||||
return s.scanMany(ctx, `
|
return s.scanMany(ctx, `
|
||||||
@@ -348,7 +348,7 @@ func (s *PackageStore) ListVisiblePackages(ctx context.Context, userID string) (
|
|||||||
ORDER BY p.source, p.title`, userID, userID)
|
ORDER BY p.source, p.title`, userID, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Package lifecycle (v0.30.0) ──────────────────
|
// ── Package lifecycle ──────────────────
|
||||||
|
|
||||||
func (s *PackageStore) SetSchemaVersion(ctx context.Context, id string, version int) error {
|
func (s *PackageStore) SetSchemaVersion(ctx context.Context, id string, version int) error {
|
||||||
result, err := DB.ExecContext(ctx,
|
result, err := DB.ExecContext(ctx,
|
||||||
@@ -388,7 +388,7 @@ func (s *PackageStore) SetPackageSettings(ctx context.Context, id string, settin
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Team-level settings (v0.2.0) ─────────────────
|
// ── Team-level settings ─────────────────
|
||||||
|
|
||||||
func (s *PackageStore) GetTeamSettings(ctx context.Context, pkgID, teamID string) (json.RawMessage, error) {
|
func (s *PackageStore) GetTeamSettings(ctx context.Context, pkgID, teamID string) (json.RawMessage, error) {
|
||||||
var settings string
|
var settings string
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// RateLimitStore manages rate limit counters in SQLite.
|
// RateLimitStore manages rate limit counters in SQLite.
|
||||||
// v0.32.0: functional parity for single-process test coverage.
|
|
||||||
type RateLimitStore struct{}
|
type RateLimitStore struct{}
|
||||||
|
|
||||||
func NewRateLimitStore() *RateLimitStore { return &RateLimitStore{} }
|
func NewRateLimitStore() *RateLimitStore { return &RateLimitStore{} }
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ func (s *TeamStore) IsMember(ctx context.Context, teamID, userID string) (bool,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── CS1 additions (v0.29.0) ─────────────────────────────────────────────
|
// ── CS1 additions ─────────────────────────────────────────────
|
||||||
|
|
||||||
func (s *TeamStore) Exists(ctx context.Context, teamID string) (bool, error) {
|
func (s *TeamStore) Exists(ctx context.Context, teamID string) (bool, error) {
|
||||||
var count int
|
var count int
|
||||||
@@ -286,7 +286,7 @@ func (s *TeamStore) ListTeamAuditActions(ctx context.Context, teamID string) ([]
|
|||||||
return actions, rows.Err()
|
return actions, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CS5b additions (v0.29.0) ────────────────────────────────────────────
|
// ── CS5b additions ────────────────────────────────────────────
|
||||||
|
|
||||||
func (s *TeamStore) GetFirstTeamIDForUser(ctx context.Context, userID string) (string, error) {
|
func (s *TeamStore) GetFirstTeamIDForUser(ctx context.Context, userID string) (string, error) {
|
||||||
var teamID string
|
var teamID string
|
||||||
@@ -298,7 +298,7 @@ func (s *TeamStore) GetFirstTeamIDForUser(ctx context.Context, userID string) (s
|
|||||||
return teamID, nil
|
return teamID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CS6 additions (v0.29.0) ────────────────────────────────────────────
|
// ── CS6 additions ────────────────────────────────────────────
|
||||||
|
|
||||||
func (s *TeamStore) AddMemberReturningID(ctx context.Context, teamID, userID, role string) (string, error) {
|
func (s *TeamStore) AddMemberReturningID(ctx context.Context, teamID, userID, role string) (string, error) {
|
||||||
id := store.NewID()
|
id := store.NewID()
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// TicketStore manages WS auth tickets in SQLite.
|
// TicketStore manages WS auth tickets in SQLite.
|
||||||
// v0.32.0: functional parity for single-process test coverage.
|
|
||||||
type TicketStore struct{}
|
type TicketStore struct{}
|
||||||
|
|
||||||
func NewTicketStore() *TicketStore { return &TicketStore{} }
|
func NewTicketStore() *TicketStore { return &TicketStore{} }
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ func (s *UserStore) scanOne(ctx context.Context, query string, args ...interface
|
|||||||
ScanJSON(sj, &u.Settings)
|
ScanJSON(sj, &u.Settings)
|
||||||
return &u, nil
|
return &u, nil
|
||||||
}
|
}
|
||||||
// ── CS1 additions (v0.29.0) ─────────────────────────────────────────────
|
// ── CS1 additions ─────────────────────────────────────────────
|
||||||
|
|
||||||
func (s *UserStore) Exists(ctx context.Context, userID string) (bool, error) {
|
func (s *UserStore) Exists(ctx context.Context, userID string) (bool, error) {
|
||||||
var count int
|
var count int
|
||||||
@@ -238,7 +238,7 @@ func (s *UserStore) SearchActive(ctx context.Context, excludeUserID, query strin
|
|||||||
return results, rows.Err()
|
return results, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CS2 additions (v0.29.0) ─────────────────────────────────────────────
|
// ── CS2 additions ─────────────────────────────────────────────
|
||||||
|
|
||||||
func (s *UserStore) MergeSettings(ctx context.Context, userID string, patch []byte) error {
|
func (s *UserStore) MergeSettings(ctx context.Context, userID string, patch []byte) error {
|
||||||
_, err := DB.ExecContext(ctx, `
|
_, err := DB.ExecContext(ctx, `
|
||||||
@@ -275,7 +275,7 @@ func (s *UserStore) CountAll(ctx context.Context) (int, error) {
|
|||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CS4 additions (v0.29.0) ─────────────────────────────────────────────
|
// ── CS4 additions ─────────────────────────────────────────────
|
||||||
|
|
||||||
func (s *UserStore) ClearVaultKeys(ctx context.Context, userID string) error {
|
func (s *UserStore) ClearVaultKeys(ctx context.Context, userID string) error {
|
||||||
_, err := DB.ExecContext(ctx, `
|
_, err := DB.ExecContext(ctx, `
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ func nullIfEmpty(s string) interface{} {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Instances (v0.3.1) ──────────────────────
|
// ── Instances ──────────────────────
|
||||||
|
|
||||||
func (s *WorkflowStore) CreateInstance(ctx context.Context, inst *models.WorkflowInstance) error {
|
func (s *WorkflowStore) CreateInstance(ctx context.Context, inst *models.WorkflowInstance) error {
|
||||||
inst.ID = store.NewID()
|
inst.ID = store.NewID()
|
||||||
@@ -597,7 +597,7 @@ func (s *WorkflowStore) ListActiveInstances(ctx context.Context) ([]models.Workf
|
|||||||
return result, rows.Err()
|
return result, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Assignments (v0.3.1) ────────────────────
|
// ── Assignments ────────────────────
|
||||||
|
|
||||||
func (s *WorkflowStore) CreateAssignment(ctx context.Context, a *models.WorkflowAssignment) error {
|
func (s *WorkflowStore) CreateAssignment(ctx context.Context, a *models.WorkflowAssignment) error {
|
||||||
a.ID = store.NewID()
|
a.ID = store.NewID()
|
||||||
@@ -690,7 +690,7 @@ func (s *WorkflowStore) ListAssignmentsByUser(ctx context.Context, userID string
|
|||||||
return s.queryAssignments(ctx, q, args...)
|
return s.queryAssignments(ctx, q, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Signoffs (v0.3.4) ─────────────────────────
|
// ── Signoffs ─────────────────────────
|
||||||
|
|
||||||
func (s *WorkflowStore) CreateSignoff(ctx context.Context, so *models.WorkflowSignoff) error {
|
func (s *WorkflowStore) CreateSignoff(ctx context.Context, so *models.WorkflowSignoff) error {
|
||||||
so.ID = store.NewID()
|
so.ID = store.NewID()
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package store
|
|||||||
import "context"
|
import "context"
|
||||||
|
|
||||||
// TicketStore manages short-lived, single-use WebSocket auth tickets.
|
// TicketStore manages short-lived, single-use WebSocket auth tickets.
|
||||||
// v0.32.0: replaces in-memory sync.Map for cross-pod ticket validation.
|
|
||||||
type TicketStore interface {
|
type TicketStore interface {
|
||||||
// Issue creates a single-use ticket for the given user.
|
// Issue creates a single-use ticket for the given user.
|
||||||
// Returns the opaque ticket ID (128-bit hex).
|
// Returns the opaque ticket ID (128-bit hex).
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ type WorkflowStore interface {
|
|||||||
GetVersion(ctx context.Context, workflowID string, versionNumber int) (*models.WorkflowVersion, error)
|
GetVersion(ctx context.Context, workflowID string, versionNumber int) (*models.WorkflowVersion, error)
|
||||||
GetLatestVersion(ctx context.Context, workflowID string) (*models.WorkflowVersion, error)
|
GetLatestVersion(ctx context.Context, workflowID string) (*models.WorkflowVersion, error)
|
||||||
|
|
||||||
// Instances (v0.3.1+)
|
// Instances
|
||||||
CreateInstance(ctx context.Context, inst *models.WorkflowInstance) error
|
CreateInstance(ctx context.Context, inst *models.WorkflowInstance) error
|
||||||
GetInstance(ctx context.Context, id string) (*models.WorkflowInstance, error)
|
GetInstance(ctx context.Context, id string) (*models.WorkflowInstance, error)
|
||||||
GetInstanceByToken(ctx context.Context, token string) (*models.WorkflowInstance, error)
|
GetInstanceByToken(ctx context.Context, token string) (*models.WorkflowInstance, error)
|
||||||
@@ -43,7 +43,7 @@ type WorkflowStore interface {
|
|||||||
MarkInstanceStale(ctx context.Context, id string) error
|
MarkInstanceStale(ctx context.Context, id string) error
|
||||||
ListActiveInstances(ctx context.Context) ([]models.WorkflowInstance, error)
|
ListActiveInstances(ctx context.Context) ([]models.WorkflowInstance, error)
|
||||||
|
|
||||||
// Assignments (v0.3.1)
|
// Assignments
|
||||||
CreateAssignment(ctx context.Context, a *models.WorkflowAssignment) error
|
CreateAssignment(ctx context.Context, a *models.WorkflowAssignment) error
|
||||||
ClaimAssignment(ctx context.Context, id string, userID string) error
|
ClaimAssignment(ctx context.Context, id string, userID string) error
|
||||||
UnclaimAssignment(ctx context.Context, id string) error
|
UnclaimAssignment(ctx context.Context, id string) error
|
||||||
@@ -53,7 +53,7 @@ type WorkflowStore interface {
|
|||||||
ListAssignmentsByInstance(ctx context.Context, instanceID string) ([]models.WorkflowAssignment, error)
|
ListAssignmentsByInstance(ctx context.Context, instanceID string) ([]models.WorkflowAssignment, error)
|
||||||
ListAssignmentsByUser(ctx context.Context, userID string, status string) ([]models.WorkflowAssignment, error)
|
ListAssignmentsByUser(ctx context.Context, userID string, status string) ([]models.WorkflowAssignment, error)
|
||||||
|
|
||||||
// Signoffs (v0.3.4)
|
// Signoffs
|
||||||
CreateSignoff(ctx context.Context, s *models.WorkflowSignoff) error
|
CreateSignoff(ctx context.Context, s *models.WorkflowSignoff) error
|
||||||
ListSignoffs(ctx context.Context, instanceID, stage string) ([]models.WorkflowSignoff, error)
|
ListSignoffs(ctx context.Context, instanceID, stage string) ([]models.WorkflowSignoff, error)
|
||||||
CountSignoffs(ctx context.Context, instanceID, stage, decision string) (int, error)
|
CountSignoffs(ctx context.Context, instanceID, stage, decision string) (int, error)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Package triggers — engine.go
|
// Package triggers — engine.go
|
||||||
//
|
//
|
||||||
// v0.2.2: Core trigger engine. Manages extension-declared triggers
|
|
||||||
// (event bus subscriptions + webhook receivers) and user-created
|
// (event bus subscriptions + webhook receivers) and user-created
|
||||||
// scheduled tasks (cron). All three converge to sandbox.Runner.CallEntryPoint.
|
// scheduled tasks (cron). All three converge to sandbox.Runner.CallEntryPoint.
|
||||||
package triggers
|
package triggers
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
// Package webhook delivers HTTP POST notifications on task/workflow completion.
|
// Package webhook delivers HTTP POST notifications on task/workflow completion.
|
||||||
//
|
//
|
||||||
// v0.27.3: Retry (3 attempts, exponential backoff), HMAC-SHA256 signature,
|
|
||||||
// 10s timeout per attempt.
|
// 10s timeout per attempt.
|
||||||
// v0.28.0: Added RunID, TokensUsed fields (D1 audit fix).
|
|
||||||
package webhook
|
package webhook
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ func (e *Engine) advanceInternal(ctx context.Context, instanceID string, stageDa
|
|||||||
}
|
}
|
||||||
currentStage := stages[currentOrdinal]
|
currentStage := stages[currentOrdinal]
|
||||||
|
|
||||||
// ── Validation gate (v0.3.4) ──────────────
|
// ── Validation gate ──────────────
|
||||||
sc := ParseStageConfig(currentStage.StageConfig)
|
sc := ParseStageConfig(currentStage.StageConfig)
|
||||||
if sc.Validation != nil && sc.Validation.RequiredApprovals > 0 {
|
if sc.Validation != nil && sc.Validation.RequiredApprovals > 0 {
|
||||||
// Check for rejections first
|
// Check for rejections first
|
||||||
@@ -296,7 +296,7 @@ func (e *Engine) Cancel(ctx context.Context, instanceID string, userID string) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Public Entry (v0.3.3) ───────────────────
|
// ── Public Entry ───────────────────
|
||||||
|
|
||||||
// StartPublic creates an instance for a public_link workflow without authentication.
|
// StartPublic creates an instance for a public_link workflow without authentication.
|
||||||
// The caller is identified as "public:<uuid>". Returns the instance including its entry_token.
|
// The caller is identified as "public:<uuid>". Returns the instance including its entry_token.
|
||||||
@@ -358,7 +358,7 @@ func (e *Engine) AdvancePublic(ctx context.Context, entryToken string, stageData
|
|||||||
return e.advanceInternal(ctx, inst.ID, stageData, inst.StartedBy, 0)
|
return e.advanceInternal(ctx, inst.ID, stageData, inst.StartedBy, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Signoffs (v0.3.4) ─────────────────────────
|
// ── Signoffs ─────────────────────────
|
||||||
|
|
||||||
// SubmitSignoff records a user's approval or rejection at the current stage boundary.
|
// SubmitSignoff records a user's approval or rejection at the current stage boundary.
|
||||||
func (e *Engine) SubmitSignoff(ctx context.Context, instanceID, userID, decision, comment string) (*models.WorkflowSignoff, error) {
|
func (e *Engine) SubmitSignoff(ctx context.Context, instanceID, userID, decision, comment string) (*models.WorkflowSignoff, error) {
|
||||||
|
|||||||
676
src/dev.html
676
src/dev.html
@@ -1,676 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Switchboard Core — Dev Gallery</title>
|
|
||||||
<link rel="stylesheet" href="css/variables.css">
|
|
||||||
<link rel="stylesheet" href="css/sw-primitives.css">
|
|
||||||
<link rel="stylesheet" href="css/sw-shell.css">
|
|
||||||
<style>
|
|
||||||
body { overflow: auto; padding: 0; }
|
|
||||||
.dev-root { height: 100vh; display: flex; flex-direction: column; }
|
|
||||||
.dev-nav { display: flex; gap: 0; border-bottom: 1px solid var(--border); background: var(--bg-surface); flex-shrink: 0; padding: 0 1rem; }
|
|
||||||
.dev-nav button {
|
|
||||||
padding: 0.6rem 1.2rem; background: none; border: none; border-bottom: 2px solid transparent;
|
|
||||||
color: var(--text-2); font-family: var(--font); font-size: 0.85rem; font-weight: 500; cursor: pointer;
|
|
||||||
}
|
|
||||||
.dev-nav button:hover { color: var(--text); }
|
|
||||||
.dev-nav button.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
||||||
.dev-page { flex: 1; overflow: auto; }
|
|
||||||
.gallery { max-width: 800px; margin: 0 auto; padding: 2rem; }
|
|
||||||
.gallery h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
|
|
||||||
.gallery .subtitle { color: var(--text-2); margin-bottom: 2rem; font-size: 0.9rem; }
|
|
||||||
.section { margin-bottom: 2.5rem; }
|
|
||||||
.section h2 {
|
|
||||||
font-size: 0.85rem; font-weight: 600; text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px; color: var(--text-3); margin-bottom: 0.75rem;
|
|
||||||
padding-bottom: 0.5rem; border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
.row { display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap; margin-bottom: 0.75rem; }
|
|
||||||
.col { display: flex; flex-direction: column; gap: 0.75rem; }
|
|
||||||
.spacer { height: 0.5rem; }
|
|
||||||
.shell-demo { height: 100%; }
|
|
||||||
.shell-demo .sw-shell { height: 100%; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module">
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const { h, render } = await import('./js/sw/vendor/preact.module.js');
|
|
||||||
const hooksModule = await import('./js/sw/vendor/hooks.module.js?v=2');
|
|
||||||
const { default: htm } = await import('./js/sw/vendor/htm.module.js');
|
|
||||||
const html = htm.bind(h);
|
|
||||||
|
|
||||||
const { useState, useEffect, useRef, useMemo, useCallback } = hooksModule;
|
|
||||||
window.preact = { h, render };
|
|
||||||
window.hooks = hooksModule;
|
|
||||||
window.html = html;
|
|
||||||
|
|
||||||
const { Button } = await import('./js/sw/primitives/button.js');
|
|
||||||
const { Spinner } = await import('./js/sw/primitives/spinner.js');
|
|
||||||
const { Avatar } = await import('./js/sw/primitives/avatar.js');
|
|
||||||
const { FormField } = await import('./js/sw/primitives/form-field.js');
|
|
||||||
const { Tooltip } = await import('./js/sw/primitives/tooltip.js');
|
|
||||||
const { Banner } = await import('./js/sw/primitives/banner.js');
|
|
||||||
const { Dialog } = await import('./js/sw/primitives/dialog.js');
|
|
||||||
const { toast, ToastContainer } = await import('./js/sw/primitives/toast.js');
|
|
||||||
const { Menu } = await import('./js/sw/primitives/menu.js');
|
|
||||||
const { Drawer } = await import('./js/sw/primitives/drawer.js');
|
|
||||||
const { Tabs } = await import('./js/sw/primitives/tabs.js');
|
|
||||||
const { Dropdown } = await import('./js/sw/primitives/dropdown.js');
|
|
||||||
const { AppShell } = await import('./js/sw/shell/app-shell.js');
|
|
||||||
const { UserMenu } = await import('./js/sw/shell/user-menu.js');
|
|
||||||
const { SurfaceViewport } = await import('./js/sw/shell/surface-viewport.js');
|
|
||||||
const { DialogStack } = await import('./js/sw/shell/dialog-stack.js');
|
|
||||||
const { confirm } = await import('./js/sw/primitives/confirm.js');
|
|
||||||
const { prompt } = await import('./js/sw/primitives/prompt.js');
|
|
||||||
|
|
||||||
// ── SDK Boot ────────────────────────────
|
|
||||||
const { boot } = await import('./js/sw/sdk/index.js');
|
|
||||||
await boot().catch(e => console.warn('SDK boot (no backend):', e));
|
|
||||||
const sw = window.sw;
|
|
||||||
|
|
||||||
// ── SDK Demo Page ───────────────────────
|
|
||||||
function SDKPage() {
|
|
||||||
const [authState, setAuthState] = useState(null);
|
|
||||||
const [apiResult, setApiResult] = useState(null);
|
|
||||||
const [permInput, setPermInput] = useState('model.use');
|
|
||||||
const [canResult, setCanResult] = useState(null);
|
|
||||||
const [eventLog, setEventLog] = useState([]);
|
|
||||||
const [emitLabel, setEmitLabel] = useState('test.event');
|
|
||||||
const logRef = useRef([]);
|
|
||||||
|
|
||||||
// Subscribe to all events for live log
|
|
||||||
useEffect(() => {
|
|
||||||
const unsub = sw.on('*', (payload, meta) => {
|
|
||||||
const entry = { ts: new Date().toLocaleTimeString(), label: meta.event, local: meta.local };
|
|
||||||
logRef.current = [entry, ...logRef.current].slice(0, 20);
|
|
||||||
setEventLog([...logRef.current]);
|
|
||||||
});
|
|
||||||
return unsub;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function refreshAuth() {
|
|
||||||
setAuthState({
|
|
||||||
isAuthenticated: sw.auth.isAuthenticated,
|
|
||||||
user: sw.auth.user,
|
|
||||||
permissions: [...sw.auth.permissions],
|
|
||||||
teams: sw.auth.teams,
|
|
||||||
policies: sw.auth.policies,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => { refreshAuth(); }, []);
|
|
||||||
|
|
||||||
async function testApi() {
|
|
||||||
try {
|
|
||||||
const res = await sw.api.channels.list();
|
|
||||||
setApiResult({ ok: true, data: res });
|
|
||||||
} catch (e) {
|
|
||||||
setApiResult({ ok: false, error: e.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function testCan() {
|
|
||||||
setCanResult(sw.can(permInput));
|
|
||||||
}
|
|
||||||
|
|
||||||
const monoStyle = 'font-family: var(--font-mono, monospace); font-size: 0.8rem; background: var(--bg-1); padding: 0.75rem; border-radius: 6px; overflow: auto; max-height: 200px; white-space: pre-wrap; word-break: break-all; color: var(--text-2);';
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="gallery">
|
|
||||||
<h1>SDK Demo</h1>
|
|
||||||
<p class="subtitle">Layer 1 — SDK modules (v0.37.3)</p>
|
|
||||||
|
|
||||||
<!-- Auth State -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>sw.auth</h2>
|
|
||||||
<div class="row" style="margin-bottom:0.5rem">
|
|
||||||
<${Button} variant="secondary" size="sm" onClick=${refreshAuth}>Refresh<//>
|
|
||||||
</div>
|
|
||||||
<div style=${monoStyle}>${JSON.stringify(authState, null, 2)}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- API Test -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>sw.api</h2>
|
|
||||||
<div class="row" style="margin-bottom:0.5rem">
|
|
||||||
<${Button} variant="secondary" size="sm" onClick=${testApi}>sw.api.channels.list()<//>
|
|
||||||
</div>
|
|
||||||
${apiResult && html`
|
|
||||||
<div style=${monoStyle}>
|
|
||||||
${apiResult.ok
|
|
||||||
? JSON.stringify(apiResult.data, null, 2)
|
|
||||||
: 'Error: ' + apiResult.error}
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- RBAC Test -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>sw.can()</h2>
|
|
||||||
<div class="row">
|
|
||||||
<input class="sw-input" style="width:180px" value=${permInput}
|
|
||||||
onInput=${e => setPermInput(e.target.value)} />
|
|
||||||
<${Button} variant="secondary" size="sm" onClick=${testCan}>Check<//>
|
|
||||||
${canResult !== null && html`
|
|
||||||
<span style="font-weight:600; color: ${canResult ? 'var(--success)' : 'var(--danger)'}">
|
|
||||||
${canResult ? 'ALLOWED' : 'DENIED'}
|
|
||||||
</span>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
<div class="row" style="margin-top:0.5rem">
|
|
||||||
<span style="color:var(--text-3); font-size:0.8rem">
|
|
||||||
sw.isAdmin: ${String(sw.isAdmin)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Theme -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>sw.theme</h2>
|
|
||||||
<div class="row">
|
|
||||||
<span style="color:var(--text-2); font-size:0.85rem">
|
|
||||||
mode: ${sw.theme.mode} · resolved: ${sw.theme.current}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<${Button} variant="secondary" size="sm" onClick=${() => sw.theme.set('light')}>Light<//>
|
|
||||||
<${Button} variant="secondary" size="sm" onClick=${() => sw.theme.set('dark')}>Dark<//>
|
|
||||||
<${Button} variant="secondary" size="sm" onClick=${() => sw.theme.set('system')}>System<//>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Events -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>sw.on / sw.emit</h2>
|
|
||||||
<div class="row" style="margin-bottom:0.5rem">
|
|
||||||
<input class="sw-input" style="width:180px" value=${emitLabel}
|
|
||||||
onInput=${e => setEmitLabel(e.target.value)} />
|
|
||||||
<${Button} variant="secondary" size="sm"
|
|
||||||
onClick=${() => sw.emit(emitLabel, { demo: true }, { localOnly: true })}>Emit<//>
|
|
||||||
</div>
|
|
||||||
<div style=${monoStyle + ' max-height:160px;'}>
|
|
||||||
${eventLog.length === 0 ? '(listening for events...)' :
|
|
||||||
eventLog.map(e => `${e.ts} ${e.local ? 'L' : 'R'} ${e.label}`).join('\n')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pipe -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>sw.pipe</h2>
|
|
||||||
<div style=${monoStyle}>${JSON.stringify(sw.pipe.list(), null, 2)}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Namespaces -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>sw.api namespaces</h2>
|
|
||||||
<div style=${monoStyle}>${Object.keys(sw.api).filter(k => typeof sw.api[k] === 'object').join(', ')}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Shell Demo Page ──────────────────────
|
|
||||||
function ShellPage() {
|
|
||||||
const [confirmResult, setConfirmResult] = useState(null);
|
|
||||||
const [promptResult, setPromptResult] = useState(null);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="gallery">
|
|
||||||
<h1>Shell Demo</h1>
|
|
||||||
<p class="subtitle">Layer 2 — Application shell (v0.37.4)</p>
|
|
||||||
|
|
||||||
<!-- UserMenu -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>UserMenu</h2>
|
|
||||||
<div class="row">
|
|
||||||
<${UserMenu} />
|
|
||||||
<${UserMenu} placement="down-left" />
|
|
||||||
</div>
|
|
||||||
<p style="color:var(--text-3); font-size:0.8rem; margin-top:0.5rem">
|
|
||||||
RBAC-gated flyout. Without backend, shows all items for demo.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Imperative Confirm -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>sw.confirm()</h2>
|
|
||||||
<div class="row">
|
|
||||||
<${Button} variant="secondary" onClick=${async () => {
|
|
||||||
const ok = await sw.confirm('Delete this item?', { destructive: true });
|
|
||||||
setConfirmResult(ok);
|
|
||||||
}}>Confirm (destructive)<//>
|
|
||||||
<${Button} variant="secondary" onClick=${async () => {
|
|
||||||
const ok = await sw.confirm('Proceed with this action?');
|
|
||||||
setConfirmResult(ok);
|
|
||||||
}}>Confirm (normal)<//>
|
|
||||||
${confirmResult !== null && html`
|
|
||||||
<span style="font-weight:600; color: ${confirmResult ? 'var(--success)' : 'var(--danger)'}">
|
|
||||||
Result: ${String(confirmResult)}
|
|
||||||
</span>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Imperative Prompt -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>sw.prompt()</h2>
|
|
||||||
<div class="row">
|
|
||||||
<${Button} variant="secondary" onClick=${async () => {
|
|
||||||
const val = await sw.prompt('Enter a name:', { defaultValue: 'untitled' });
|
|
||||||
setPromptResult(val);
|
|
||||||
}}>Prompt<//>
|
|
||||||
${promptResult !== null && html`
|
|
||||||
<span style="font-weight:600; color:var(--text-2)">
|
|
||||||
Result: ${JSON.stringify(promptResult)}
|
|
||||||
</span>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Surface Viewport -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>SurfaceViewport</h2>
|
|
||||||
<p style="color:var(--text-3); font-size:0.8rem; margin-bottom:0.5rem">
|
|
||||||
UserMenu is placed by the surface (top-right), not the shell.
|
|
||||||
</p>
|
|
||||||
<div style="border:1px dashed var(--border); border-radius:var(--radius, 6px); height:200px; position:relative; overflow:hidden;">
|
|
||||||
<${SurfaceViewport} surface=${html`
|
|
||||||
<div style="padding:1rem; color:var(--text-2); height:100%;">
|
|
||||||
<p>Mock surface content. The shell provides the viewport; the surface owns everything inside.</p>
|
|
||||||
<div style="position:absolute; top:0.5rem; right:0.5rem;">
|
|
||||||
<${UserMenu} placement="down-left" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- App component tree -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>App Component Tree</h2>
|
|
||||||
<div style="font-family:var(--font-mono, monospace); font-size:0.8rem; color:var(--text-2); background:var(--bg-1); padding:0.75rem; border-radius:6px; white-space:pre;">${
|
|
||||||
`<App>
|
|
||||||
<AppShell banner message footer>
|
|
||||||
<SurfaceViewport surface={...} />
|
|
||||||
</AppShell>
|
|
||||||
<ToastContainer />
|
|
||||||
<DialogStack />
|
|
||||||
</App>`
|
|
||||||
}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Surfaces Demo Page ────────────────────
|
|
||||||
function SurfacesPage() {
|
|
||||||
return html`
|
|
||||||
<div class="gallery">
|
|
||||||
<h1>Surfaces Demo</h1>
|
|
||||||
<p class="subtitle">v0.37.5 — Login + Settings surfaces</p>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Login Surface</h2>
|
|
||||||
<p style="color:var(--text-3); font-size:0.85rem; margin-bottom:0.75rem">
|
|
||||||
The login surface is a standalone page (not embedded in AppShell).
|
|
||||||
It renders at /login with its own layout: Hero panel + Auth panel.
|
|
||||||
</p>
|
|
||||||
<div style="font-family:var(--font-mono, monospace); font-size:0.8rem; color:var(--text-2); background:var(--bg-1); padding:0.75rem; border-radius:6px; white-space:pre;">${
|
|
||||||
`<LoginSurface>
|
|
||||||
<Hero />
|
|
||||||
<AuthPanel>
|
|
||||||
authMode === 'builtin':
|
|
||||||
<LoginForm /> — sw.auth.login()
|
|
||||||
<RegisterForm /> — sw.api.auth.register()
|
|
||||||
authMode === 'oidc' | 'mtls':
|
|
||||||
<SSOPanel />
|
|
||||||
</AuthPanel>
|
|
||||||
</LoginSurface>`
|
|
||||||
}</div>
|
|
||||||
<p style="color:var(--text-3); font-size:0.8rem; margin-top:0.5rem">
|
|
||||||
Files: src/js/sw/surfaces/login/
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Settings Surface</h2>
|
|
||||||
<p style="color:var(--text-3); font-size:0.85rem; margin-bottom:0.75rem">
|
|
||||||
Full Preact surface with left nav + section routing.
|
|
||||||
Core sections (General, Appearance, Profile, Teams, Models, Providers,
|
|
||||||
Personas, Roles, Usage) are native Preact components.
|
|
||||||
Deferred sections use BridgeSection wrappers.
|
|
||||||
</p>
|
|
||||||
<div style="font-family:var(--font-mono, monospace); font-size:0.8rem; color:var(--text-2); background:var(--bg-1); padding:0.75rem; border-radius:6px; white-space:pre;">${
|
|
||||||
`<SettingsSurface>
|
|
||||||
<Topbar />
|
|
||||||
<Nav>
|
|
||||||
General, Appearance, Models, Personas, Profile,
|
|
||||||
Teams, Workflows, Tasks, Git Keys, Data & Privacy
|
|
||||||
[BYOK] Providers, Roles, Usage
|
|
||||||
</Nav>
|
|
||||||
<Content>
|
|
||||||
section === 'general' → <GeneralSection />
|
|
||||||
section === 'appearance' → <AppearanceSection />
|
|
||||||
section === 'profile' → <ProfileSection />
|
|
||||||
section === 'teams' → <TeamsSection />
|
|
||||||
section === 'models' → <ModelsSection />
|
|
||||||
section === 'providers' → <ProvidersSection />
|
|
||||||
section === 'personas' → <PersonasSection />
|
|
||||||
section === 'roles' → <RolesSection />
|
|
||||||
section === 'usage' → <UsageSection />
|
|
||||||
section === 'tasks' etc → <BridgeSection /> (old JS)
|
|
||||||
</Content>
|
|
||||||
</SettingsSurface>`
|
|
||||||
}</div>
|
|
||||||
<p style="color:var(--text-3); font-size:0.8rem; margin-top:0.5rem">
|
|
||||||
Files: src/js/sw/surfaces/settings/
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function PrimitivesPage() {
|
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
|
||||||
const [activeTab, setActiveTab] = useState('tab1');
|
|
||||||
const [dropVal, setDropVal] = useState('opt2');
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const menuAnchor = useRef(null);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="gallery">
|
|
||||||
<h1>Primitives Gallery</h1>
|
|
||||||
<p class="subtitle">Layer 0 — Preact + htm components (v0.37.2)</p>
|
|
||||||
|
|
||||||
<!-- Buttons -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>Button</h2>
|
|
||||||
<div class="row">
|
|
||||||
<${Button} variant="primary">Primary<//>
|
|
||||||
<${Button} variant="secondary">Secondary<//>
|
|
||||||
<${Button} variant="danger">Danger<//>
|
|
||||||
<${Button} variant="ghost">Ghost<//>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<${Button} size="sm">Small<//>
|
|
||||||
<${Button} size="md">Medium<//>
|
|
||||||
<${Button} size="lg">Large<//>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<${Button} disabled>Disabled<//>
|
|
||||||
<${Button} loading=${true}>Loading<//>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Spinner -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>Spinner</h2>
|
|
||||||
<div class="row">
|
|
||||||
<${Spinner} size="sm" />
|
|
||||||
<${Spinner} size="md" />
|
|
||||||
<${Spinner} size="lg" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Avatar -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>Avatar</h2>
|
|
||||||
<div class="row">
|
|
||||||
<${Avatar} name="Jane Doe" size="sm" />
|
|
||||||
<${Avatar} name="Jane Doe" size="md" />
|
|
||||||
<${Avatar} name="Jane Doe" size="lg" />
|
|
||||||
<${Avatar} name="X" size="md" />
|
|
||||||
<${Avatar} src="https://i.pravatar.cc/48" name="Photo" size="lg" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- FormField -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>FormField</h2>
|
|
||||||
<div class="col" style="max-width:320px">
|
|
||||||
<${FormField} label="Username" required>
|
|
||||||
<input class="sw-input" placeholder="Enter username" />
|
|
||||||
<//>
|
|
||||||
<${FormField} label="Email" error="Invalid email address">
|
|
||||||
<input class="sw-input" value="bad@" />
|
|
||||||
<//>
|
|
||||||
<${FormField} label="Bio" hint="Optional, max 200 chars">
|
|
||||||
<textarea class="sw-input" rows="2" placeholder="Tell us about yourself" />
|
|
||||||
<//>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tooltip -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>Tooltip</h2>
|
|
||||||
<div class="row">
|
|
||||||
<${Tooltip} content="Top tooltip">
|
|
||||||
<${Button} variant="secondary">Hover me (top)<//>
|
|
||||||
<//>
|
|
||||||
<${Tooltip} content="Bottom tooltip" position="bottom">
|
|
||||||
<${Button} variant="secondary">Bottom<//>
|
|
||||||
<//>
|
|
||||||
<${Tooltip} content="Right tooltip" position="right">
|
|
||||||
<${Button} variant="secondary">Right<//>
|
|
||||||
<//>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Banner -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>Banner</h2>
|
|
||||||
<div class="col">
|
|
||||||
<${Banner} text="Informational banner message" variant="info" />
|
|
||||||
<${Banner} text="Warning: maintenance scheduled" variant="warn" dismissible />
|
|
||||||
<${Banner} text="Error: service unavailable" variant="error" />
|
|
||||||
<${Banner} text="Successfully deployed v0.37.2" variant="success" dismissible />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tabs -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>Tabs</h2>
|
|
||||||
<${Tabs}
|
|
||||||
tabs=${[
|
|
||||||
{ label: 'General', value: 'tab1' },
|
|
||||||
{ label: 'Appearance', value: 'tab2' },
|
|
||||||
{ label: 'Notifications', value: 'tab3' },
|
|
||||||
{ label: 'Disabled', value: 'tab4', disabled: true },
|
|
||||||
]}
|
|
||||||
active=${activeTab}
|
|
||||||
onChange=${setActiveTab}
|
|
||||||
/>
|
|
||||||
<div style="padding: 1rem 0; color: var(--text-2); font-size: 0.9rem">
|
|
||||||
Active tab: ${activeTab}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Dropdown -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>Dropdown</h2>
|
|
||||||
<div class="row">
|
|
||||||
<div style="width:220px">
|
|
||||||
<${Dropdown}
|
|
||||||
options=${[
|
|
||||||
{ label: 'Claude Sonnet', value: 'opt1' },
|
|
||||||
{ label: 'Claude Opus', value: 'opt2' },
|
|
||||||
{ label: 'Claude Haiku', value: 'opt3' },
|
|
||||||
{ label: 'GPT-4o', value: 'opt4' },
|
|
||||||
{ label: 'Disabled', value: 'opt5', disabled: true },
|
|
||||||
]}
|
|
||||||
value=${dropVal}
|
|
||||||
onChange=${setDropVal}
|
|
||||||
searchable
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Menu -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>Menu</h2>
|
|
||||||
<div class="row">
|
|
||||||
<${Button} ref=${menuAnchor} variant="secondary"
|
|
||||||
onClick=${() => setMenuOpen(!menuOpen)}>
|
|
||||||
Open Menu
|
|
||||||
<//>
|
|
||||||
<${Menu}
|
|
||||||
open=${menuOpen}
|
|
||||||
anchor=${menuAnchor.current}
|
|
||||||
items=${[
|
|
||||||
{ label: 'Settings', action: 'settings', icon: '\u2699' },
|
|
||||||
{ label: 'Profile', action: 'profile', icon: '\ud83d\udc64' },
|
|
||||||
{ divider: true },
|
|
||||||
{ label: 'Submenu', children: [
|
|
||||||
{ label: 'Option A', action: 'a' },
|
|
||||||
{ label: 'Option B', action: 'b' },
|
|
||||||
]},
|
|
||||||
{ divider: true },
|
|
||||||
{ label: 'Disabled', action: 'x', disabled: true },
|
|
||||||
{ label: 'Sign Out', action: 'logout' },
|
|
||||||
]}
|
|
||||||
onSelect=${(action) => { toast.info('Selected: ' + action); }}
|
|
||||||
onClose=${() => setMenuOpen(false)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Dialog -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>Dialog</h2>
|
|
||||||
<div class="row">
|
|
||||||
<${Button} onClick=${() => setDialogOpen(true)}>Open Dialog<//>
|
|
||||||
</div>
|
|
||||||
<${Dialog}
|
|
||||||
open=${dialogOpen}
|
|
||||||
title="Example Dialog"
|
|
||||||
onClose=${() => setDialogOpen(false)}
|
|
||||||
actions=${[
|
|
||||||
{ label: 'Cancel', variant: 'secondary', onClick: () => setDialogOpen(false) },
|
|
||||||
{ label: 'Confirm', variant: 'primary', onClick: () => { setDialogOpen(false); toast.success('Confirmed!'); } },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<p>This is a modal dialog with focus trapping, Escape to close, and backdrop click to dismiss.</p>
|
|
||||||
<//>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Drawer -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>Drawer</h2>
|
|
||||||
<div class="row">
|
|
||||||
<${Button} variant="secondary" onClick=${() => setDrawerOpen(true)}>Open Drawer<//>
|
|
||||||
</div>
|
|
||||||
<${Drawer}
|
|
||||||
open=${drawerOpen}
|
|
||||||
title="Settings"
|
|
||||||
onClose=${() => setDrawerOpen(false)}
|
|
||||||
>
|
|
||||||
<p style="color: var(--text-2)">Slide-in panel for settings, admin sections, etc.</p>
|
|
||||||
<div class="spacer" />
|
|
||||||
<${FormField} label="Display Name">
|
|
||||||
<input class="sw-input" value="Jane Doe" />
|
|
||||||
<//>
|
|
||||||
<//>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Toast -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>Toast</h2>
|
|
||||||
<div class="row">
|
|
||||||
<${Button} variant="secondary" onClick=${() => toast.success('Operation successful')}>Success<//>
|
|
||||||
<${Button} variant="secondary" onClick=${() => toast.error('Something went wrong')}>Error<//>
|
|
||||||
<${Button} variant="secondary" onClick=${() => toast.warn('Check your settings')}>Warning<//>
|
|
||||||
<${Button} variant="secondary" onClick=${() => toast.info('New message received')}>Info<//>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VARIANTS = ['info', 'warn', 'error', 'success'];
|
|
||||||
|
|
||||||
function ShellControls({ bannerOn, setBannerOn, bannerVariant, setBannerVariant, bannerText, setBannerText,
|
|
||||||
msgOn, setMsgOn, msgVariant, setMsgVariant, msgText, setMsgText,
|
|
||||||
footerOn, setFooterOn }) {
|
|
||||||
return html`
|
|
||||||
<div style="display:flex; align-items:center; gap:0.75rem; padding:0.4rem 0; flex-wrap:wrap;">
|
|
||||||
<${Button} variant=${bannerOn ? 'primary' : 'secondary'} size="sm"
|
|
||||||
onClick=${() => setBannerOn(!bannerOn)}>Banner<//>
|
|
||||||
${bannerOn && html`
|
|
||||||
<${Dropdown} options=${VARIANTS.map(v => ({ label: v, value: v }))}
|
|
||||||
value=${bannerVariant} onChange=${setBannerVariant} />
|
|
||||||
<input class="sw-input" style="width:140px" placeholder="text..."
|
|
||||||
value=${bannerText} onInput=${e => setBannerText(e.target.value)} />
|
|
||||||
`}
|
|
||||||
<${Button} variant=${msgOn ? 'primary' : 'secondary'} size="sm"
|
|
||||||
onClick=${() => setMsgOn(!msgOn)}>Message<//>
|
|
||||||
${msgOn && html`
|
|
||||||
<${Dropdown} options=${VARIANTS.map(v => ({ label: v, value: v }))}
|
|
||||||
value=${msgVariant} onChange=${setMsgVariant} />
|
|
||||||
<input class="sw-input" style="width:140px" placeholder="text..."
|
|
||||||
value=${msgText} onInput=${e => setMsgText(e.target.value)} />
|
|
||||||
`}
|
|
||||||
<${Button} variant=${footerOn ? 'primary' : 'secondary'} size="sm"
|
|
||||||
onClick=${() => setFooterOn(!footerOn)}>Footer<//>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DevRoot() {
|
|
||||||
const [page, setPage] = useState('primitives');
|
|
||||||
const [bannerOn, setBannerOn] = useState(false);
|
|
||||||
const [msgOn, setMsgOn] = useState(false);
|
|
||||||
const [footerOn, setFooterOn] = useState(false);
|
|
||||||
const [bannerVariant, setBannerVariant] = useState('info');
|
|
||||||
const [msgVariant, setMsgVariant] = useState('info');
|
|
||||||
const [bannerText, setBannerText] = useState('');
|
|
||||||
const [msgText, setMsgText] = useState('');
|
|
||||||
|
|
||||||
const banner = bannerOn ? { text: bannerText || 'Banner', variant: bannerVariant } : null;
|
|
||||||
const msg = msgOn ? { text: msgText || 'Message', variant: msgVariant } : null;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<${AppShell}
|
|
||||||
banner=${banner}
|
|
||||||
message=${msg}
|
|
||||||
footer=${footerOn ? html`Switchboard Core · Layer 1 Shell` : null}
|
|
||||||
>
|
|
||||||
<div class="dev-root">
|
|
||||||
<nav class="dev-nav">
|
|
||||||
<button class=${page === 'primitives' ? 'active' : ''} onClick=${() => setPage('primitives')}>Primitives</button>
|
|
||||||
<button class=${page === 'shell' ? 'active' : ''} onClick=${() => setPage('shell')}>Shell</button>
|
|
||||||
<button class=${page === 'sdk' ? 'active' : ''} onClick=${() => setPage('sdk')}>SDK</button>
|
|
||||||
<button class=${page === 'surfaces' ? 'active' : ''} onClick=${() => setPage('surfaces')}>Surfaces</button>
|
|
||||||
<div style="flex:1" />
|
|
||||||
<${ShellControls} ...${{
|
|
||||||
bannerOn, setBannerOn, bannerVariant, setBannerVariant, bannerText, setBannerText,
|
|
||||||
msgOn, setMsgOn, msgVariant, setMsgVariant, msgText, setMsgText,
|
|
||||||
footerOn, setFooterOn
|
|
||||||
}} />
|
|
||||||
</nav>
|
|
||||||
<div class="dev-page">
|
|
||||||
${page === 'primitives' && html`<${PrimitivesPage} />`}
|
|
||||||
${page === 'shell' && html`<${ShellPage} />`}
|
|
||||||
${page === 'sdk' && html`<${SDKPage} />`}
|
|
||||||
${page === 'surfaces' && html`<${SurfacesPage} />`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<//>
|
|
||||||
<${ToastContainer} />
|
|
||||||
<${DialogStack} />
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(html`<${DevRoot} />`, document.getElementById('app'));
|
|
||||||
} catch(e) { console.error('DEV:', e); }
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -243,7 +243,6 @@ describe('init() profile gate', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Boot-time 401 redirect suppression ──────
|
// ── Boot-time 401 redirect suppression ──────
|
||||||
// v0.37.14: Stale cookie blank-login-page fix.
|
|
||||||
// When the login page boots the SDK with stale localStorage tokens,
|
// When the login page boots the SDK with stale localStorage tokens,
|
||||||
// the REST client's 401 handler must NOT redirect to /login (we're
|
// the REST client's 401 handler must NOT redirect to /login (we're
|
||||||
// already there). boot() handles 401 gracefully on its own.
|
// already there). boot() handles 401 gracefully on its own.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user