diff --git a/CHANGELOG.md b/CHANGELOG.md index d131d82..9047700 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,47 @@ 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 Docs surface polish and dynamic OpenAPI spec with extension route merging. diff --git a/ROADMAP.md b/ROADMAP.md index b0ba27f..7255721 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # 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, 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 | |------|--------|-------------| -| 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 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 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. | -| 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. | -| 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=*`. | +| 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 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 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. | +| 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. | +| 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=*`. | ### v0.6.4 — Admin Health/Metrics Tab + Cluster Merge diff --git a/VERSION b/VERSION index b616048..844f6a9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.2 +0.6.3 diff --git a/docs/DISTRIBUTION.md b/docs/DISTRIBUTION.md index 3c77f3f..04f5337 100644 --- a/docs/DISTRIBUTION.md +++ b/docs/DISTRIBUTION.md @@ -21,20 +21,22 @@ The production Docker image ships with pre-built packages. A **curated default s | Package | Type | Description | |---------|------|-------------| | notes | surface | Markdown notes with graph view | +| chat | surface | Real-time chat surface | | chat-core | library | Conversations, messages, read cursors | -| 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 | +| mermaid-renderer | extension | Diagram rendering | +| schedules | surface | Cron task management UI | #### Opt-in (ship in image, require `BUNDLED_PACKAGES` to enable) | Package | Type | Description | |---------|------|-------------| | 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 | | hello-dashboard | surface | Welcome/getting started surface | | 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 | | js-sandbox | extension | JavaScript sandbox | | katex-renderer | extension | LaTeX rendering | -| mermaid-renderer | extension | Diagram rendering | | regex-tester | extension | Regex testing tool | | icd-test-runner | surface | E2E API test suite | | sdk-test-runner | surface | SDK feature test suite | diff --git a/docs/PACKAGE-REGISTRY.md b/docs/PACKAGE-REGISTRY.md new file mode 100644 index 0000000..ade0005 --- /dev/null +++ b/docs/PACKAGE-REGISTRY.md @@ -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. diff --git a/scripts/generate-registry.sh b/scripts/generate-registry.sh new file mode 100755 index 0000000..0ad3072 --- /dev/null +++ b/scripts/generate-registry.sh @@ -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 diff --git a/server/config/config.go b/server/config/config.go index dd8bb29..994b59b 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -26,18 +26,18 @@ type Config struct { // Seed users (dev/test only) — CSV: "user:pass:role,user2:pass2:role2" 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. // Personal keys use per-user UEK (derived from password). EncryptionKey string - // File storage (v0.12.0+) + // File storage // STORAGE_BACKEND: "pvc" or "s3". Empty = auto-detect (pvc if path writable). // STORAGE_PATH: mount point for PVC backend (default /data/storage). StorageBackend string StoragePath string - // S3-compatible storage (v0.12.0+) + // S3-compatible storage // Works with AWS S3, MinIO, Ceph RGW, or any S3-compatible API. S3Endpoint string // custom endpoint URL (required for MinIO/Ceph, optional for AWS) 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/") 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_LEVEL: "debug", "info" (default), "warn", "error". LogFormat string LogLevel string - // Auth mode (v0.24.0): "builtin" (default) | "mtls" | "oidc" + // Auth mode: "builtin" (default) | "mtls" | "oidc" AuthMode string - // mTLS (v0.24.1) + // mTLS // Headers injected by TLS-terminating reverse proxy. MTLSHeaderDN string // default "X-SSL-Client-DN" MTLSHeaderVerify string // default "X-SSL-Client-Verify" @@ -64,7 +64,7 @@ type Config struct { MTLSAutoActivate bool // auto-activate new users (default true) 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. // BUNDLED_PACKAGES_DIR: directory containing pre-built .pkg archives (default /app/bundled-packages). // BUNDLED_PACKAGES: controls which bundled packages are auto-installed: @@ -75,7 +75,7 @@ type Config struct { BundledPackagesDir string BundledPackages string - // OIDC (v0.24.1) + // OIDC OIDCIssuerURL string // e.g. "https://keycloak.corp/realms/switchboard" OIDCExternalIssuerURL string // OIDC_EXTERNAL_ISSUER_URL OIDCClientID string @@ -87,7 +87,7 @@ type Config struct { OIDCGroupsClaim string // JWT claim for group sync (default "groups") 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. // CLUSTER_NODE_ID: override for deterministic identity (default: hostname-PID). // CLUSTER_HEARTBEAT_INTERVAL: tick frequency (default "10s"). diff --git a/server/crypto/backfill.go b/server/crypto/backfill.go index 6809ba0..4bc5096 100644 --- a/server/crypto/backfill.go +++ b/server/crypto/backfill.go @@ -10,7 +10,7 @@ import ( // 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. // -// 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). // // 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) 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 db.QueryRow(` SELECT EXISTS ( @@ -121,7 +121,7 @@ func EnforceEncryptionKey(db *sql.DB, encryptionKey string) error { if hasPlaintext { return fmt.Errorf( "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)") diff --git a/server/database/migrate.go b/server/database/migrate.go index 429f641..6c92aac 100644 --- a/server/database/migrate.go +++ b/server/database/migrate.go @@ -19,7 +19,7 @@ func SchemaVersion() string { return schemaVersion } // Migrate runs all pending migrations for the current dialect. // Migration files live in migrations// subdirectories. // 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 { if DB == nil { return fmt.Errorf("database not connected") diff --git a/server/events/bus_test.go b/server/events/bus_test.go index 1441e59..9e5b6be 100644 --- a/server/events/bus_test.go +++ b/server/events/bus_test.go @@ -113,14 +113,14 @@ func TestRouteFor(t *testing.T) { {"pong", DirToClient}, // Tool bridge routes {"tool.call.abc123", DirToClient}, - {"tool.result.abc123", DirBoth}, // v0.32.0: DirBoth for cross-pod WaitFor + {"tool.result.abc123", DirBoth}, // Extension lifecycle {"extension.loaded", DirLocal}, {"extension.error", DirLocal}, - // Realtime (v0.5.0) + // Realtime {"realtime.chat.message", DirToClient}, {"realtime.custom.event", DirToClient}, - // Room management (v0.5.0) + // Room management {"room.subscribe", DirFromClient}, {"room.unsubscribe", DirFromClient}, } @@ -227,7 +227,6 @@ func TestToolCallRouteToClient(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 // prevent re-sending to clients (see subscribeToBus in ws.go). if !ShouldAcceptFromClient("tool.result.abc123") { diff --git a/server/events/ticket_adapter.go b/server/events/ticket_adapter.go index 8063983..956bd9e 100644 --- a/server/events/ticket_adapter.go +++ b/server/events/ticket_adapter.go @@ -1,6 +1,5 @@ package events -// v0.32.0: TicketValidatorAdapter bridges the context-aware store.TicketStore // to the middleware.TicketValidator interface (which has no context parameter). // Replaces the in-memory TicketStore that lived in this file previously. diff --git a/server/events/types.go b/server/events/types.go index 5ace1d5..5ade823 100644 --- a/server/events/types.go +++ b/server/events/types.go @@ -9,7 +9,6 @@ type Event struct { Payload json.RawMessage `json:"payload"` 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 // (harmless if clients see it — they ignore unknown fields). TargetUserID string `json:"target_user_id,omitempty"` @@ -38,8 +37,8 @@ var routeTable = map[string]Direction{ // User/presence "user.presence": DirToClient, "user.status": DirToClient, - "user.mentioned": DirToClient, // v0.23.2: targeted @mention notification - "typing.user": DirToClient, // v0.23.2: human typing in DM/channel + "user.mentioned": DirToClient, + "typing.user": DirToClient, // System "system.notify": DirToClient, @@ -48,16 +47,16 @@ var routeTable = map[string]Direction{ // Model/provider status "model.status": DirToClient, - // Role alerts (v0.17.0) + // Role alerts "role.fallback": DirToClient, - // Notifications (v0.20.0) + // Notifications "notification.new": DirToClient, // targeted via Hub.PublishToUser, not room-based "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.assigned": DirToClient, // new assignment → team members "workflow.claimed": DirToClient, // assignment claimed → team + claimer @@ -65,10 +64,10 @@ var routeTable = map[string]Direction{ "workflow.completed": DirToClient, // workflow finished → instance participants "workflow.cancelled": DirToClient, // instance cancelled "workflow.error": DirToClient, // engine/hook error - "workflow.sla_breach": DirToClient, // SLA exceeded → team + admins (v0.3.3) - "workflow.stale": DirToClient, // instance marked stale (v0.3.3) - "workflow.signoff": DirToClient, // signoff submitted (v0.3.4) - "workflow.rejected": DirToClient, // rejection triggered cancel/reroute (v0.3.4) + "workflow.sla_breach": DirToClient, // SLA exceeded → team + admins + "workflow.stale": DirToClient, // instance marked stale + "workflow.signoff": DirToClient, // signoff submitted + "workflow.rejected": DirToClient, // rejection triggered cancel/reroute // Plugin hooks — never cross the wire "plugin.hook.": DirLocal, @@ -77,20 +76,20 @@ var routeTable = map[string]Direction{ // Tool execution (browser tools bridge) "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.loaded": DirLocal, // Client-only "extension.error": DirLocal, - // Trigger system (v0.2.2) + // Trigger system "trigger.fired": DirLocal, // Trigger invocation event (observability) "trigger.error": DirLocal, // Trigger execution error - // Realtime (v0.5.0): extension-published events, room-scoped + // Realtime: extension-published events, room-scoped "realtime.": DirToClient, - // Room management (v0.5.0): client room join/leave + // Room management: client room join/leave "room.subscribe": DirFromClient, "room.unsubscribe": DirFromClient, diff --git a/server/events/ws.go b/server/events/ws.go index 64f1f7f..06c6a56 100644 --- a/server/events/ws.go +++ b/server/events/ws.go @@ -174,7 +174,6 @@ func (h *Hub) IsConnected(userID string) bool { } // 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. func (h *Hub) PublishToUser(userID string, event Event) { event.TargetUserID = userID @@ -238,13 +237,11 @@ func (c *Conn) subscribeToBus() { return } - // v0.32.0: Targeted delivery — only deliver to the intended user. // Targeted events skip room filtering (user-scoped, not room-scoped). if e.TargetUserID != "" && e.TargetUserID != c.userID { return } - // v0.32.0: tool.result travels cross-pod for WaitFor (DirBoth) but // must not be forwarded to WebSocket clients — they sent it. if strings.HasPrefix(e.Label, "tool.result.") { return @@ -316,7 +313,6 @@ func (c *Conn) readPump() { continue } - // v0.5.0: Room management — intercept before bus publish (like ping) if event.Label == "room.subscribe" { var req struct { Room string `json:"room"` diff --git a/server/handlers/admin_connections.go b/server/handlers/admin_connections.go index c14e116..f624613 100644 --- a/server/handlers/admin_connections.go +++ b/server/handlers/admin_connections.go @@ -1,6 +1,5 @@ package handlers -// v0.38.1: Extension connection handlers — admin/global scope. // Methods on AdminHandler, mirrors admin provider config pattern. import ( diff --git a/server/handlers/auth.go b/server/handlers/auth.go index 2b46a46..f407347 100644 --- a/server/handlers/auth.go +++ b/server/handlers/auth.go @@ -345,7 +345,6 @@ func hashToken(token string) string { // - AdminHandler.destroyVault (admin-initiated reset) // // 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) { if err := stores.Users.ClearVaultKeys(ctx, userID); err != nil { 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. // // 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) { vaultSet, encryptedUEK, salt, nonce, err := stores.Users.GetVaultKeys(ctx, userID) 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 // 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 == "" { handle := auth.UniqueHandle(ctx, s.Users, models.HandleFromName(cfg.AdminUsername)) s.Users.Update(ctx, existing.ID, map[string]interface{}{"handle": handle}) diff --git a/server/handlers/connection_resolver.go b/server/handlers/connection_resolver.go index e0ef176..473c889 100644 --- a/server/handlers/connection_resolver.go +++ b/server/handlers/connection_resolver.go @@ -1,6 +1,5 @@ package handlers -// v0.38.1: ConnectionResolver bridges the sandbox connections module // to the store + vault layers. Implements sandbox.ConnectionResolver. import ( diff --git a/server/handlers/connection_types.go b/server/handlers/connection_types.go index 487af54..cfbd1f7 100644 --- a/server/handlers/connection_types.go +++ b/server/handlers/connection_types.go @@ -1,6 +1,5 @@ package handlers -// v0.38.4: Connection type discovery endpoint. // Returns merged connection types from all active packages. // Library declarations take precedence over non-library declarations. diff --git a/server/handlers/connections.go b/server/handlers/connections.go index dc5074b..b8e0fbd 100644 --- a/server/handlers/connections.go +++ b/server/handlers/connections.go @@ -1,6 +1,5 @@ package handlers -// v0.38.1: Extension connection handlers — personal scope + resolution. // Pattern follows apiconfigs.go (ProviderConfigHandler). import ( @@ -28,7 +27,6 @@ func NewConnectionHandler(s store.Stores, vault *crypto.KeyResolver) *Connection // ListConnections returns the user's personal connections. func (h *ConnectionHandler) ListConnections(c *gin.Context) { userID := getUserID(c) - // v0.38.5: Show all accessible connections (personal + team + global) // so users can see which connections are available to them. conns, err := h.stores.Connections.ListAccessible(c.Request.Context(), userID) if err != nil { diff --git a/server/handlers/dependencies.go b/server/handlers/dependencies.go index a1caa71..3497a74 100644 --- a/server/handlers/dependencies.go +++ b/server/handlers/dependencies.go @@ -8,7 +8,7 @@ import ( "switchboard-core/models" ) -// ── Extension Dependencies (v0.38.2) ────────────────── +// ── Extension Dependencies ────────────────── // Admin-only read endpoints for dependency graph visibility. // Dependencies are created/deleted implicitly by InstallPackage/DeletePackage. diff --git a/server/handlers/ext_api.go b/server/handlers/ext_api.go index dd902cd..37d2b2e 100644 --- a/server/handlers/ext_api.go +++ b/server/handlers/ext_api.go @@ -1,6 +1,5 @@ // 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 // /s/:slug/api/*path with JWT auth. // @@ -332,7 +331,7 @@ func hasPermission(granted []string, perm string) bool { return false } -// ── api_schema support (v0.6.2) ───────────────────────────────────── +// ── api_schema support ───────────────────────────────────── // APISchemaEntry represents one route's OpenAPI-level documentation // declared via the optional manifest "api_schema" array. diff --git a/server/handlers/ext_db_schema.go b/server/handlers/ext_db_schema.go index c29fdf4..aadb49a 100644 --- a/server/handlers/ext_db_schema.go +++ b/server/handlers/ext_db_schema.go @@ -1,6 +1,6 @@ package handlers -// ext_db_schema.go — v0.29.2 +// ext_db_schema.go // // DDL generation and lifecycle management for extension-owned database tables. // Tables are namespaced as ext_{pkg_slug}_{logical_name} and tracked in the diff --git a/server/handlers/extension_permissions.go b/server/handlers/extension_permissions.go index 92ae66f..8ae3521 100644 --- a/server/handlers/extension_permissions.go +++ b/server/handlers/extension_permissions.go @@ -10,7 +10,6 @@ import ( ) // ExtPermHandler serves extension permission management endpoints. -// v0.29.0 CS2: Admin reviews and grants/revokes declared permissions. type ExtPermHandler struct { stores store.Stores } diff --git a/server/handlers/extension_secrets.go b/server/handlers/extension_secrets.go index 12c2b6f..0d98b4e 100644 --- a/server/handlers/extension_secrets.go +++ b/server/handlers/extension_secrets.go @@ -10,7 +10,6 @@ import ( ) // 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. // // Secrets are stored in GlobalConfig under key "ext_secrets:{packageID}" diff --git a/server/handlers/extensions.go b/server/handlers/extensions.go index 9145bcf..91fcd96 100644 --- a/server/handlers/extensions.go +++ b/server/handlers/extensions.go @@ -16,7 +16,6 @@ import ( ) // ExtensionHandler serves extension management endpoints. -// v0.28.7: Backed by PackageStore (packages table) instead of ExtensionStore. type ExtensionHandler struct { stores store.Stores } @@ -94,7 +93,6 @@ func (h *ExtensionHandler) UpdateUserExtensionSettings(c *gin.Context) { settings := json.RawMessage("{}") if body.Settings != nil { - // v0.2.0: enforce user_overridable — strip locked keys before saving schema := store.ParseSettingsSchema(pkg.Manifest) if len(schema) > 0 { var incoming map[string]any @@ -229,18 +227,15 @@ func (h *ExtensionHandler) AdminInstallExtension(c *gin.Context) { return } - // v0.29.0: Parse manifest permissions and declare them. // If permissions are declared, package moves to pending_review. 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 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) } } - // v0.2.2: Sync triggers from manifest SyncManifestTriggers(c.Request.Context(), h.stores, triggers.GlobalEngine(), pkg.ID, manifestMap) c.JSON(201, gin.H{"data": pkg}) @@ -403,7 +398,6 @@ func (h *ExtensionHandler) AdminUninstallExtension(c *gin.Context) { 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 { log.Printf("[ext-db] schema drop failed for %s: %v", id, err) } diff --git a/server/handlers/groups.go b/server/handlers/groups.go index 4b7c9eb..27bc7db 100644 --- a/server/handlers/groups.go +++ b/server/handlers/groups.go @@ -243,7 +243,7 @@ func (h *GroupHandler) AddMember(c *gin.Context) { return } - // Notify the added user (v0.20.0) + // Notify the added user if svc := notifications.Default(); svc != nil { notifications.NotifyGroupMemberAdded(svc, req.UserID, groupID, group.Name) } @@ -273,7 +273,7 @@ func (h *GroupHandler) RemoveMember(c *gin.Context) { return } - // Notify the removed user (v0.20.0) + // Notify the removed user if svc := notifications.Default(); svc != nil && group != nil { notifications.NotifyGroupMemberRemoved(svc, userID, groupID, group.Name) } diff --git a/server/handlers/notification_test.go b/server/handlers/notification_test.go index 55e6b94..242f4a3 100644 --- a/server/handlers/notification_test.go +++ b/server/handlers/notification_test.go @@ -62,7 +62,7 @@ func setupNotifHarness(t *testing.T) *notifHarness { protected.PUT("/notifications/preferences/:type", notifH.SetPreference) protected.DELETE("/notifications/preferences/:type", notifH.DeletePreference) - // Admin broadcast route (v0.28.6) + // Admin broadcast route admin := api.Group("/admin") admin.Use(middleware.Auth(cfg, stores.Users, userCache)) 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) { h := setupNotifHarness(t) diff --git a/server/handlers/notifications.go b/server/handlers/notifications.go index 0956fac..be84441 100644 --- a/server/handlers/notifications.go +++ b/server/handlers/notifications.go @@ -175,7 +175,7 @@ func (h *NotificationHandler) Delete(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"ok": true}) } -// ── Notification Preferences (v0.20.0 Phase 3) ── +// ── Notification Preferences ── // GET /api/v1/notifications/preferences 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}) } -// ── Admin Broadcast (v0.28.6) ─────────────── +// ── Admin Broadcast ─────────────── // POST /api/v1/admin/notifications/broadcast // Admin-only: sends a system.announcement notification to all active users. diff --git a/server/handlers/openapi.go b/server/handlers/openapi.go index 50b4cb6..068e290 100644 --- a/server/handlers/openapi.go +++ b/server/handlers/openapi.go @@ -1,6 +1,5 @@ // 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 // path items; others get auto-generated stubs. // diff --git a/server/handlers/package_registry.go b/server/handlers/package_registry.go index 8a941ec..58480e1 100644 --- a/server/handlers/package_registry.go +++ b/server/handlers/package_registry.go @@ -1,6 +1,6 @@ package handlers -// package_registry.go — v0.30.0 CS4 +// package_registry.go // // Package registry (marketplace discovery). Admin browses available // packages from an external JSON registry and installs them by URL. diff --git a/server/handlers/packages.go b/server/handlers/packages.go index 6850b85..b787cdc 100644 --- a/server/handlers/packages.go +++ b/server/handlers/packages.go @@ -27,12 +27,12 @@ import ( var validPackageID = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$`) // PackageHandler manages unified package lifecycle (admin-only). -// Replaces SurfaceHandler (v0.28.7). +// Replaces SurfaceHandler. type PackageHandler struct { stores store.Stores packagesDir string // e.g. /data/packages — where archives are extracted - sandbox *sandbox.Sandbox // v0.30.0: for schema migration scripts - runner *sandbox.Runner // v0.38.2: for test-tool execution + sandbox *sandbox.Sandbox + runner *sandbox.Runner } 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} } -// 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) { 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) { h.runner = r } @@ -95,7 +95,6 @@ func (h *PackageHandler) GetPackage(c *gin.Context) { func (h *PackageHandler) EnablePackage(c *gin.Context) { id := c.Param("id") - // v0.3.7: Block enabling dormant packages (unmet requires). pkg, err := h.stores.Packages.Get(c.Request.Context(), id) if err != nil || pkg == nil { c.JSON(http.StatusNotFound, gin.H{"error": "package not found"}) @@ -149,7 +148,6 @@ func (h *PackageHandler) DeletePackage(c *gin.Context) { return } - // v0.38.2: Libraries with active consumers cannot be uninstalled. if pkg.Type == "library" && h.stores.Dependencies != nil { hasConsumers, _ := h.stores.Dependencies.HasConsumers(c.Request.Context(), id) 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 { 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 { 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 cleanupTmp bool - // v0.30.0: Check for pre-downloaded file from registry install if regFile, ok := c.Get("_registry_file"); ok { tmpPath = regFile.(string) // Don't remove — caller manages lifecycle @@ -278,7 +273,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) { return } - // v0.38.0: Entry point validation for starlark-tier packages. // Scripts are loaded from disk at runtime — no _starlark_script injection. if manifestTier, _ := manifest["tier"].(string); manifestTier == "starlark" { 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"}) 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 // (e.g. editor: browser-tier surface + admin-configurable settings). if !hasExtBehavior && !hasSettings { @@ -393,7 +386,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) { return } case "library": - // v0.38.2: Libraries must declare exports, cannot have tools/pipes/route. exports, _ := manifest["exports"].([]any) if len(exports) == 0 { 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") - // v0.30.0: use registry source if set by registry install handler pkgSource := "extension" if src, ok := c.Get("_registry_source"); ok { 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) } - // v0.29.2: Create namespaced DB tables declared in the manifest. if tables, ok := ParseDBTables(manifest); ok { 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) } } - // v0.38.5: Declare manifest permissions (same as AdminInstallExtension). // This creates the permission rows and sets status to pending_review // if the package declares permissions. SyncManifestPermissions(c, h.stores, pkgID, manifest) - // v0.2.2: Sync triggers from manifest SyncManifestTriggers(c.Request.Context(), h.stores, triggers.GlobalEngine(), pkgID, manifest) - // v0.30.0: Run schema migrations if declared. newSchemaVersion := ParseSchemaVersion(manifest) if newSchemaVersion > 0 && h.sandbox != nil { 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 err := InstallWorkflowFromManifest(c, h.stores, pkgID, manifest); err != nil { 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. if deps, ok := manifest["dependencies"].(map[string]any); ok && len(deps) > 0 { // 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)) } - // v0.2.1: Auto-set default_surface when the first extension surface is installed. if (pkgType == "surface" || pkgType == "full") && pkgSource != "core" { if raw, err := h.stores.GlobalConfig.Get(c.Request.Context(), "default_surface"); err != nil || raw == nil { 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). knownCaps := map[string]bool{} var unmetReqs []string @@ -689,7 +672,7 @@ func extractableRelPath(name string) string { return "" } -// ── Package settings (v0.30.0 CS2) ────────────── +// ── Package settings ────────────── // GetPackageSettings returns the settings schema from the manifest merged // with current admin-configured values. @@ -823,7 +806,7 @@ func (h *PackageHandler) ListEnabledSurfaces(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": enabled}) } -// ── Test Tool (v0.38.2) ───────────────────────── +// ── Test Tool ───────────────────────── // POST /admin/packages/:id/test-tool // Invokes a starlark extension's on_tool_call entry point directly, // 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. // POST /api/v1/admin/packages/:id/update @@ -1175,7 +1158,7 @@ func mergePackageSettings(existing *store.PackageRegistration, manifest map[stri return data } -// ── Package export (v0.5.4) ────────────────────── +// ── Package export ────────────────────── // ExportPackage exports an installed package as a downloadable .pkg archive. // GET /api/v1/admin/packages/:id/export diff --git a/server/handlers/packages_bundled.go b/server/handlers/packages_bundled.go index 306f556..43781b2 100644 --- a/server/handlers/packages_bundled.go +++ b/server/handlers/packages_bundled.go @@ -23,23 +23,16 @@ import ( ) // 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 // to be set explicitly (or "*" for all). // -// Recommended production override (lean): -// BUNDLED_PACKAGES=notes,chat,chat-core,cluster-dashboard +// Recommended production override: BUNDLED_PACKAGES=notes,chat,chat-core,mermaid-renderer,schedules var defaultBundledPackages = map[string]bool{ - "notes": true, - "chat": true, - "chat-core": true, - "workflow-chat": true, - "dashboard": true, - "cluster-dashboard": true, - "workflow-demo": true, - "bug-report-triage": true, - "content-approval": true, - "employee-onboarding": true, + "notes": true, + "chat": true, + "chat-core": true, + "mermaid-renderer": true, + "schedules": true, } // InstallBundledPackages scans bundledDir for .pkg archives and installs diff --git a/server/handlers/presence.go b/server/handlers/presence.go index a519f5a..382e556 100644 --- a/server/handlers/presence.go +++ b/server/handlers/presence.go @@ -1,12 +1,11 @@ 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. // GET /api/v1/presence?users=id1,id2 returns current status. // Online = last_seen within 90s. // -// v0.29.0: Raw SQL replaced with PresenceStore + UserStore methods. import ( "net/http" diff --git a/server/handlers/profile_bootstrap.go b/server/handlers/profile_bootstrap.go index 85b2e0d..9e3e3dc 100644 --- a/server/handlers/profile_bootstrap.go +++ b/server/handlers/profile_bootstrap.go @@ -8,7 +8,6 @@ package handlers // (profile, permissions, teams/mine, settings) into one call. // The SDK calls this at startup and on token refresh. // -// v0.37.15 import ( "net/http" diff --git a/server/handlers/safe_json.go b/server/handlers/safe_json.go index 6f26b8c..6110d8d 100644 --- a/server/handlers/safe_json.go +++ b/server/handlers/safe_json.go @@ -51,7 +51,7 @@ func (s *jsonScanner) Scan(src interface{}) error { // memory on the next rows.Scan() call, corrupting our data after the // fact. This caused intermittent SafeJSON 500s on paginated list // 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)) copy(cp, v) *s.dest = json.RawMessage(cp) diff --git a/server/handlers/safe_json_test.go b/server/handlers/safe_json_test.go index d7bcff2..688cec5 100644 --- a/server/handlers/safe_json_test.go +++ b/server/handlers/safe_json_test.go @@ -185,7 +185,7 @@ func TestSafeJSON_PaginatedWithCorrupt(t *testing.T) { } // ── 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, // rows.Next() can overwrite the buffer and corrupt previously-scanned // json.RawMessage values in a paginated list. diff --git a/server/handlers/settings.go b/server/handlers/settings.go index 9a82ef1..1fdd8c2 100644 --- a/server/handlers/settings.go +++ b/server/handlers/settings.go @@ -2,7 +2,6 @@ package handlers // settings.go — User profile, preferences, and password management. // -// v0.29.0: Raw SQL replaced with UserStore methods. import ( "context" diff --git a/server/handlers/team_package_settings.go b/server/handlers/team_package_settings.go index 18032aa..3d63e4c 100644 --- a/server/handlers/team_package_settings.go +++ b/server/handlers/team_package_settings.go @@ -1,6 +1,6 @@ package handlers -// team_package_settings.go — v0.2.0 +// team_package_settings.go // // Team-admin endpoints for managing team-scoped package settings. // These sit in the settings cascade between global (admin) and user. diff --git a/server/handlers/teams.go b/server/handlers/teams.go index b8d3dbe..41e0a0d 100644 --- a/server/handlers/teams.go +++ b/server/handlers/teams.go @@ -419,7 +419,7 @@ func (h *TeamHandler) ListTeamAuditActions(c *gin.Context) { 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. var builtinRoles = []string{"admin", "member"} diff --git a/server/handlers/upgrade_test.go b/server/handlers/upgrade_test.go index 7d1c1ea..1226168 100644 --- a/server/handlers/upgrade_test.go +++ b/server/handlers/upgrade_test.go @@ -14,7 +14,7 @@ import ( ) // ═══════════════════════════════════════════════ -// Upgrade Tests — v0.5.5 +// Upgrade Tests // // Schema edge cases, settings migration, and // package compatibility across kernel upgrades. diff --git a/server/handlers/user_packages.go b/server/handlers/user_packages.go index f48bbc7..94b625d 100644 --- a/server/handlers/user_packages.go +++ b/server/handlers/user_packages.go @@ -1,6 +1,6 @@ package handlers -// user_packages.go — v0.30.0 CS5 +// user_packages.go // // Non-admin package management. Users install personal-scoped packages; // team admins install team-scoped packages. diff --git a/server/handlers/workflow_packages.go b/server/handlers/workflow_packages.go index 0bff0ee..a65643a 100644 --- a/server/handlers/workflow_packages.go +++ b/server/handlers/workflow_packages.go @@ -1,6 +1,6 @@ package handlers -// workflow_packages.go — v0.30.2 CS1 +// workflow_packages.go // // Handles workflow-specific package operations: // - ExportWorkflowPackage: bundles a workflow definition + stages into a .pkg diff --git a/server/handlers/workflow_public_handlers.go b/server/handlers/workflow_public_handlers.go index 180c3bc..0a65150 100644 --- a/server/handlers/workflow_public_handlers.go +++ b/server/handlers/workflow_public_handlers.go @@ -11,7 +11,7 @@ import ( "switchboard-core/workflow" ) -// ── Public Workflow Handlers (v0.3.3) ─────── +// ── Public Workflow Handlers ─────── // WorkflowPublicHandler manages unauthenticated workflow entry endpoints. type WorkflowPublicHandler struct { diff --git a/server/handlers/workflow_signoff_handlers.go b/server/handlers/workflow_signoff_handlers.go index fcbb22c..14dfc92 100644 --- a/server/handlers/workflow_signoff_handlers.go +++ b/server/handlers/workflow_signoff_handlers.go @@ -10,7 +10,7 @@ import ( "switchboard-core/workflow" ) -// ── Signoff Handlers (v0.3.4) ───────────────── +// ── Signoff Handlers ───────────────── // WorkflowSignoffHandler manages signoff HTTP endpoints. type WorkflowSignoffHandler struct { diff --git a/server/handlers/workflow_store_test.go b/server/handlers/workflow_store_test.go index 84e2f6a..ad0cd39 100644 --- a/server/handlers/workflow_store_test.go +++ b/server/handlers/workflow_store_test.go @@ -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) { 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) { 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) { database.RequireTestDB(t) diff --git a/server/handlers/workflow_team.go b/server/handlers/workflow_team.go index 070eb66..b7d00ed 100644 --- a/server/handlers/workflow_team.go +++ b/server/handlers/workflow_team.go @@ -9,7 +9,7 @@ import ( "switchboard-core/models" ) -// ── Team-Scoped Workflow Wrappers (v0.31.2) ──────────────── +// ── Team-Scoped Workflow Wrappers ──────────────── // // Thin wrappers that enforce team ownership before delegating // to the existing WorkflowHandler methods. Follows the same diff --git a/server/handlers/workflows.go b/server/handlers/workflows.go index c90a17c..dc2a598 100644 --- a/server/handlers/workflows.go +++ b/server/handlers/workflows.go @@ -106,7 +106,6 @@ func (h *WorkflowHandler) Create(c *gin.Context) { 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 { tid := ftid.(string) w.TeamID = &tid @@ -417,7 +416,6 @@ func (h *WorkflowHandler) Clone(c *gin.Context) { CreatedBy: c.GetString("user_id"), } - // v0.31.2: team-scoped route injects force_team_id if ftid, ok := c.Get("force_team_id"); ok { tid := ftid.(string) clone.TeamID = &tid diff --git a/server/main.go b/server/main.go index cfe5673..455d477 100644 --- a/server/main.go +++ b/server/main.go @@ -36,7 +36,6 @@ import ( sqliteStore "switchboard-core/store/sqlite" ) -// v0.33.0: Embedded OpenAPI spec and Swagger UI for /api/docs. // //go:embed static/openapi.yaml var openapiSpec []byte @@ -59,13 +58,10 @@ func main() { // ── Server startup ────────────────────── 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. logging.Init(cfg.LogFormat, cfg.LogLevel) - - // v0.29.1: Config-file provider types (Ollama, LiteLLM, vLLM, etc.) - var stores store.Stores uekCache := crypto.NewUEKCache() var keyResolver *crypto.KeyResolver @@ -109,7 +105,6 @@ func main() { stores = postgres.NewStores(database.DB) } - // v0.33.0: Start Prometheus DB pool collector metrics.StartDBCollector(database.DB, 15*time.Second) // Bootstrap admin from env (K8s secret) — upserts on every restart @@ -161,23 +156,18 @@ func main() { sandbox.New(sandbox.DefaultConfig()), stores, ) - // v0.38.1: connections module — extension connection resolution starlarkRunner.SetConnectionResolver(handlers.NewConnectionResolverAdapter(stores, keyResolver)) - // v0.29.2: db module — extension namespaced table access starlarkRunner.SetDB(database.DB, database.IsPostgres()) - // v0.38.0: disk-based script loading + load() support if cfg.StoragePath != "" { starlarkRunner.SetPackagesDir(cfg.StoragePath + "/packages") } - // v0.5.0: realtime module — extension publish to WebSocket channels starlarkRunner.SetBus(bus) - // v0.38.5: allow extensions to reach private IPs (self-hosted Gitea, etc.) if os.Getenv("EXT_ALLOW_PRIVATE_IPS") == "true" { starlarkRunner.SetAllowPrivateIPs(true) log.Printf(" ⚠️ Extension SSRF protection relaxed: private IPs allowed") } - // ── Bundled Packages (v0.3.8) ─────────────── + // ── Bundled Packages ─────────────── // Auto-install bundled .pkg archives on first run. // Skipped if SKIP_BUNDLED_PACKAGES=true or packages already exist. if !cfg.SkipBundledPackages && stores.Packages != nil { @@ -188,7 +178,7 @@ func main() { handlers.InstallBundledPackages(cfg.BundledPackagesDir, bundledPkgDir, cfg.BundledPackages, stores, starlarkRunner) } - // ── Trigger Engine (v0.2.2) ───────────────── + // ── Trigger Engine ───────────────── triggerEngine := triggers.New(stores, starlarkRunner, bus) if err := triggerEngine.Start(context.Background()); err != nil { log.Printf(" ⚠️ Trigger engine start: %v", err) @@ -210,7 +200,7 @@ func main() { // ── WebSocket Hub ───────────────────────── hub := events.NewHub(bus, middleware.GetAllowedOrigins(cfg)) - // ── Cluster Registry (v0.6.0) ──────────── + // ── Cluster Registry ──────────── // PG-backed node self-registration + heartbeat. No-op on SQLite. var clusterReg *cluster.Registry 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} - // ── Notification Service (v0.20.0) ─────── + // ── Notification Service ─────── var notifSvc *notifications.Service if stores.Notifications != nil { notifSvc = notifications.NewService(stores.Notifications, hub). @@ -277,7 +267,6 @@ func main() { c.JSON(200, info) }) - // v0.32.0: Kubernetes probe endpoints // Liveness: process is alive and serving (no dependency checks). base.GET("/healthz/live", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) @@ -299,10 +288,8 @@ func main() { 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())) - // v0.33.0: OpenAPI spec + Swagger UI (no auth — documentation) base.GET("/api/docs", func(c *gin.Context) { 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) 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) { spec, err := handlers.BuildOpenAPISpec(stores, openapiSpec, Version) if err != nil { @@ -401,7 +387,7 @@ func main() { extH := handlers.NewExtensionHandler(stores) api.GET("/extensions/:id/assets/*path", extH.ServeExtensionAsset) - // ── Webhook Inbound (v0.2.2) ────────────── + // ── Webhook Inbound ────────────── // Public routes — HMAC-verified in handler, no auth middleware. api.POST("/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) ── wfEngine := workflow.NewEngine(stores, bus, starlarkRunner) - // ── Public Workflow Entry (v0.3.3) ───── + // ── Public Workflow Entry ───── publicWfH := handlers.NewWorkflowPublicHandler(wfEngine, stores) publicWf := api.Group("/public/workflows") publicWf.Use(authLimiter.Limit()) @@ -419,7 +405,7 @@ func main() { publicWf.POST("/advance/:token", publicWfH.AdvancePublic) } - // ── Workflow Scanner (v0.3.3) ────────── + // ── Workflow Scanner ────────── wfScanner := workflow.NewScanner(stores, bus) wfScanner.Start() defer wfScanner.Stop() @@ -429,7 +415,7 @@ func main() { protected.Use(middleware.Auth(cfg, stores.Users, userCache)) protected.Use(middleware.ValidatePathParams()) { - // ── WebSocket Ticket (v0.28.8) ─────────── + // ── WebSocket Ticket ─────────── // Issue a single-use ticket for WebSocket auth. // Client fetches this, then connects with ?ticket=. protected.POST("/ws/ticket", func(c *gin.Context) { @@ -442,15 +428,15 @@ func main() { c.JSON(http.StatusOK, gin.H{"ticket": ticket}) }) - // Presence (v0.23.1) + // Presence presence := handlers.NewPresenceHandler(stores) protected.POST("/presence/heartbeat", presence.Heartbeat) protected.GET("/presence", presence.Query) - // User search (v0.23.2 — DM user picker) + // User search protected.GET("/users/search", presence.SearchUsers) - // Workflows (v0.26.1 — team-owned staged processes) + // Workflows wfH := handlers.NewWorkflowHandler(stores) protected.GET("/workflows", wfH.List) 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.GET("/workflows/:id/versions/:version", wfH.GetVersion) - // Workflow instances + assignments (v0.3.2) + // Workflow instances + assignments wfInstH := handlers.NewWorkflowInstanceHandler(wfEngine, stores) protected.POST("/workflows/:id/instances", middleware.RequirePermission(auth.PermWorkflowSubmit, stores), wfInstH.Start) 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.GET("/assignments/mine", wfAssignH.ListMine) - // Workflow signoffs (v0.3.4) + // Workflow signoffs wfSignoffH := handlers.NewWorkflowSignoffHandler(wfEngine, stores) protected.POST("/instances/:iid/signoffs", wfSignoffH.Submit) protected.GET("/instances/:iid/signoffs", wfSignoffH.List) - // Surface discovery (v0.25.0, v0.28.7: unified packages) + // Surface discovery pkgH := handlers.NewPackageHandler(stores) protected.GET("/surfaces", pkgH.ListEnabledSurfaces) - // User package management (v0.30.0) + // User package management userPkgDir := "" if cfg.StoragePath != "" { userPkgDir = cfg.StoragePath + "/packages" @@ -500,11 +486,11 @@ func main() { protected.POST("/packages/install", userPkgH.InstallPersonalPackage) protected.DELETE("/packages/:id", userPkgH.DeletePersonalPackage) - // Connection Type Discovery (v0.38.4) + // Connection Type Discovery connTypeH := handlers.NewConnectionTypeHandler(stores) protected.GET("/connection-types", connTypeH.ListConnectionTypes) - // Extension Connections (personal scope — v0.38.1) + // Extension Connections (personal scope connH := handlers.NewConnectionHandler(stores, keyResolver) protected.GET("/connections", connH.ListConnections) protected.POST("/connections", connH.CreateConnection) @@ -521,21 +507,21 @@ func main() { protected.GET("/settings", settings.GetSettings) protected.PUT("/settings", settings.UpdateSettings) - // Documentation (v0.6.1) + // Documentation docsDir := findDocsDir() docsH := handlers.NewDocsHandler(docsDir) protected.GET("/docs", docsH.ListDocs) 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) 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) protected.GET("/profile/bootstrap", bootH.GetBootstrap) - // Notifications (v0.20.0) + // Notifications notifH := handlers.NewNotificationHandler(stores, hub) protected.GET("/notifications", notifH.List) protected.GET("/notifications/unread-count", notifH.UnreadCount) @@ -543,12 +529,12 @@ func main() { protected.POST("/notifications/mark-all-read", notifH.MarkAllRead) protected.DELETE("/notifications/:id", notifH.Delete) - // Notification preferences (v0.20.0 Phase 3) + // Notification preferences protected.GET("/notifications/preferences", notifH.ListPreferences) protected.PUT("/notifications/preferences/:type", notifH.SetPreference) protected.DELETE("/notifications/preferences/:type", notifH.DeletePreference) - // ── Scheduled Tasks — User (v0.2.2) ──── + // ── Scheduled Tasks — User ──── userSchedH := handlers.NewScheduleHandler(stores, triggerEngine) protected.GET("/schedules", userSchedH.ListMySchedules) protected.POST("/schedules", userSchedH.CreateSchedule) @@ -562,7 +548,7 @@ func main() { teams := handlers.NewTeamHandler(stores, keyResolver) protected.GET("/teams/mine", teams.MyTeams) - // Groups (user: my groups — v0.16.0) + // Groups (user: my groups groupH := handlers.NewGroupHandler(stores) protected.GET("/groups/mine", groupH.MyGroups) @@ -580,18 +566,18 @@ func main() { // Team groups (team admins manage team-scoped groups) teamScoped.GET("/groups", groupH.ListTeamGroups) - // Team connections (v0.38.1) + // Team connections teamScoped.GET("/connections", teams.ListTeamConnections) teamScoped.POST("/connections", teams.CreateTeamConnection) teamScoped.PUT("/connections/:id", teams.UpdateTeamConnection) teamScoped.DELETE("/connections/:id", teams.DeleteTeamConnection) - // Team package management (v0.30.0) + // Team package management teamPkgH := handlers.NewUserPackageHandler(stores, userPkgDir) teamScoped.POST("/packages/install", teamPkgH.InstallTeamPackage) teamScoped.DELETE("/packages/:id", teamPkgH.DeleteTeamPackage) - // Team package settings — cascade overrides (v0.2.0) + // Team package settings — cascade overrides teamPkgSettingsH := handlers.NewTeamPackageSettingsHandler(stores) teamScoped.GET("/packages/:id/settings", teamPkgSettingsH.GetTeamPackageSettings) teamScoped.PUT("/packages/:id/settings", teamPkgSettingsH.UpdateTeamPackageSettings) @@ -601,7 +587,7 @@ func main() { teamScoped.GET("/audit", teams.ListTeamAuditLog) teamScoped.GET("/audit/actions", teams.ListTeamAuditActions) - // Team workflows — self-service (v0.31.2) + // Team workflows — self-service teamWfH := handlers.NewWorkflowHandler(stores) teamScoped.GET("/workflows", teamWfH.ListTeamWorkflows) teamScoped.POST("/workflows", teamWfH.CreateTeamWorkflow) @@ -619,7 +605,7 @@ func main() { teamScoped.GET("/workflows/available", teamWfH.ListGlobalWorkflows) teamScoped.GET("/workflows/:id/versions/:version", teamWfH.GetTeamWorkflowVersion) - // Team workflow instances + assignments (v0.3.2) + // Team workflow instances + assignments teamWfInstH := handlers.NewWorkflowInstanceHandler(wfEngine, stores) teamScoped.POST("/workflows/:id/instances", teamWfInstH.Start) teamScoped.GET("/workflows/:id/instances", teamWfInstH.ListInstances) @@ -630,7 +616,7 @@ func main() { teamWfAssignH := handlers.NewWorkflowAssignmentHandler(wfEngine, stores) teamScoped.GET("/assignments", teamWfAssignH.ListByTeam) - // Team workflow signoffs (v0.3.4) + // Team workflow signoffs teamWfSignoffH := handlers.NewWorkflowSignoffHandler(wfEngine, stores) teamScoped.POST("/instances/:iid/signoffs", teamWfSignoffH.Submit) teamScoped.GET("/instances/:iid/signoffs", teamWfSignoffH.List) @@ -674,7 +660,7 @@ func main() { // Stats admin.GET("/stats", adm.GetStats) - // Global Connections (v0.38.1) + // Global Connections admin.GET("/connections", adm.ListGlobalConnections) admin.POST("/connections", adm.CreateGlobalConnection) admin.PUT("/connections/:id", adm.UpdateGlobalConnection) @@ -692,7 +678,7 @@ func main() { admin.PUT("/teams/:id/members/:memberId", teamAdm.UpdateMember) admin.DELETE("/teams/:id/members/:memberId", teamAdm.RemoveMember) - // Admin broadcast (v0.28.6) + // Admin broadcast adminNotifH := handlers.NewNotificationHandler(stores, hub) admin.POST("/notifications/broadcast", adminNotifH.Broadcast) @@ -700,7 +686,7 @@ func main() { admin.GET("/audit", adm.ListAuditLog) admin.GET("/audit/actions", adm.ListAuditActions) - // Groups (admin — v0.16.0) + // Groups (admin groupAdm := handlers.NewGroupHandler(stores) admin.GET("/groups", groupAdm.ListGroups) admin.POST("/groups", groupAdm.CreateGroup) @@ -711,11 +697,11 @@ func main() { admin.POST("/groups/:id/members", groupAdm.AddMember) admin.DELETE("/groups/:id/members/:userId", groupAdm.RemoveMember) - // Permissions (v0.24.2) + // Permissions admin.GET("/permissions", groupAdm.ListPermissions) admin.GET("/users/:id/permissions", groupAdm.GetUserPermissions) - // Resource Grants (admin — v0.16.0) + // Resource Grants (admin admin.GET("/grants/:type/:id", groupAdm.GetResourceGrant) admin.PUT("/grants/:type/:id", groupAdm.SetResourceGrant) @@ -728,7 +714,7 @@ func main() { // Vault admin.GET("/vault/status", adm.VaultStatus) - // Email / SMTP test (v0.20.0 Phase 3) + // Email / SMTP test emailAdm := handlers.NewAdminEmailHandler(stores) admin.POST("/notifications/test-email", emailAdm.TestEmail) @@ -739,7 +725,7 @@ func main() { admin.PUT("/extensions/:id", extAdm.AdminUpdateExtension) admin.DELETE("/extensions/:id", extAdm.AdminUninstallExtension) - // Extension permissions (admin — v0.29.0) + // Extension permissions (admin extPermH := handlers.NewExtPermHandler(stores) admin.GET("/extensions/:id/permissions", extPermH.ListPackagePermissions) 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/grant-all", extPermH.GrantAllPermissions) - // Extension secrets (admin — v0.29.0 CS3) + // Extension secrets (admin extSecH := handlers.NewExtSecretsHandler(stores) admin.GET("/extensions/:id/secrets", extSecH.GetSecrets) admin.PUT("/extensions/:id/secrets", extSecH.SetSecrets) admin.DELETE("/extensions/:id/secrets", extSecH.DeleteSecrets) - // Package lifecycle management (v0.28.7 — replaces surfaces + extensions admin) + // Package lifecycle management packagesDir := "" if cfg.StoragePath != "" { packagesDir = cfg.StoragePath + "/packages" } 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) admin.GET("/packages/registry", registryH.BrowseRegistry) admin.POST("/packages/registry/install", registryH.InstallFromRegistry) @@ -772,20 +758,20 @@ func main() { admin.PUT("/packages/:id/enable", pkgAdm.EnablePackage) admin.PUT("/packages/:id/disable", pkgAdm.DisablePackage) admin.DELETE("/packages/:id", pkgAdm.DeletePackage) - admin.GET("/packages/:id/settings", pkgAdm.GetPackageSettings) // v0.30.0 - admin.PUT("/packages/:id/settings", pkgAdm.UpdatePackageSettings) // v0.30.0 - admin.GET("/packages/:id/dependencies", pkgAdm.ListDependencies) // v0.38.2 - admin.GET("/packages/:id/consumers", pkgAdm.ListConsumers) // v0.38.2 - admin.POST("/packages/:id/test-tool", pkgAdm.TestTool) // v0.38.2 - admin.POST("/packages/:id/update", pkgAdm.UpdatePackage) // v0.5.4 - admin.GET("/packages/:id/export", pkgAdm.ExportPackage) // v0.5.4 - admin.GET("/dependencies", pkgAdm.ListAllDependencies) // v0.38.2 + admin.GET("/packages/:id/settings", pkgAdm.GetPackageSettings) + admin.PUT("/packages/:id/settings", pkgAdm.UpdatePackageSettings) + admin.GET("/packages/:id/dependencies", pkgAdm.ListDependencies) + admin.GET("/packages/:id/consumers", pkgAdm.ListConsumers) + admin.POST("/packages/:id/test-tool", pkgAdm.TestTool) + admin.POST("/packages/:id/update", pkgAdm.UpdatePackage) + admin.GET("/packages/:id/export", pkgAdm.ExportPackage) + admin.GET("/dependencies", pkgAdm.ListAllDependencies) - // Workflow package export (v0.30.2) + // Workflow package export wfPkgH := handlers.NewWorkflowPackageHandler(stores) admin.GET("/workflows/:id/export", wfPkgH.ExportWorkflowPackage) - // ── Triggers (v0.2.2) ───────────────── + // ── Triggers ───────────────── trigH := handlers.NewTriggerHandler(stores, triggerEngine) admin.GET("/triggers", trigH.ListTriggers) admin.GET("/triggers/:id", trigH.GetTrigger) @@ -795,20 +781,20 @@ func main() { admin.GET("/triggers/:id/logs", trigH.ListTriggerLogs) admin.GET("/packages/:id/triggers", trigH.ListPackageTriggers) - // ── Scheduled Tasks — Admin (v0.2.2) ── + // ── Scheduled Tasks — Admin ── schedH := handlers.NewScheduleHandler(stores, triggerEngine) admin.GET("/schedules", schedH.AdminListSchedules) admin.PUT("/schedules/:id/enable", schedH.AdminEnableSchedule) admin.PUT("/schedules/:id/disable", schedH.AdminDisableSchedule) admin.DELETE("/schedules/:id", schedH.AdminDeleteSchedule) - // ── Cluster (v0.6.0) ───────────────── + // ── Cluster ───────────────── if stores.Cluster != nil { clusterH := handlers.NewClusterHandler(stores) admin.GET("/cluster", clusterH.ListNodes) } - // ── Backup/Restore (v0.6.1) ───────── + // ── Backup/Restore ───────── backupH := handlers.NewBackupHandler(stores, packagesDir, cfg.StoragePath) admin.POST("/backup", backupH.CreateBackup) admin.GET("/backups", backupH.ListBackups) @@ -828,9 +814,7 @@ func main() { } // ── 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). - // 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. if cfg.StoragePath != "" { packagesStaticDir := cfg.StoragePath + "/packages" @@ -850,13 +834,12 @@ func main() { pages.SetVersion(Version) pageEngine := pages.New(cfg, stores) - // Root redirect → configurable default surface (v0.2.1) + // Root redirect → configurable default surface base.GET("/", pageEngine.DefaultSurfaceRedirect()) // Login page — no auth required base.GET("/login", pageEngine.RenderLogin()) - // v0.25.0: Surface routes generated from manifest registry. // Replaces manual per-surface route blocks. pageEngine.RegisterPageRoutes(base, pages.PageRouteMiddleware{ Authenticated: middleware.AuthOrRedirect(cfg, stores.Users, userCache), @@ -864,10 +847,9 @@ func main() { middleware.AuthOrRedirect(cfg, stores.Users, userCache), 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). { extAPIH := handlers.NewExtAPIHandler(stores, starlarkRunner) diff --git a/server/middleware/auth.go b/server/middleware/auth.go index 2512a2c..d98cd6c 100644 --- a/server/middleware/auth.go +++ b/server/middleware/auth.go @@ -290,7 +290,7 @@ type TicketValidator interface { // WsAuth returns a Gin middleware for the WebSocket endpoint. // It authenticates via three methods in priority order: // -// 1. ?ticket= — single-use ticket (preferred, v0.28.8+) +// 1. ?ticket= — single-use ticket (preferred) // 2. ?token= — legacy JWT in query param (deprecated) // 3. Authorization header — standard Bearer JWT // diff --git a/server/middleware/ratelimit.go b/server/middleware/ratelimit.go index 01f3c8c..7c6f6b4 100644 --- a/server/middleware/ratelimit.go +++ b/server/middleware/ratelimit.go @@ -10,7 +10,6 @@ import ( ) // 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. // Auth endpoints have their own protections (bcrypt, lockout); blocking diff --git a/server/middleware/team.go b/server/middleware/team.go index 0e54ae6..eee502f 100644 --- a/server/middleware/team.go +++ b/server/middleware/team.go @@ -10,7 +10,6 @@ import ( // RequireTeamAdmin returns middleware that restricts access to team admins. // System admins are always allowed through. -// v0.29.0: accepts TeamStore instead of using database.DB directly. func RequireTeamAdmin(teams store.TeamStore) gin.HandlerFunc { return func(c *gin.Context) { role, _ := c.Get("role") @@ -43,7 +42,6 @@ func RequireTeamAdmin(teams store.TeamStore) gin.HandlerFunc { // RequireTeamMember returns middleware that restricts access to users who // belong to the team identified by :teamId (any role). // System admins are always allowed through. -// v0.29.0: accepts TeamStore instead of using database.DB directly. func RequireTeamMember(teams store.TeamStore) gin.HandlerFunc { return func(c *gin.Context) { role, _ := c.Get("role") diff --git a/server/models/ext_connection.go b/server/models/ext_connection.go index a63e3eb..ad06151 100644 --- a/server/models/ext_connection.go +++ b/server/models/ext_connection.go @@ -1,6 +1,6 @@ package models -// ── Extension Connections (v0.38.1) ────────────────── +// ── Extension Connections ────────────────── // ExtConnection represents a scoped credential/endpoint configuration // for extensions that integrate with external services. Same scope diff --git a/server/models/ext_dependency.go b/server/models/ext_dependency.go index 214b6bb..63a113d 100644 --- a/server/models/ext_dependency.go +++ b/server/models/ext_dependency.go @@ -1,6 +1,6 @@ package models -// ── Extension Dependencies (v0.38.2) ────────────────── +// ── Extension Dependencies ────────────────── // ExtDependency records that a consumer package depends on a library package. // Created at consumer install time from the manifest's "dependencies" map. diff --git a/server/models/models.go b/server/models/models.go index c57621a..c0bd9c6 100644 --- a/server/models/models.go +++ b/server/models/models.go @@ -218,7 +218,7 @@ func IntPtr(i int) *int { return &i } func BoolPtr(b bool) *bool { return &b } // ========================================= -// GROUPS (v0.16.0) +// GROUPS // ========================================= // 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. // KBSearchResult is a single result from similarity search. // ChannelKB represents a knowledge base linked to a channel. -// PROVIDER HEALTH (v0.22.0) +// PROVIDER HEALTH // ProviderStatus represents the derived health state. @@ -294,10 +294,10 @@ type ResourceGrant struct { // 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. -// ROUTING POLICIES (v0.22.2) +// ROUTING POLICIES // RoutingPolicy is one routing rule that controls how requests are // dispatched to provider configs. Stored in the routing_policies table. diff --git a/server/models/models_extension_perm.go b/server/models/models_extension_perm.go index 8f77789..cd4fe20 100644 --- a/server/models/models_extension_perm.go +++ b/server/models/models_extension_perm.go @@ -31,11 +31,11 @@ const ( ExtPermDBRead = "db.read" ExtPermDBWrite = "db.write" ExtPermAPIHTTP = "api.http" - ExtPermFormValidate = "forms.validate" // v0.29.3: form validation hooks - ExtPermWorkflowAccess = "workflow.access" // v0.30.2: workflow definition + stage data access - ExtPermConnectionsRead = "connections.read" // v0.38.1: extension connection resolution - ExtPermTriggersRegister = "triggers.register" // v0.2.2: event/webhook trigger registration - ExtPermRealtimePublish = "realtime.publish" // v0.5.0: publish events to realtime channels + ExtPermFormValidate = "forms.validate" + ExtPermWorkflowAccess = "workflow.access" + ExtPermConnectionsRead = "connections.read" + ExtPermTriggersRegister = "triggers.register" + ExtPermRealtimePublish = "realtime.publish" ) // ValidExtensionPermissions is the set of recognized permission keys. diff --git a/server/models/models_notification.go b/server/models/models_notification.go index 2d7031c..20df782 100644 --- a/server/models/models_notification.go +++ b/server/models/models_notification.go @@ -3,7 +3,7 @@ package models import "time" // ========================================= -// NOTIFICATIONS (v0.20.0) +// NOTIFICATIONS // ========================================= // Notification represents an in-app notification for a user. diff --git a/server/models/workflow.go b/server/models/workflow.go index 12c9fb1..f4b4413 100644 --- a/server/models/workflow.go +++ b/server/models/workflow.go @@ -120,22 +120,21 @@ var ValidAudiences = map[string]bool{ // ── Typed Form Template ───────────────────── -// TypedFormTemplate is the structured form_template schema (v0.29.3). -// v0.35.0: added Fieldsets for progressive multi-step forms. +// TypedFormTemplate is the structured form_template schema. type TypedFormTemplate struct { 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"` } -// 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. type FormFieldset struct { Label string `json:"label"` 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. type FieldCondition struct { When string `json:"when"` // key of the field to check @@ -153,7 +152,7 @@ type FormField struct { Default interface{} `json:"default,omitempty"` Options []FormOption `json:"options,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. @@ -200,7 +199,6 @@ func ParseTypedFormTemplate(raw json.RawMessage) *TypedFormTemplate { return nil } - // v0.35.0: If fieldsets present, flatten into fields for backward compat if len(tpl.Fieldsets) > 0 { var allFields []FormField for _, fs := range tpl.Fieldsets { @@ -237,7 +235,6 @@ type FieldError struct { func ValidateFormData(tpl *TypedFormTemplate, data map[string]interface{}) []FieldError { var errs []FieldError for _, f := range tpl.Fields { - // v0.35.0: Skip fields whose condition is not met if f.Condition != nil && !evaluateFieldCondition(f.Condition, data) { 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"}} } -// ── Field Condition Evaluator (v0.35.0) ───── +// ── Field Condition Evaluator ───── // evaluateFieldCondition checks if a conditional field should be visible/validated. func evaluateFieldCondition(cond *FieldCondition, data map[string]interface{}) bool { @@ -505,7 +502,7 @@ type WorkflowAssignment struct { 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. type WorkflowSignoff struct { diff --git a/server/notifications/sources.go b/server/notifications/sources.go index deac952..22cc782 100644 --- a/server/notifications/sources.go +++ b/server/notifications/sources.go @@ -14,7 +14,7 @@ import ( // ── Role Fallback ─────────────────────────── // 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. // roleFallbackPayload matches the payload emitted by the role resolver. diff --git a/server/pages/loaders.go b/server/pages/loaders.go index b0c074c..1793c26 100644 --- a/server/pages/loaders.go +++ b/server/pages/loaders.go @@ -28,10 +28,10 @@ type AdminPageData struct { } // 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 { Section string `json:"section"` - ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"` // v0.38.3 + ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"` } // ── Loader registration ────────────────────── @@ -39,7 +39,7 @@ type SettingsPageData struct { // TeamAdminPageData is what the team-admin surface receives. type TeamAdminPageData struct { Section string `json:"section"` - ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"` // v0.38.3 + ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"` } func (e *Engine) registerLoaders() { @@ -91,7 +91,6 @@ func sectionCategory(section string) string { } // ── Settings loader ────────────────────────── -// v0.22.7: Reads feature gates from GlobalConfig to control // which nav links/tabs are visible (BYOK, User Personas). 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 } -// ── Config section discovery (v0.38.3) ─────── +// ── Config section discovery ─────── // // Packages declare a config_section in their manifest. This helper scans // enabled packages and returns entries whose surfaces array includes the diff --git a/server/pages/pages.go b/server/pages/pages.go index a283d7a..0fb2a54 100644 --- a/server/pages/pages.go +++ b/server/pages/pages.go @@ -38,7 +38,7 @@ type Engine struct { cfg *config.Config stores store.Stores loaders map[string]DataLoaderFunc - surfaces []SurfaceManifest // v0.25.0: registered surface definitions + surfaces []SurfaceManifest devMode bool } @@ -95,25 +95,20 @@ type PageData struct { User *UserContext Data any // surface-specific data from loader - // v0.22.7: Theme + settings injection Theme string // "dark", "light", or "" (= use default) SurfaceSettings any // JSON-serialized to window.__SETTINGS__ - // v0.25.0: Surface manifest — layout preset, components, etc. Manifest *SurfaceManifest `json:"manifest,omitempty"` - // v0.25.0: Enabled surface IDs for conditional nav rendering. EnabledSurfaces []string `json:"-"` - // v0.27.0: Extension surfaces for sidebar nav rendering. ExtensionSurfaces []ExtensionNavItem `json:"-"` - // v0.22.7: Login/splash page fields InstanceName string // branding: instance display name LogoURL string // branding: custom logo URL Tagline string // branding: tagline under instance name 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. @@ -137,7 +132,6 @@ type ExtensionNavItem struct { // ConfigSectionEntry describes a package-declared config section for // lazy-loading in Settings, Admin, or Team Admin surfaces. -// v0.38.3: manifest-driven discovery — no new tables or endpoints. type ConfigSectionEntry struct { PackageID string `json:"package_id"` // section key & asset path prefix Label string `json:"label"` // nav link text @@ -207,7 +201,7 @@ func New(cfg *config.Config, stores store.Stores) *Engine { e.parseTemplates() e.registerLoaders() e.registerCoreSurfaces() - e.SeedSurfaces() // v0.25.0: Write core manifests to DB (preserves admin toggle state) + e.SeedSurfaces() return e } @@ -278,7 +272,6 @@ func (e *Engine) parseTemplates() { "toJSON": toJSON, "eq": func(a, b string) bool { return a == b }, "contains": stringContains, - "roleFilterType": roleFilterType, "esc": template.HTMLEscapeString, "safeHTML": func(s string) template.HTML { return template.HTML(s) }, "join": strings.Join, @@ -323,7 +316,6 @@ func (e *Engine) Render(c *gin.Context, name string, data PageData) { data.Message = e.loadMessage() data.Footer = e.loadFooter() - // v0.22.7: Default theme if not set if data.Theme == "" { data.Theme = "dark" } @@ -478,7 +470,6 @@ func (e *Engine) RegisterPageRoutes(base *gin.RouterGroup, mw PageRouteMiddlewar 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. group.GET("/s/:slug", e.RenderExtensionSurface()) } @@ -593,7 +584,6 @@ func stripPrefix(route, prefix string) string { // RenderLogin serves the standalone login page. func (e *Engine) RenderLogin() gin.HandlerFunc { return func(c *gin.Context) { - // v0.22.7: Populate splash/login fields from global config instanceName, logoURL, tagline := e.loadBranding() regOpen := e.isRegistrationOpen() @@ -620,8 +610,8 @@ type WorkflowPageData struct { FormTemplateJSON string // typed form template JSON (empty if custom) TotalStages int CurrentStage int - SurfacePkgID string // v0.30.2: custom package surface override (empty = use StageMode) - BrandingJSON string // v0.35.0: workflow branding JSON (accent_color, logo_url, tagline) + SurfacePkgID string + BrandingJSON string } // WorkflowLandingPageData is passed to workflow-landing.html. @@ -639,7 +629,7 @@ type WorkflowLandingPageData struct { PersonaName string PersonaIcon string 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 } @@ -659,7 +649,7 @@ func (e *Engine) RenderWorkflow() gin.HandlerFunc { // Load session display name 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 totalStages, currentStage int stageMode = "custom" // default @@ -919,18 +909,6 @@ func stringContains(haystack []string, needle string) bool { 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. func dict(pairs ...any) map[string]any { m := make(map[string]any, len(pairs)/2) diff --git a/server/pages/pages_surfaces.go b/server/pages/pages_surfaces.go index 2a027dd..1fafa92 100644 --- a/server/pages/pages_surfaces.go +++ b/server/pages/pages_surfaces.go @@ -35,7 +35,6 @@ func (e *Engine) SeedSurfaces() { } 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 // 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" { diff --git a/server/sandbox/connections_module.go b/server/sandbox/connections_module.go index 812829a..ccf512c 100644 --- a/server/sandbox/connections_module.go +++ b/server/sandbox/connections_module.go @@ -1,6 +1,5 @@ // Package sandbox — connections_module.go // -// v0.38.1: Connections module for Starlark extensions. // Requires permission: connections.read // // Starlark API: diff --git a/server/sandbox/db_module.go b/server/sandbox/db_module.go index 374a4fb..e8a3307 100644 --- a/server/sandbox/db_module.go +++ b/server/sandbox/db_module.go @@ -1,6 +1,5 @@ // Package sandbox — db_module.go // -// v0.29.2 CS1: Structured db module for Starlark extensions. // // Permissions: // db.read → db.query(), db.view(), db.list_tables() diff --git a/server/sandbox/http_module.go b/server/sandbox/http_module.go index 72aaa9b..1b2a7f8 100644 --- a/server/sandbox/http_module.go +++ b/server/sandbox/http_module.go @@ -1,6 +1,5 @@ // Package sandbox — http_module.go // -// v0.29.1 CS0: HTTP outbound module for Starlark extensions. // Requires permission: api.http // // Starlark API: diff --git a/server/sandbox/lib_module.go b/server/sandbox/lib_module.go index db87d20..787175e 100644 --- a/server/sandbox/lib_module.go +++ b/server/sandbox/lib_module.go @@ -1,6 +1,5 @@ // Package sandbox — lib_module.go // -// v0.38.2: Library loading module for Starlark extensions. // No permission required — any starlark package can use lib.require(). // // Starlark API: diff --git a/server/sandbox/modules.go b/server/sandbox/modules.go index cd281b8..644c4a7 100644 --- a/server/sandbox/modules.go +++ b/server/sandbox/modules.go @@ -1,6 +1,5 @@ // Package sandbox — modules.go // -// v0.29.0 CS3: Module factories for Starlark sandbox. // Each factory returns a starlarkstruct.Module that the runner // injects into the script namespace based on granted permissions. package sandbox diff --git a/server/sandbox/realtime_module.go b/server/sandbox/realtime_module.go index 065b52f..6e8c5a8 100644 --- a/server/sandbox/realtime_module.go +++ b/server/sandbox/realtime_module.go @@ -1,6 +1,5 @@ // Package sandbox — realtime_module.go // -// v0.5.0: Starlark realtime.publish() module. // // Allows extensions to publish events to WebSocket channels. // Events are scoped to rooms — only clients subscribed to the diff --git a/server/sandbox/runner.go b/server/sandbox/runner.go index 8c26017..eb39e98 100644 --- a/server/sandbox/runner.go +++ b/server/sandbox/runner.go @@ -1,19 +1,14 @@ // 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. // -// v0.29.1 CS0: Wires api.http permission to BuildHTTPModule with // 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. // -// 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 // 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. // // 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 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. TeamID string } @@ -57,13 +52,13 @@ type RunContext struct { type Runner struct { sandbox *Sandbox stores store.Stores - packagesDir string // v0.38.0: disk path for load() support + packagesDir string 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 dbPostgres bool // true = use $N placeholders; false = use ? - allowPrivateIPs bool // v0.38.5: disable SSRF private IP check - bus *events.Bus // v0.5.0: event bus for realtime module + allowPrivateIPs bool + bus *events.Bus } // NewRunner creates a runner with the given sandbox and dependencies. @@ -79,7 +74,7 @@ func (r *Runner) SetNotifier(n NotificationSender) { 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) { 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. // Required for load() support — scripts are read from disk at runtime. -// v0.38.0. func (r *Runner) SetPackagesDir(dir string) { 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) { r.bus = bus } @@ -114,7 +108,6 @@ func (r *Runner) SetAllowPrivateIPs(allow bool) { // ExecPackage loads a package's script from disk (primary) or manifest // (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 // 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 } - // v0.38.2: per-invocation lib context for lib.load() caching + cycle detection lc := newLibContext() // 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 // (e.g., http module reads network_access, provider reads requires_provider). // 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) { if r.stores.ExtPermissions == 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 (v0.2.0 cascade). + // A package reads its own admin + team + user settings. userID := "" teamID := "" 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) - // v0.38.2: lib module — always injected, no permission required. // Allows any starlark package to load declared library dependencies. if lc != nil { modules["lib"] = BuildLibModule(ctx, r, packageID, rc, lc) diff --git a/server/sandbox/sandbox.go b/server/sandbox/sandbox.go index aee01ac..a03e5e7 100644 --- a/server/sandbox/sandbox.go +++ b/server/sandbox/sandbox.go @@ -1,6 +1,5 @@ // Package sandbox — sandboxed Starlark interpreter. // -// v0.29.0 CS1: Provides execution with timeout, step limits, // captured output, and injectable predeclared modules. // // The sandbox is intentionally restrictive: diff --git a/server/sandbox/settings_module.go b/server/sandbox/settings_module.go index fe3c970..e3eb981 100644 --- a/server/sandbox/settings_module.go +++ b/server/sandbox/settings_module.go @@ -1,6 +1,6 @@ package sandbox -// settings_module.go — v0.2.0 +// settings_module.go // // The settings module lets extensions read their resolved settings via // the three-tier cascade: global → team → user, respecting the diff --git a/server/sandbox/unicode_scan.go b/server/sandbox/unicode_scan.go index 65bb330..a9cd950 100644 --- a/server/sandbox/unicode_scan.go +++ b/server/sandbox/unicode_scan.go @@ -1,6 +1,5 @@ // Package sandbox — unicode_scan.go // -// v0.5.1: Invisible Unicode scanning gate. // // Defends against GlassWorm (variation selector payloads), Trojan Source // (bidi override attacks / CVE-2021-42574), and other invisible Unicode diff --git a/server/sandbox/workflow_module.go b/server/sandbox/workflow_module.go index c75a465..376b26c 100644 --- a/server/sandbox/workflow_module.go +++ b/server/sandbox/workflow_module.go @@ -1,6 +1,6 @@ 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, // manage instances, and programmatically advance workflow stages. @@ -27,7 +27,6 @@ import ( // BuildWorkflowModule creates the "workflow" Starlark module for a package. // 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; // Starlark builtins for them will be added when the engine interface is // 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) { 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) diff --git a/server/store/interfaces.go b/server/store/interfaces.go index 0351b6a..c2a91a7 100644 --- a/server/store/interfaces.go +++ b/server/store/interfaces.go @@ -42,19 +42,19 @@ type Stores struct { ResourceGrants ResourceGrantStore Notifications NotificationStore NotifPrefs NotificationPreferenceStore - Presence PresenceStore // v0.29.0: User online/offline status - Health HealthStore // v0.29.0: Provider health window management - Connections ConnectionStore // v0.38.1: Extension connection credentials - Dependencies DependencyStore // v0.38.2: Library package dependency graph - Packages PackageStore // v0.28.7: Unified package registry (surfaces + extensions) - Workflows WorkflowStore // v0.26.1: Workflow definitions + stages - ExtPermissions ExtensionPermissionStore // v0.29.0: Extension declared/granted capabilities - ExtData ExtDataStore // v0.29.2: Extension namespaced table catalog - Tickets TicketStore // v0.32.0: WS auth tickets (PG-backed for cross-pod) - RateLimits RateLimitStore // v0.32.0: Distributed rate limiting - Triggers TriggerStore // v0.2.2: Extension event/webhook triggers - ScheduledTasks ScheduledTaskStore // v0.2.2: User-created cron tasks - Cluster ClusterStore // v0.6.0: PG-backed cluster node registry (nil on SQLite) + Presence PresenceStore + Health HealthStore + Connections ConnectionStore + Dependencies DependencyStore + Packages PackageStore + Workflows WorkflowStore + ExtPermissions ExtensionPermissionStore + ExtData ExtDataStore + Tickets TicketStore + RateLimits RateLimitStore + Triggers TriggerStore + ScheduledTasks ScheduledTaskStore + Cluster ClusterStore } // TeamAvailableModel is returned by CatalogStore.ListTeamAvailable. @@ -85,7 +85,7 @@ type UserStore interface { RevokeAllRefreshTokens(ctx context.Context, userID string) 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(ctx context.Context, userID string) (bool, error) @@ -94,7 +94,7 @@ type UserStore interface { // Excludes the calling user. Max 20 results. SearchActive(ctx context.Context, excludeUserID, query string) ([]UserSearchResult, error) - // ── CS2 additions (v0.29.0) ── + // ── CS2 additions ── // CountAll returns the total number of users. CountAll(ctx context.Context) (int, error) @@ -108,7 +108,7 @@ type UserStore interface { // UpdateVaultKeys re-wraps the user's vault keys (password change). 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(ctx context.Context, userID string) error @@ -147,7 +147,7 @@ type TeamStore interface { IsTeamAdmin(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(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(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(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(ctx context.Context, teamID, userID, role string) (string, error) @@ -175,11 +175,6 @@ type TeamStore interface { 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 // ========================================= @@ -212,7 +207,7 @@ type GlobalConfigStore interface { Set(ctx context.Context, key string, value models.JSONMap, updatedBy string) 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(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(ctx context.Context) error - // ── CS6 additions (v0.29.0) ── + // ── CS6 additions ── // GetString returns the raw value column as a string for simple settings // (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 { @@ -257,7 +252,7 @@ type GroupStore interface { } // ========================================= -// RESOURCE GRANT STORE (v0.16.0) +// RESOURCE GRANT STORE // ========================================= type ResourceGrantStore interface { @@ -270,7 +265,7 @@ type ResourceGrantStore interface { } // ========================================= -// NOTIFICATION STORE (v0.20.0) +// NOTIFICATION STORE // ========================================= type NotificationStore interface { @@ -284,7 +279,7 @@ type NotificationStore interface { } // ========================================= -// NOTIFICATION PREFERENCES STORE (v0.20.0) +// NOTIFICATION PREFERENCES STORE // ========================================= 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 @@ -315,7 +310,7 @@ type HealthStore interface { } // ========================================= -// PRESENCE STORE (v0.29.0) +// PRESENCE STORE // ========================================= // PresenceStore manages user online/offline status. @@ -334,7 +329,7 @@ type PresenceStore interface { // ========================================= // SHARED TYPES // ========================================= -// CONNECTION STORE (v0.38.1) +// CONNECTION STORE // ========================================= type ConnectionStore interface { @@ -367,7 +362,7 @@ type ConnectionStore interface { } // ========================================= -// DEPENDENCY STORE (v0.38.2) +// DEPENDENCY STORE // ========================================= type DependencyStore interface { diff --git a/server/store/package_iface.go b/server/store/package_iface.go index f09cb14..d411925 100644 --- a/server/store/package_iface.go +++ b/server/store/package_iface.go @@ -6,7 +6,7 @@ import ( ) // PackageStore manages the unified package registry (surfaces + extensions). -// Replaces SurfaceRegistryStore and ExtensionStore (v0.28.7). +// Replaces SurfaceRegistryStore and ExtensionStore. type PackageStore interface { // ── Lifecycle (from SurfaceRegistryStore) ──────────── @@ -66,13 +66,13 @@ type PackageStore interface { // DeleteUserSettings removes per-user settings, reverting to defaults. DeleteUserSettings(ctx context.Context, pkgID, userID string) error - // ── Scoped visibility (v0.30.0) ──────────────── + // ── Scoped visibility ──────────────── // ListVisiblePackages returns packages visible to the given user: // global packages + team packages for user's teams + personal packages. 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(ctx context.Context, id string, version int) error @@ -83,7 +83,7 @@ type PackageStore interface { // SetPackageSettings stores admin-configured package-level settings. 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(ctx context.Context, pkgID, teamID string) (json.RawMessage, error) @@ -134,7 +134,7 @@ type PackageUserSettings struct { 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 { PackageID string `json:"package_id" db:"package_id"` TeamID string `json:"team_id" db:"team_id"` diff --git a/server/store/postgres/ext_connection.go b/server/store/postgres/ext_connection.go index 92c7d00..f20c6dd 100644 --- a/server/store/postgres/ext_connection.go +++ b/server/store/postgres/ext_connection.go @@ -9,7 +9,7 @@ import ( "switchboard-core/models" ) -// ── ConnectionStore (v0.38.1) ──────────────────────── +// ── ConnectionStore ──────────────────────── type ConnectionStore struct{} diff --git a/server/store/postgres/ext_dependency.go b/server/store/postgres/ext_dependency.go index d9aab13..e446a26 100644 --- a/server/store/postgres/ext_dependency.go +++ b/server/store/postgres/ext_dependency.go @@ -7,7 +7,7 @@ import ( "switchboard-core/models" ) -// ── DependencyStore (v0.38.2) ──────────────────────── +// ── DependencyStore ──────────────────────── type DependencyStore struct{} diff --git a/server/store/postgres/global_config.go b/server/store/postgres/global_config.go index 5e74825..9c36a1f 100644 --- a/server/store/postgres/global_config.go +++ b/server/store/postgres/global_config.go @@ -58,7 +58,7 @@ func (s *GlobalConfigStore) GetAll(ctx context.Context) (map[string]models.JSONM return result, rows.Err() } -// ── OIDC state (v0.29.0-cs4) ──────────────────────────────────────────── +// ── OIDC state ──────────────────────────────────────────── func (s *GlobalConfigStore) SaveOIDCState(ctx context.Context, state, nonce, redirectTo string) error { _, err := DB.ExecContext(ctx, ` @@ -86,7 +86,7 @@ func (s *GlobalConfigStore) CleanupOIDCState(ctx context.Context) error { return err } -// ── CS6 additions (v0.29.0) ───────────────────────────────────────────── +// ── CS6 additions ───────────────────────────────────────────── func (s *GlobalConfigStore) GetString(ctx context.Context, key string) (string, error) { var val string diff --git a/server/store/postgres/packages.go b/server/store/postgres/packages.go index 008f56f..460c407 100644 --- a/server/store/postgres/packages.go +++ b/server/store/postgres/packages.go @@ -310,7 +310,7 @@ func (s *PackageStore) scanMany(ctx context.Context, query string, args ...inter return result, rows.Err() } -// ── Scoped visibility (v0.30.0) ────────────────── +// ── Scoped visibility ────────────────── func (s *PackageStore) ListVisiblePackages(ctx context.Context, userID string) ([]store.PackageRegistration, error) { return s.scanMany(ctx, ` @@ -327,7 +327,7 @@ func (s *PackageStore) ListVisiblePackages(ctx context.Context, userID string) ( 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 { result, err := DB.ExecContext(ctx, @@ -367,7 +367,7 @@ func (s *PackageStore) SetPackageSettings(ctx context.Context, id string, settin return nil } -// ── Team-level settings (v0.2.0) ───────────────── +// ── Team-level settings ───────────────── func (s *PackageStore) GetTeamSettings(ctx context.Context, pkgID, teamID string) (json.RawMessage, error) { var settings []byte diff --git a/server/store/postgres/ratelimit.go b/server/store/postgres/ratelimit.go index 192a984..47298ec 100644 --- a/server/store/postgres/ratelimit.go +++ b/server/store/postgres/ratelimit.go @@ -7,7 +7,6 @@ import ( ) // RateLimitStore manages distributed rate limit counters in Postgres. -// v0.32.0: fixed-window counter — upsert per-second bucket, check burst. type RateLimitStore struct{} func NewRateLimitStore() *RateLimitStore { return &RateLimitStore{} } diff --git a/server/store/postgres/team.go b/server/store/postgres/team.go index d971f08..2ea8e79 100644 --- a/server/store/postgres/team.go +++ b/server/store/postgres/team.go @@ -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) { var exists bool @@ -279,7 +279,7 @@ func (s *TeamStore) ListTeamAuditActions(ctx context.Context, teamID string) ([] return actions, rows.Err() } -// ── CS5b additions (v0.29.0) ──────────────────────────────────────────── +// ── CS5b additions ──────────────────────────────────────────── func (s *TeamStore) GetFirstTeamIDForUser(ctx context.Context, userID string) (string, error) { var teamID string @@ -291,7 +291,7 @@ func (s *TeamStore) GetFirstTeamIDForUser(ctx context.Context, userID string) (s return teamID, nil } -// ── CS6 additions (v0.29.0) ──────────────────────────────────────────── +// ── CS6 additions ──────────────────────────────────────────── func (s *TeamStore) AddMemberReturningID(ctx context.Context, teamID, userID, role string) (string, error) { var id string diff --git a/server/store/postgres/tickets.go b/server/store/postgres/tickets.go index 58c9a54..f06d70c 100644 --- a/server/store/postgres/tickets.go +++ b/server/store/postgres/tickets.go @@ -8,7 +8,6 @@ import ( ) // TicketStore manages WS auth tickets in Postgres. -// v0.32.0: replaces in-memory sync.Map for cross-pod validation. type TicketStore struct{} func NewTicketStore() *TicketStore { return &TicketStore{} } diff --git a/server/store/postgres/user.go b/server/store/postgres/user.go index 14350f2..ed7ca35 100644 --- a/server/store/postgres/user.go +++ b/server/store/postgres/user.go @@ -187,7 +187,7 @@ func scanOneUser(ctx context.Context, query string, args ...interface{}) (*model ScanJSON(sj, &u.Settings) return &u, nil } -// ── CS1 additions (v0.29.0) ───────────────────────────────────────────── +// ── CS1 additions ───────────────────────────────────────────── func (s *UserStore) Exists(ctx context.Context, userID string) (bool, error) { var exists bool @@ -231,7 +231,7 @@ func (s *UserStore) SearchActive(ctx context.Context, excludeUserID, query strin return results, rows.Err() } -// ── CS2 additions (v0.29.0) ───────────────────────────────────────────── +// ── CS2 additions ───────────────────────────────────────────── func (s *UserStore) MergeSettings(ctx context.Context, userID string, patch []byte) error { _, err := DB.ExecContext(ctx, ` @@ -268,7 +268,7 @@ func (s *UserStore) CountAll(ctx context.Context) (int, error) { return count, err } -// ── CS4 additions (v0.29.0) ───────────────────────────────────────────── +// ── CS4 additions ───────────────────────────────────────────── func (s *UserStore) ClearVaultKeys(ctx context.Context, userID string) error { _, err := DB.ExecContext(ctx, ` diff --git a/server/store/postgres/workflows.go b/server/store/postgres/workflows.go index 8352f21..2baaee1 100644 --- a/server/store/postgres/workflows.go +++ b/server/store/postgres/workflows.go @@ -404,7 +404,7 @@ func nullIfEmpty(s string) interface{} { return s } -// ── Instances (v0.3.1) ────────────────────── +// ── Instances ────────────────────── func (s *WorkflowStore) CreateInstance(ctx context.Context, inst *models.WorkflowInstance) error { stageData := jsonOrEmpty(inst.StageData) @@ -573,7 +573,7 @@ func (s *WorkflowStore) ListActiveInstances(ctx context.Context) ([]models.Workf return result, rows.Err() } -// ── Assignments (v0.3.1) ──────────────────── +// ── Assignments ──────────────────── func (s *WorkflowStore) CreateAssignment(ctx context.Context, a *models.WorkflowAssignment) error { reviewData := jsonOrEmpty(a.ReviewData) @@ -661,7 +661,7 @@ func (s *WorkflowStore) ListAssignmentsByUser(ctx context.Context, userID string return s.queryAssignments(ctx, q, args...) } -// ── Signoffs (v0.3.4) ───────────────────────── +// ── Signoffs ───────────────────────── func (s *WorkflowStore) CreateSignoff(ctx context.Context, so *models.WorkflowSignoff) error { return DB.QueryRowContext(ctx, ` diff --git a/server/store/ratelimit_iface.go b/server/store/ratelimit_iface.go index 00da431..cb82f7e 100644 --- a/server/store/ratelimit_iface.go +++ b/server/store/ratelimit_iface.go @@ -6,7 +6,6 @@ import ( ) // RateLimitStore manages distributed rate limit counters. -// v0.32.0: replaces in-memory token bucket for cross-pod rate limiting. type RateLimitStore interface { // Allow checks if a request is within the rate limit. // key is "{scope}:{identifier}" e.g. "auth:192.168.1.1". diff --git a/server/store/settings_cascade.go b/server/store/settings_cascade.go index 077b46f..cf23945 100644 --- a/server/store/settings_cascade.go +++ b/server/store/settings_cascade.go @@ -1,6 +1,6 @@ package store -// settings_cascade.go — v0.2.0 +// settings_cascade.go // // Pure functions for three-tier settings resolution: // global (admin) → team → user diff --git a/server/store/sqlite/ext_connection.go b/server/store/sqlite/ext_connection.go index 4fcba6c..5094bcb 100644 --- a/server/store/sqlite/ext_connection.go +++ b/server/store/sqlite/ext_connection.go @@ -11,7 +11,7 @@ import ( "switchboard-core/store" ) -// ── ConnectionStore (v0.38.1) ──────────────────────── +// ── ConnectionStore ──────────────────────── type ConnectionStore struct{} diff --git a/server/store/sqlite/ext_dependency.go b/server/store/sqlite/ext_dependency.go index f374eef..6ace306 100644 --- a/server/store/sqlite/ext_dependency.go +++ b/server/store/sqlite/ext_dependency.go @@ -7,7 +7,7 @@ import ( "switchboard-core/models" ) -// ── DependencyStore (v0.38.2) ──────────────────────── +// ── DependencyStore ──────────────────────── type DependencyStore struct{} diff --git a/server/store/sqlite/global_config.go b/server/store/sqlite/global_config.go index 28b89df..1e7be23 100644 --- a/server/store/sqlite/global_config.go +++ b/server/store/sqlite/global_config.go @@ -57,7 +57,7 @@ func (s *GlobalConfigStore) GetAll(ctx context.Context) (map[string]models.JSONM return result, rows.Err() } -// ── OIDC state (v0.29.0-cs4) ──────────────────────────────────────────── +// ── OIDC state ──────────────────────────────────────────── func (s *GlobalConfigStore) SaveOIDCState(ctx context.Context, state, nonce, redirectTo string) error { _, err := DB.ExecContext(ctx, ` @@ -84,7 +84,7 @@ func (s *GlobalConfigStore) CleanupOIDCState(ctx context.Context) error { return err } -// ── CS6 additions (v0.29.0) ───────────────────────────────────────────── +// ── CS6 additions ───────────────────────────────────────────── func (s *GlobalConfigStore) GetString(ctx context.Context, key string) (string, error) { var val string diff --git a/server/store/sqlite/packages.go b/server/store/sqlite/packages.go index cbe0ae0..704f5c1 100644 --- a/server/store/sqlite/packages.go +++ b/server/store/sqlite/packages.go @@ -331,7 +331,7 @@ func (s *PackageStore) scanMany(ctx context.Context, query string, args ...inter return result, rows.Err() } -// ── Scoped visibility (v0.30.0) ────────────────── +// ── Scoped visibility ────────────────── func (s *PackageStore) ListVisiblePackages(ctx context.Context, userID string) ([]store.PackageRegistration, error) { 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) } -// ── Package lifecycle (v0.30.0) ────────────────── +// ── Package lifecycle ────────────────── func (s *PackageStore) SetSchemaVersion(ctx context.Context, id string, version int) error { result, err := DB.ExecContext(ctx, @@ -388,7 +388,7 @@ func (s *PackageStore) SetPackageSettings(ctx context.Context, id string, settin return nil } -// ── Team-level settings (v0.2.0) ───────────────── +// ── Team-level settings ───────────────── func (s *PackageStore) GetTeamSettings(ctx context.Context, pkgID, teamID string) (json.RawMessage, error) { var settings string diff --git a/server/store/sqlite/ratelimit.go b/server/store/sqlite/ratelimit.go index 503a6d8..c2b1f40 100644 --- a/server/store/sqlite/ratelimit.go +++ b/server/store/sqlite/ratelimit.go @@ -7,7 +7,6 @@ import ( ) // RateLimitStore manages rate limit counters in SQLite. -// v0.32.0: functional parity for single-process test coverage. type RateLimitStore struct{} func NewRateLimitStore() *RateLimitStore { return &RateLimitStore{} } diff --git a/server/store/sqlite/team.go b/server/store/sqlite/team.go index a8873e3..6da5eba 100644 --- a/server/store/sqlite/team.go +++ b/server/store/sqlite/team.go @@ -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) { var count int @@ -286,7 +286,7 @@ func (s *TeamStore) ListTeamAuditActions(ctx context.Context, teamID string) ([] return actions, rows.Err() } -// ── CS5b additions (v0.29.0) ──────────────────────────────────────────── +// ── CS5b additions ──────────────────────────────────────────── func (s *TeamStore) GetFirstTeamIDForUser(ctx context.Context, userID string) (string, error) { var teamID string @@ -298,7 +298,7 @@ func (s *TeamStore) GetFirstTeamIDForUser(ctx context.Context, userID string) (s return teamID, nil } -// ── CS6 additions (v0.29.0) ──────────────────────────────────────────── +// ── CS6 additions ──────────────────────────────────────────── func (s *TeamStore) AddMemberReturningID(ctx context.Context, teamID, userID, role string) (string, error) { id := store.NewID() diff --git a/server/store/sqlite/tickets.go b/server/store/sqlite/tickets.go index fbc1466..d950f7c 100644 --- a/server/store/sqlite/tickets.go +++ b/server/store/sqlite/tickets.go @@ -8,7 +8,6 @@ import ( ) // TicketStore manages WS auth tickets in SQLite. -// v0.32.0: functional parity for single-process test coverage. type TicketStore struct{} func NewTicketStore() *TicketStore { return &TicketStore{} } diff --git a/server/store/sqlite/user.go b/server/store/sqlite/user.go index 3cb450d..41ced69 100644 --- a/server/store/sqlite/user.go +++ b/server/store/sqlite/user.go @@ -194,7 +194,7 @@ func (s *UserStore) scanOne(ctx context.Context, query string, args ...interface ScanJSON(sj, &u.Settings) return &u, nil } -// ── CS1 additions (v0.29.0) ───────────────────────────────────────────── +// ── CS1 additions ───────────────────────────────────────────── func (s *UserStore) Exists(ctx context.Context, userID string) (bool, error) { var count int @@ -238,7 +238,7 @@ func (s *UserStore) SearchActive(ctx context.Context, excludeUserID, query strin return results, rows.Err() } -// ── CS2 additions (v0.29.0) ───────────────────────────────────────────── +// ── CS2 additions ───────────────────────────────────────────── func (s *UserStore) MergeSettings(ctx context.Context, userID string, patch []byte) error { _, err := DB.ExecContext(ctx, ` @@ -275,7 +275,7 @@ func (s *UserStore) CountAll(ctx context.Context) (int, error) { return count, err } -// ── CS4 additions (v0.29.0) ───────────────────────────────────────────── +// ── CS4 additions ───────────────────────────────────────────── func (s *UserStore) ClearVaultKeys(ctx context.Context, userID string) error { _, err := DB.ExecContext(ctx, ` diff --git a/server/store/sqlite/workflows.go b/server/store/sqlite/workflows.go index b568edf..1ff8dd9 100644 --- a/server/store/sqlite/workflows.go +++ b/server/store/sqlite/workflows.go @@ -418,7 +418,7 @@ func nullIfEmpty(s string) interface{} { return s } -// ── Instances (v0.3.1) ────────────────────── +// ── Instances ────────────────────── func (s *WorkflowStore) CreateInstance(ctx context.Context, inst *models.WorkflowInstance) error { inst.ID = store.NewID() @@ -597,7 +597,7 @@ func (s *WorkflowStore) ListActiveInstances(ctx context.Context) ([]models.Workf return result, rows.Err() } -// ── Assignments (v0.3.1) ──────────────────── +// ── Assignments ──────────────────── func (s *WorkflowStore) CreateAssignment(ctx context.Context, a *models.WorkflowAssignment) error { a.ID = store.NewID() @@ -690,7 +690,7 @@ func (s *WorkflowStore) ListAssignmentsByUser(ctx context.Context, userID string return s.queryAssignments(ctx, q, args...) } -// ── Signoffs (v0.3.4) ───────────────────────── +// ── Signoffs ───────────────────────── func (s *WorkflowStore) CreateSignoff(ctx context.Context, so *models.WorkflowSignoff) error { so.ID = store.NewID() diff --git a/server/store/ticket_iface.go b/server/store/ticket_iface.go index 9ab4a45..a652182 100644 --- a/server/store/ticket_iface.go +++ b/server/store/ticket_iface.go @@ -3,7 +3,6 @@ package store import "context" // 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 { // Issue creates a single-use ticket for the given user. // Returns the opaque ticket ID (128-bit hex). diff --git a/server/store/workflow_iface.go b/server/store/workflow_iface.go index a1ab2e7..c6e7fbe 100644 --- a/server/store/workflow_iface.go +++ b/server/store/workflow_iface.go @@ -31,7 +31,7 @@ type WorkflowStore interface { GetVersion(ctx context.Context, workflowID string, versionNumber int) (*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 GetInstance(ctx context.Context, id 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 ListActiveInstances(ctx context.Context) ([]models.WorkflowInstance, error) - // Assignments (v0.3.1) + // Assignments CreateAssignment(ctx context.Context, a *models.WorkflowAssignment) error ClaimAssignment(ctx context.Context, id string, userID 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) ListAssignmentsByUser(ctx context.Context, userID string, status string) ([]models.WorkflowAssignment, error) - // Signoffs (v0.3.4) + // Signoffs CreateSignoff(ctx context.Context, s *models.WorkflowSignoff) error ListSignoffs(ctx context.Context, instanceID, stage string) ([]models.WorkflowSignoff, error) CountSignoffs(ctx context.Context, instanceID, stage, decision string) (int, error) diff --git a/server/triggers/engine.go b/server/triggers/engine.go index 95367e6..665f2c6 100644 --- a/server/triggers/engine.go +++ b/server/triggers/engine.go @@ -1,6 +1,5 @@ // Package triggers — engine.go // -// v0.2.2: Core trigger engine. Manages extension-declared triggers // (event bus subscriptions + webhook receivers) and user-created // scheduled tasks (cron). All three converge to sandbox.Runner.CallEntryPoint. package triggers diff --git a/server/webhook/webhook.go b/server/webhook/webhook.go index 5cf7312..b7c3226 100644 --- a/server/webhook/webhook.go +++ b/server/webhook/webhook.go @@ -1,8 +1,6 @@ // 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. -// v0.28.0: Added RunID, TokensUsed fields (D1 audit fix). package webhook import ( diff --git a/server/workflow/engine.go b/server/workflow/engine.go index cb3ba53..80fe246 100644 --- a/server/workflow/engine.go +++ b/server/workflow/engine.go @@ -158,7 +158,7 @@ func (e *Engine) advanceInternal(ctx context.Context, instanceID string, stageDa } currentStage := stages[currentOrdinal] - // ── Validation gate (v0.3.4) ────────────── + // ── Validation gate ────────────── sc := ParseStageConfig(currentStage.StageConfig) if sc.Validation != nil && sc.Validation.RequiredApprovals > 0 { // Check for rejections first @@ -296,7 +296,7 @@ func (e *Engine) Cancel(ctx context.Context, instanceID string, userID string) e return nil } -// ── Public Entry (v0.3.3) ─────────────────── +// ── Public Entry ─────────────────── // StartPublic creates an instance for a public_link workflow without authentication. // The caller is identified as "public:". 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) } -// ── Signoffs (v0.3.4) ───────────────────────── +// ── Signoffs ───────────────────────── // 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) { diff --git a/src/dev.html b/src/dev.html deleted file mode 100644 index 80f2356..0000000 --- a/src/dev.html +++ /dev/null @@ -1,676 +0,0 @@ - - - - - - Switchboard Core — Dev Gallery - - - - - - -
- - - diff --git a/src/js/__tests__/auth-resilience.test.js b/src/js/__tests__/auth-resilience.test.js index 3d74feb..445d82c 100644 --- a/src/js/__tests__/auth-resilience.test.js +++ b/src/js/__tests__/auth-resilience.test.js @@ -243,7 +243,6 @@ describe('init() profile gate', () => { }); // ── 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, // the REST client's 401 handler must NOT redirect to /login (we're // already there). boot() handles 401 gracefully on its own. diff --git a/src/js/__tests__/extensions-builtin.test.js b/src/js/__tests__/extensions-builtin.test.js index 2985f36..ee6684b 100644 --- a/src/js/__tests__/extensions-builtin.test.js +++ b/src/js/__tests__/extensions-builtin.test.js @@ -5,7 +5,6 @@ const path = require('path'); const { createBrowserContext, loadSource, SRC } = require('./helpers'); const vm = require('vm'); -// v0.37.10: extensions.js deleted — extension system moves to Preact in v0.37.12. // Skip all tests if the source file doesn't exist. const EXTENSIONS_EXISTS = fs.existsSync(path.join(SRC, 'extensions.js')); const maybeDescribe = EXTENSIONS_EXISTS ? describe : describe.skip; diff --git a/src/js/__tests__/extensions.test.js b/src/js/__tests__/extensions.test.js index 9fdbc36..37d6e85 100644 --- a/src/js/__tests__/extensions.test.js +++ b/src/js/__tests__/extensions.test.js @@ -5,7 +5,6 @@ const path = require('path'); const { createBrowserContext, loadSource, SRC } = require('./helpers'); const vm = require('vm'); -// v0.37.10: extensions.js deleted — extension system moves to Preact in v0.37.12. // Skip all tests if the source file doesn't exist. const EXTENSIONS_EXISTS = fs.existsSync(path.join(SRC, 'extensions.js')); const maybeDescribe = EXTENSIONS_EXISTS ? describe : describe.skip; diff --git a/src/js/__tests__/helpers.js b/src/js/__tests__/helpers.js index b6c4ef8..2b9b0a4 100644 --- a/src/js/__tests__/helpers.js +++ b/src/js/__tests__/helpers.js @@ -5,7 +5,6 @@ // simulated browser environment so tests run // against the ACTUAL frontend code. // -// v0.37.10: Removed loadAppModules (api.js + app.js deleted). // ========================================== const fs = require('fs'); @@ -108,7 +107,7 @@ function createBrowserContext(overrides = {}) { function loadSource(sandbox, filename) { const filepath = path.join(SRC, filename); if (!fs.existsSync(filepath)) { - throw new Error(`Source file not found: ${filename} (deleted in v0.37.10?)`); + throw new Error(`Source file not found: ${filename} (deleted?)`); } const code = fs.readFileSync(filepath, 'utf-8'); const ctx = vm.createContext(sandbox); @@ -129,7 +128,6 @@ function readSourceSafe(filename) { /** * Extract the model-processing transform from fetchModels (app-state.js). * This is the core mapping logic that converts API response → App.models. - * v0.22.8: unified persona naming — no more preset aliases. */ function processModelsResponse(data, hiddenModels = new Set()) { return (data.data || data.models || []).map(m => { diff --git a/src/js/__tests__/policy-gating.test.js b/src/js/__tests__/policy-gating.test.js index 6f56972..2ff4692 100644 --- a/src/js/__tests__/policy-gating.test.js +++ b/src/js/__tests__/policy-gating.test.js @@ -7,13 +7,9 @@ // policy exists but the frontend doesn't // check it. // -// v0.22.5: Updated for server-rendered Go templates. -// v0.37.5: Settings surface moved to Preact — legacy SPA tests // replaced with component source audits. -// v0.37.10: Legacy SPA source audit removed (ui-core.js, app.js, // pages.js, settings-handlers.js, ui-admin.js all deleted). // Policy gating now verified via Preact surfaces + admin templates. -// v0.37.12: Admin Go templates deleted (Preact since v0.37.6). Template // element ID assertions removed — Preact component source audits // at the bottom of this file cover the same policy keys. // @@ -130,10 +126,9 @@ describe('Team member dropdown population', () => { }); // ── Kernel surface template ────────────────── -// v0.1.0: Chat surface removed. Verify admin mount exists and // old SPA scaffold is gone. -describe('Kernel surface templates (v0.1.0)', () => { +describe('Kernel surface templates', () => { const templateSrc = readAllTemplates(); it('admin-mount div exists', () => { @@ -153,7 +148,6 @@ describe('Kernel surface templates (v0.1.0)', () => { }); // ── Admin Preact surface ───────────────────── -// v0.37.6: Admin surface handles settings save via Preact. // Verify the admin surface has policy key handling. describe('Admin Preact surface handles settings', () => { diff --git a/src/js/debug.js b/src/js/debug.js index d108077..34384f9 100644 --- a/src/js/debug.js +++ b/src/js/debug.js @@ -1,11 +1,11 @@ // ========================================== -// Debug Bootstrap (v0.37.18) +// Debug Bootstrap // ========================================== // Thin entry point: initializes the debug engine (synchronous, // captures early errors) then mounts the Preact debug modal // after Preact globals are available. // -// Replaces the 673-line imperative debug.js from v0.37.14. +// Replaces the 673-line imperative debug.js. // Engine: src/js/sw/components/debug/engine.js // Modal: src/js/sw/components/debug/index.js diff --git a/src/js/sw/components/debug/badge.js b/src/js/sw/components/debug/badge.js index d6ca88e..83bc2a2 100644 --- a/src/js/sw/components/debug/badge.js +++ b/src/js/sw/components/debug/badge.js @@ -4,7 +4,6 @@ // Bug badge that shows error count. Subscribes to engine // for reactive updates. // -// v0.37.18: Preact rebuild from debug.js _updateBadge(). const html = window.html; const { useEffect } = window.hooks; diff --git a/src/js/sw/components/debug/console-tab.js b/src/js/sw/components/debug/console-tab.js index 1b5bd8d..2835e71 100644 --- a/src/js/sw/components/debug/console-tab.js +++ b/src/js/sw/components/debug/console-tab.js @@ -4,7 +4,6 @@ // Filterable console log display with type coloring, // elapsed timestamps, and auto-scroll. // -// v0.37.18: Preact rebuild from debug.js _renderConsoleTab(). const html = window.html; const { useState, useMemo, useRef, useEffect } = window.hooks; diff --git a/src/js/sw/components/debug/engine.js b/src/js/sw/components/debug/engine.js index dd02bf3..d0b8f8f 100644 --- a/src/js/sw/components/debug/engine.js +++ b/src/js/sw/components/debug/engine.js @@ -5,7 +5,6 @@ // Singleton — init() must run before SDK boot to capture early errors. // UI-agnostic: Preact components subscribe via .subscribe(). // -// v0.37.18: Extracted from debug.js (v0.37.14). // // Exports: debugEngine (singleton) diff --git a/src/js/sw/components/debug/index.js b/src/js/sw/components/debug/index.js index 4b66509..77dbdba 100644 --- a/src/js/sw/components/debug/index.js +++ b/src/js/sw/components/debug/index.js @@ -5,7 +5,6 @@ // Global overlay — not a routed surface. // Mounts via mountDebugModal(el, engine). // -// v0.37.18: Preact rebuild of debug modal (CR P2-5). import { debugEngine } from './engine.js'; import { ConsoleTab } from './console-tab.js'; diff --git a/src/js/sw/components/debug/network-tab.js b/src/js/sw/components/debug/network-tab.js index 1ab0ca5..0c63696 100644 --- a/src/js/sw/components/debug/network-tab.js +++ b/src/js/sw/components/debug/network-tab.js @@ -4,7 +4,6 @@ // Fetch log with expandable request/response details. // Newest entries first. // -// v0.37.18: Preact rebuild from debug.js _renderNetworkTab(). const html = window.html; const { useState } = window.hooks; diff --git a/src/js/sw/components/debug/repl-tab.js b/src/js/sw/components/debug/repl-tab.js index 102ad67..cbe6ef5 100644 --- a/src/js/sw/components/debug/repl-tab.js +++ b/src/js/sw/components/debug/repl-tab.js @@ -5,7 +5,6 @@ // command history, and collapsible JSON output. // Admin-gated OR ?debug=1 URL param. // -// v0.37.18: Preact rebuild from repl.js (v0.37.14). const html = window.html; const { useState, useRef, useEffect, useCallback } = window.hooks; diff --git a/src/js/sw/components/debug/state-tab.js b/src/js/sw/components/debug/state-tab.js index 956ae12..9dfed04 100644 --- a/src/js/sw/components/debug/state-tab.js +++ b/src/js/sw/components/debug/state-tab.js @@ -3,7 +3,6 @@ // ========================================== // Displays current application state snapshot as formatted JSON. // -// v0.37.18: Preact rebuild from debug.js _renderStateTab(). const html = window.html; const { useMemo } = window.hooks; diff --git a/src/js/sw/sdk/api-domains.js b/src/js/sw/sdk/api-domains.js index 7d373b5..c863f8f 100644 --- a/src/js/sw/sdk/api-domains.js +++ b/src/js/sw/sdk/api-domains.js @@ -131,13 +131,13 @@ export function createDomains(restClient) { updateWorkflow: (id, wfId, data) => rc.put(`/api/v1/teams/${id}/workflows/${wfId}`, data), deleteWorkflow: (id, wfId) => rc.del(`/api/v1/teams/${id}/workflows/${wfId}`), publishWorkflow: (id, wfId) => rc.post(`/api/v1/teams/${id}/workflows/${wfId}/publish`, {}), - // Team workflow stages (v0.37.15 — FE wiring for existing BE routes) + // Team workflow stages workflowStages: (id, wfId) => rc.get(`/api/v1/teams/${id}/workflows/${wfId}/stages`), createWorkflowStage: (id, wfId, data) => rc.post(`/api/v1/teams/${id}/workflows/${wfId}/stages`, data), updateWorkflowStage: (id, wfId, sid, data) => rc.put(`/api/v1/teams/${id}/workflows/${wfId}/stages/${sid}`, data), deleteWorkflowStage: (id, wfId, sid) => rc.del(`/api/v1/teams/${id}/workflows/${wfId}/stages/${sid}`), reorderWorkflowStages: (id, wfId, ids) => rc.patch(`/api/v1/teams/${id}/workflows/${wfId}/stages/reorder`, { ordered_ids: ids }), - // Adopt global workflows (v0.3.6) + // Adopt global workflows availableWorkflows: (id) => rc.get(`/api/v1/teams/${id}/workflows/available`), adoptWorkflow: (id, wfId) => rc.post(`/api/v1/teams/${id}/workflows/${wfId}/adopt`, {}), }, @@ -239,7 +239,6 @@ export function createDomains(restClient) { del: (id) => rc.del(`/api/v1/admin/extensions/${id}`), }, - // v0.38.1: Global connections connections: crud(rc, '/api/v1/admin/connections'), packages: { @@ -251,20 +250,18 @@ export function createDomains(restClient) { del: (id) => rc.del(`/api/v1/admin/packages/${id}`), settings: (id) => rc.get(`/api/v1/admin/packages/${id}/settings`), updateSettings: (id, data) => rc.put(`/api/v1/admin/packages/${id}/settings`, data), - dependencies: (id) => rc.get(`/api/v1/admin/packages/${id}/dependencies`), // v0.38.2 - consumers: (id) => rc.get(`/api/v1/admin/packages/${id}/consumers`), // v0.38.2 - update: (id, file) => rc.upload(`/api/v1/admin/packages/${id}/update`, file), // v0.5.4 - exportPkg: (id) => `/api/v1/admin/packages/${id}/export`, // v0.5.4 (URL for window.open) + dependencies: (id) => rc.get(`/api/v1/admin/packages/${id}/dependencies`), + consumers: (id) => rc.get(`/api/v1/admin/packages/${id}/consumers`), + update: (id, file) => rc.upload(`/api/v1/admin/packages/${id}/update`, file), + exportPkg: (id) => `/api/v1/admin/packages/${id}/export`, registry: () => rc.get('/api/v1/admin/packages/registry'), - registryInstall: (url) => rc.post('/api/v1/admin/packages/registry/install', { url }), - // v0.5.0: Extension permissions + registryInstall: (url) => rc.post('/api/v1/admin/packages/registry/install', { download_url: url }), permissions: (id) => rc.get(`/api/v1/admin/extensions/${id}/permissions`), grantPerm: (id, perm) => rc.post(`/api/v1/admin/extensions/${id}/permissions/${perm}/grant`, {}), revokePerm: (id, perm) => rc.post(`/api/v1/admin/extensions/${id}/permissions/${perm}/revoke`, {}), grantAllPerms: (id) => rc.post(`/api/v1/admin/extensions/${id}/permissions/grant-all`, {}), }, - // v0.38.2: Full dependency graph dependencies: { list: () => rc.get('/api/v1/admin/dependencies'), }, @@ -278,7 +275,6 @@ export function createDomains(restClient) { del: (id) => rc.del(`/api/v1/admin/surfaces/${id}`), }, - // v0.6.1: Backup/Restore backup: { create: (opts) => rc.post('/api/v1/admin/backup' + _qs(opts)), list: () => rc.get('/api/v1/admin/backups'), diff --git a/src/js/sw/sdk/can.js b/src/js/sw/sdk/can.js index 8b4992e..9c2293c 100644 --- a/src/js/sw/sdk/can.js +++ b/src/js/sw/sdk/can.js @@ -27,7 +27,6 @@ export function createCan(authRef) { /** * Is the current user a platform admin? - * v0.2.0: Uses RBAC grant instead of legacy role field. * @returns {boolean} */ function isAdmin() { diff --git a/src/js/sw/sdk/index.js b/src/js/sw/sdk/index.js index fea44d2..8d44b50 100644 --- a/src/js/sw/sdk/index.js +++ b/src/js/sw/sdk/index.js @@ -104,7 +104,7 @@ export async function boot() { sw.slots = slots; sw.actions = actions; - // Realtime — room-scoped pub/sub over WebSocket (v0.5.0) + // Realtime — room-scoped pub/sub over WebSocket sw.realtime = realtime; // Shell helpers — imperative confirm/prompt backed by primitives diff --git a/src/js/sw/shell/surface-viewport.js b/src/js/sw/shell/surface-viewport.js index 479449a..d0c1f9d 100644 --- a/src/js/sw/shell/surface-viewport.js +++ b/src/js/sw/shell/surface-viewport.js @@ -1,7 +1,7 @@ /** * SurfaceViewport — container where the active surface renders * - * Deliberately thin for v0.37.4. Error boundaries and + * Deliberately thin. Error boundaries and * surface transitions will be added in later versions. */ const { html } = window; diff --git a/src/js/sw/surfaces/admin/connections.js b/src/js/sw/surfaces/admin/connections.js index 80f847c..1371b0f 100644 --- a/src/js/sw/surfaces/admin/connections.js +++ b/src/js/sw/surfaces/admin/connections.js @@ -1,6 +1,5 @@ /** * Admin > Connections — global extension connection CRUD - * v0.38.1: Scoped credential management for extensions */ const { html } = window; const { useState, useEffect, useCallback } = hooks; diff --git a/src/js/sw/surfaces/admin/index.js b/src/js/sw/surfaces/admin/index.js index 9653995..5ab1c5c 100644 --- a/src/js/sw/surfaces/admin/index.js +++ b/src/js/sw/surfaces/admin/index.js @@ -4,7 +4,7 @@ * Reads globals: * __SECTION__ — active section name (string) * __BASE__ — base path - * __CONFIG_SECTIONS__ — v0.38.3: extension config sections (array|null) + * __CONFIG_SECTIONS__ * * Layout: topbar (back + category tabs) + body (sidebar nav + content area). * All 24+ sections are native Preact components loaded lazily. @@ -34,7 +34,7 @@ const ADMIN_LABELS = { audit: 'Audit', }; -// ── v0.38.3: Extension config sections ────── +// ── Extension config sections ────── // Packages declare config_section in their manifest targeting "admin". // We merge them into the appropriate category and section module map. const _configSections = window.__CONFIG_SECTIONS__ || []; @@ -72,7 +72,6 @@ const sectionModules = { audit: () => import(`./audit.js${_v}`), }; -// v0.38.3: Register dynamic section loaders for extension config sections for (const cs of _configSections) { const pkgId = cs.package_id; const component = cs.component || 'js/config.js'; diff --git a/src/js/sw/surfaces/admin/packages.js b/src/js/sw/surfaces/admin/packages.js index 7a7b906..8b3e2c7 100644 --- a/src/js/sw/surfaces/admin/packages.js +++ b/src/js/sw/surfaces/admin/packages.js @@ -46,7 +46,7 @@ export default function PackagesSection() { const [registryPkgs, setRegistryPkgs] = useState([]); const [registryLoading, setRegistryLoading] = useState(false); const [installing, setInstalling] = useState(false); - const [permsId, setPermsId] = useState(null); // v0.5.0: permissions drawer + const [permsId, setPermsId] = useState(null); const [perms, setPerms] = useState([]); const BASE = window.__BASE__ || ''; @@ -162,7 +162,7 @@ export default function PackagesSection() { } catch (e) { sw.toast(e.message, 'error'); } } - // ── Permissions drawer (v0.5.0) ──────────── + // ── Permissions drawer ──────────── async function togglePerms(pkgId) { if (permsId === pkgId) { setPermsId(null); setPerms([]); return; } try { @@ -323,7 +323,7 @@ export default function PackagesSection() { `} - ${/* ── Inline permissions drawer (v0.5.0) ── */``} + ${/* ── Inline permissions drawer ── */``} ${permsId === pkg.id && html`
${perms.length === 0 diff --git a/src/js/sw/surfaces/admin/settings.js b/src/js/sw/surfaces/admin/settings.js index ddf907f..7092766 100644 --- a/src/js/sw/surfaces/admin/settings.js +++ b/src/js/sw/surfaces/admin/settings.js @@ -36,6 +36,7 @@ export default function SettingsSection() { message_variant: cfg_.message?.variant || 'info', footer_enabled: !!cfg_.footer?.enabled, footer_text: cfg_.footer?.text || '', + package_registry_url: cfg_.package_registry?.url || '', }); setVault(v); } catch (e) { sw.toast(e.message, 'error'); } @@ -78,6 +79,9 @@ export default function SettingsSection() { text: cfg.footer_text, }}); + // Package Registry + await sw.api.admin.settings.update('package_registry', { value: { url: cfg.package_registry_url } }); + sw.toast('Settings saved', 'success'); } catch (e) { sw.toast(e.message, 'error'); } finally { setSaving(false); } @@ -166,6 +170,13 @@ export default function SettingsSection() { }
+

Package Registry

+
+ set('package_registry_url', e.target.value)} placeholder="https://registry.example.com/registry.json" /> +
+ URL to a JSON registry index. Enables browsing and one-click install under Packages > Registry. +
+

Email

diff --git a/src/js/sw/surfaces/docs/index.js b/src/js/sw/surfaces/docs/index.js index 4dac450..d50f577 100644 --- a/src/js/sw/surfaces/docs/index.js +++ b/src/js/sw/surfaces/docs/index.js @@ -1,5 +1,5 @@ /** - * DocsSurface — builtin documentation viewer (v0.6.2) + * DocsSurface — builtin documentation viewer * * Reads globals: * __SECTION__ — active doc slug (e.g. "GETTING-STARTED") @@ -8,7 +8,6 @@ * Fetches markdown from GET /api/v1/docs/:name, renders with * a simple markdown-to-HTML converter. Sidebar lists all docs. * - * v0.6.2: dark mode fix, topbar navigation, error handling. */ const { html } = window; const { useState, useEffect, useCallback, useMemo } = hooks; diff --git a/src/js/sw/surfaces/settings/connections.js b/src/js/sw/surfaces/settings/connections.js index 13a38f9..4ab1c83 100644 --- a/src/js/sw/surfaces/settings/connections.js +++ b/src/js/sw/surfaces/settings/connections.js @@ -1,6 +1,5 @@ /** * ConnectionsSection — personal extension connection CRUD - * v0.38.1: Scoped credential management for extensions */ const { html } = window; const { useState, useEffect, useCallback } = hooks; diff --git a/src/js/sw/surfaces/settings/index.js b/src/js/sw/surfaces/settings/index.js index 759c548..22ac63a 100644 --- a/src/js/sw/surfaces/settings/index.js +++ b/src/js/sw/surfaces/settings/index.js @@ -4,7 +4,7 @@ * Reads globals: * __SECTION__ — active section name (string) * __BASE__ — base path - * __CONFIG_SECTIONS__ — v0.38.3: extension config sections (array|null) + * __CONFIG_SECTIONS__ * * Layout: topbar + left nav + content area (same CSS classes as before). * All sections are native Preact components loaded lazily. @@ -44,7 +44,7 @@ const SECTION_TITLES = { connections: 'Connections', notifications: 'Notifications', }; -// ── v0.38.3: Extension config sections ────── +// ── Extension config sections ────── // Packages declare config_section in their manifest. The backend passes // matching entries via __CONFIG_SECTIONS__. We merge them into the nav // and section module map for lazy loading. diff --git a/src/js/sw/surfaces/team-admin/connections.js b/src/js/sw/surfaces/team-admin/connections.js index 5d9a6b9..83d6a24 100644 --- a/src/js/sw/surfaces/team-admin/connections.js +++ b/src/js/sw/surfaces/team-admin/connections.js @@ -1,6 +1,5 @@ /** * Team Admin > Connections — team-scoped extension connection CRUD - * v0.38.1: Scoped credential management for extensions */ const { html } = window; const { useState, useEffect, useCallback } = hooks; diff --git a/src/js/sw/surfaces/team-admin/index.js b/src/js/sw/surfaces/team-admin/index.js index 04e4513..eaf224a 100644 --- a/src/js/sw/surfaces/team-admin/index.js +++ b/src/js/sw/surfaces/team-admin/index.js @@ -4,7 +4,7 @@ * Reads globals: * __SECTION__ — active section name (string) * __BASE__ — base path - * __CONFIG_SECTIONS__ — v0.38.3: extension config sections (array|null) + * __CONFIG_SECTIONS__ * * Layout: topbar (back + team name) + sidebar nav + content area. * All 10+ sections are native Preact components loaded lazily. @@ -36,7 +36,7 @@ const sectionModules = { activity: () => import('./activity.js'), }; -// ── v0.38.3: Extension config sections ────── +// ── Extension config sections ────── const _configSections = window.__CONFIG_SECTIONS__ || []; const _base = window.__BASE__ || ''; for (const cs of _configSections) { diff --git a/src/js/sw/surfaces/team-admin/members.js b/src/js/sw/surfaces/team-admin/members.js index 8df11e7..ccadd87 100644 --- a/src/js/sw/surfaces/team-admin/members.js +++ b/src/js/sw/surfaces/team-admin/members.js @@ -1,6 +1,5 @@ /** * Team Admin > Members - * v0.3.4: custom team roles support */ const { html } = window; const { useState, useEffect, useCallback } = hooks; diff --git a/src/js/sw/surfaces/team-admin/workflows.js b/src/js/sw/surfaces/team-admin/workflows.js index c18c199..89e2023 100644 --- a/src/js/sw/surfaces/team-admin/workflows.js +++ b/src/js/sw/surfaces/team-admin/workflows.js @@ -1,5 +1,5 @@ /** - * Team Admin > Workflows — v0.37.15 rewrite + * Team Admin > Workflows * * Tab layout: Workflows | Assignments | Monitor * - Workflows: CRUD + inline stage editor (E2) @@ -365,7 +365,6 @@ function StageForm({ stage, teams, onSave, onCancel }) { stage?.branch_rules ? (typeof stage.branch_rules === 'string' ? stage.branch_rules : JSON.stringify(stage.branch_rules, null, 2)) : '' ); - // v0.3.4: stage_config fields const sc = stage?.stage_config ? (typeof stage.stage_config === 'string' ? JSON.parse(stage.stage_config || '{}') : stage.stage_config) : {}; const [requiredRole, setRequiredRole] = useState(sc.required_role || ''); const [valApprovals, setValApprovals] = useState(sc.validation?.required_approvals || ''); @@ -664,7 +663,7 @@ function MonitorTab({ teamId }) { `; } -// ── Signoff Panel (v0.3.4) ────────────────── +// ── Signoff Panel ────────────────── function SignoffPanel({ instanceId, teamId }) { const [signoffs, setSignoffs] = useState([]); diff --git a/src/sw.js b/src/sw.js index e20063a..6ea830d 100644 --- a/src/sw.js +++ b/src/sw.js @@ -11,7 +11,7 @@ const CACHE_NAME = 'switchboard-%%APP_VERSION%%-%%BUILD_HASH%%'; -// App shell files to pre-cache on install (cleaned up in v0.37.12) +// App shell files to pre-cache on install (cleaned up) const SHELL_FILES = [ './', './index.html', @@ -35,11 +35,8 @@ const SHELL_FILES = [ './css/sw-chat-surface.css', './css/sw-notes-pane.css', './css/sw-notes-surface.css', - // JS — debug tooling (v0.37.18: repl.js absorbed into Preact debug components) + // JS — debug tooling './js/debug.js', - // Vendor - './vendor/marked.min.js', - './vendor/purify.min.js', // Static assets './favicon.svg', './favicon-light.svg', diff --git a/src/vendor/marked.min.js b/src/vendor/marked.min.js deleted file mode 100644 index 0f9417f..0000000 --- a/src/vendor/marked.min.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * marked v16.3.0 - a markdown parser - * Copyright (c) 2011-2025, Christopher Jeffrey. (MIT Licensed) - * https://github.com/markedjs/marked - */ - -/** - * DO NOT EDIT THIS FILE - * The code in this file is generated from files in ./src/ - */ -(function(g,f){if(typeof exports=="object"&&typeof module<"u"){module.exports=f()}else if("function"==typeof define && define.amd){define("marked",f)}else {g["marked"]=f()}}(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this,function(){var exports={};var __exports=exports;var module={exports}; -"use strict";var G=Object.defineProperty;var be=Object.getOwnPropertyDescriptor;var Re=Object.getOwnPropertyNames;var Oe=Object.prototype.hasOwnProperty;var Te=(a,e)=>{for(var t in e)G(a,t,{get:e[t],enumerable:!0})},we=(a,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of Re(e))!Oe.call(a,r)&&r!==t&&G(a,r,{get:()=>e[r],enumerable:!(n=be(e,r))||n.enumerable});return a};var ye=a=>we(G({},"__esModule",{value:!0}),a);var dt={};Te(dt,{Hooks:()=>S,Lexer:()=>x,Marked:()=>A,Parser:()=>b,Renderer:()=>P,TextRenderer:()=>$,Tokenizer:()=>y,defaults:()=>O,getDefaults:()=>_,lexer:()=>ht,marked:()=>d,options:()=>it,parse:()=>pt,parseInline:()=>ut,parser:()=>ct,setOptions:()=>ot,use:()=>at,walkTokens:()=>lt});module.exports=ye(dt);function _(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var O=_();function N(a){O=a}var C={exec:()=>null};function h(a,e=""){let t=typeof a=="string"?a:a.source,n={replace:(r,i)=>{let s=typeof i=="string"?i:i.source;return s=s.replace(m.caret,"$1"),t=t.replace(r,s),n},getRegex:()=>new RegExp(t,e)};return n}var m={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:a=>new RegExp(`^( {0,3}${a})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:a=>new RegExp(`^ {0,${Math.min(3,a-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),hrRegex:a=>new RegExp(`^ {0,${Math.min(3,a-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:a=>new RegExp(`^ {0,${Math.min(3,a-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:a=>new RegExp(`^ {0,${Math.min(3,a-1)}}#`),htmlBeginRegex:a=>new RegExp(`^ {0,${Math.min(3,a-1)}}<(?:[a-z].*>|!--)`,"i")},Pe=/^(?:[ \t]*(?:\n|$))+/,Se=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,$e=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,I=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,_e=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,F=/(?:[*+-]|\d{1,9}[.)])/,ie=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,oe=h(ie).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),Le=h(ie).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),Q=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,Me=/^[^\n]+/,U=/(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/,ze=h(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",U).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),Ae=h(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,F).getRegex(),v="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",K=/|$))/,Ee=h("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",K).replace("tag",v).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),ae=h(Q).replace("hr",I).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),Ce=h(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",ae).getRegex(),W={blockquote:Ce,code:Se,def:ze,fences:$e,heading:_e,hr:I,html:Ee,lheading:oe,list:Ae,newline:Pe,paragraph:ae,table:C,text:Me},se=h("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",I).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),Ie={...W,lheading:Le,table:se,paragraph:h(Q).replace("hr",I).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",se).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex()},Be={...W,html:h(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",K).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:C,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:h(Q).replace("hr",I).replace("heading",` *#{1,6} *[^ -]`).replace("lheading",oe).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},qe=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,ve=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,le=/^( {2,}|\\)\n(?!\s*$)/,De=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\]*?>/g,ce=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,je=h(ce,"u").replace(/punct/g,D).getRegex(),Fe=h(ce,"u").replace(/punct/g,pe).getRegex(),he="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",Qe=h(he,"gu").replace(/notPunctSpace/g,ue).replace(/punctSpace/g,X).replace(/punct/g,D).getRegex(),Ue=h(he,"gu").replace(/notPunctSpace/g,Ge).replace(/punctSpace/g,Ze).replace(/punct/g,pe).getRegex(),Ke=h("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,ue).replace(/punctSpace/g,X).replace(/punct/g,D).getRegex(),We=h(/\\(punct)/,"gu").replace(/punct/g,D).getRegex(),Xe=h(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),Je=h(K).replace("(?:-->|$)","-->").getRegex(),Ve=h("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",Je).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),q=/(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`[^`]*`|[^\[\]\\`])*?/,Ye=h(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",q).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),de=h(/^!?\[(label)\]\[(ref)\]/).replace("label",q).replace("ref",U).getRegex(),ke=h(/^!?\[(ref)\](?:\[\])?/).replace("ref",U).getRegex(),et=h("reflink|nolink(?!\\()","g").replace("reflink",de).replace("nolink",ke).getRegex(),J={_backpedal:C,anyPunctuation:We,autolink:Xe,blockSkip:Ne,br:le,code:ve,del:C,emStrongLDelim:je,emStrongRDelimAst:Qe,emStrongRDelimUnd:Ke,escape:qe,link:Ye,nolink:ke,punctuation:He,reflink:de,reflinkSearch:et,tag:Ve,text:De,url:C},tt={...J,link:h(/^!?\[(label)\]\((.*?)\)/).replace("label",q).getRegex(),reflink:h(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",q).getRegex()},j={...J,emStrongRDelimAst:Ue,emStrongLDelim:Fe,url:h(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},ge=a=>rt[a];function w(a,e){if(e){if(m.escapeTest.test(a))return a.replace(m.escapeReplace,ge)}else if(m.escapeTestNoEncode.test(a))return a.replace(m.escapeReplaceNoEncode,ge);return a}function V(a){try{a=encodeURI(a).replace(m.percentDecode,"%")}catch{return null}return a}function Y(a,e){let t=a.replace(m.findPipe,(i,s,o)=>{let l=!1,u=s;for(;--u>=0&&o[u]==="\\";)l=!l;return l?"|":" |"}),n=t.split(m.splitPipe),r=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length0?-2:-1}function me(a,e,t,n,r){let i=e.href,s=e.title||null,o=a[1].replace(r.other.outputLinkReplace,"$1");n.state.inLink=!0;let l={type:a[0].charAt(0)==="!"?"image":"link",raw:t,href:i,title:s,text:o,tokens:n.inlineTokens(o)};return n.state.inLink=!1,l}function st(a,e,t){let n=a.match(t.other.indentCodeCompensation);if(n===null)return e;let r=n[1];return e.split(` -`).map(i=>{let s=i.match(t.other.beginningSpace);if(s===null)return i;let[o]=s;return o.length>=r.length?i.slice(r.length):i}).join(` -`)}var y=class{options;rules;lexer;constructor(e){this.options=e||O}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?n:z(n,` -`)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],r=st(n,t[3]||"",this.rules);return{type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:r}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let r=z(n,"#");(this.options.pedantic||!r||this.rules.other.endingSpaceChar.test(r))&&(n=r.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:z(t[0],` -`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=z(t[0],` -`).split(` -`),r="",i="",s=[];for(;n.length>0;){let o=!1,l=[],u;for(u=0;u1,i={type:"list",raw:"",ordered:r,start:r?+n.slice(0,-1):"",loose:!1,items:[]};n=r?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=r?n:"[*+-]");let s=this.rules.other.listItemRegex(n),o=!1;for(;e;){let u=!1,p="",c="";if(!(t=s.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let f=t[2].split(` -`,1)[0].replace(this.rules.other.listReplaceTabs,H=>" ".repeat(3*H.length)),k=e.split(` -`,1)[0],R=!f.trim(),g=0;if(this.options.pedantic?(g=2,c=f.trimStart()):R?g=t[1].length+1:(g=t[2].search(this.rules.other.nonSpaceChar),g=g>4?1:g,c=f.slice(g),g+=t[1].length),R&&this.rules.other.blankLine.test(k)&&(p+=k+` -`,e=e.substring(k.length+1),u=!0),!u){let H=this.rules.other.nextBulletRegex(g),te=this.rules.other.hrRegex(g),ne=this.rules.other.fencesBeginRegex(g),re=this.rules.other.headingBeginRegex(g),xe=this.rules.other.htmlBeginRegex(g);for(;e;){let Z=e.split(` -`,1)[0],E;if(k=Z,this.options.pedantic?(k=k.replace(this.rules.other.listReplaceNesting," "),E=k):E=k.replace(this.rules.other.tabCharGlobal," "),ne.test(k)||re.test(k)||xe.test(k)||H.test(k)||te.test(k))break;if(E.search(this.rules.other.nonSpaceChar)>=g||!k.trim())c+=` -`+E.slice(g);else{if(R||f.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||ne.test(f)||re.test(f)||te.test(f))break;c+=` -`+k}!R&&!k.trim()&&(R=!0),p+=Z+` -`,e=e.substring(Z.length+1),f=E.slice(g)}}i.loose||(o?i.loose=!0:this.rules.other.doubleBlankLine.test(p)&&(o=!0));let T=null,ee;this.options.gfm&&(T=this.rules.other.listIsTask.exec(c),T&&(ee=T[0]!=="[ ] ",c=c.replace(this.rules.other.listReplaceTask,""))),i.items.push({type:"list_item",raw:p,task:!!T,checked:ee,loose:!1,text:c,tokens:[]}),i.raw+=p}let l=i.items.at(-1);if(l)l.raw=l.raw.trimEnd(),l.text=l.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let u=0;uf.type==="space"),c=p.length>0&&p.some(f=>this.rules.other.anyLine.test(f.raw));i.loose=c}if(i.loose)for(let u=0;u({text:l,tokens:this.lexer.inline(l),header:!1,align:s.align[u]})));return s}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===` -`?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let s=z(n.slice(0,-1),"\\");if((n.length-s.length)%2===0)return}else{let s=fe(t[2],"()");if(s===-2)return;if(s>-1){let l=(t[0].indexOf("!")===0?5:4)+t[1].length+s;t[2]=t[2].substring(0,s),t[0]=t[0].substring(0,l).trim(),t[3]=""}}let r=t[2],i="";if(this.options.pedantic){let s=this.rules.other.pedanticHrefTitle.exec(r);s&&(r=s[1],i=s[3])}else i=t[3]?t[3].slice(1,-1):"";return r=r.trim(),this.rules.other.startAngleBracket.test(r)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?r=r.slice(1):r=r.slice(1,-1)),me(t,{href:r&&r.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let r=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),i=t[r.toLowerCase()];if(!i){let s=n[0].charAt(0);return{type:"text",raw:s,text:s}}return me(n,i,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let r=this.rules.inline.emStrongLDelim.exec(e);if(!r||r[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(r[1]||r[2]||"")||!n||this.rules.inline.punctuation.exec(n)){let s=[...r[0]].length-1,o,l,u=s,p=0,c=r[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(c.lastIndex=0,t=t.slice(-1*e.length+s);(r=c.exec(t))!=null;){if(o=r[1]||r[2]||r[3]||r[4]||r[5]||r[6],!o)continue;if(l=[...o].length,r[3]||r[4]){u+=l;continue}else if((r[5]||r[6])&&s%3&&!((s+l)%3)){p+=l;continue}if(u-=l,u>0)continue;l=Math.min(l,l+u+p);let f=[...r[0]][0].length,k=e.slice(0,s+r.index+f+l);if(Math.min(s,l)%2){let g=k.slice(1,-1);return{type:"em",raw:k,text:g,tokens:this.lexer.inlineTokens(g)}}let R=k.slice(2,-2);return{type:"strong",raw:k,text:R,tokens:this.lexer.inlineTokens(R)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),r=this.rules.other.nonSpaceChar.test(n),i=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return r&&i&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){let t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,r;return t[2]==="@"?(n=t[1],r="mailto:"+n):(n=t[1],r=n),{type:"link",raw:t[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,r;if(t[2]==="@")n=t[0],r="mailto:"+n;else{let i;do i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(i!==t[0]);n=t[0],t[1]==="www."?r="http://"+t[0]:r=t[0]}return{type:"link",raw:t[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:n}}}};var x=class a{tokens;options;state;tokenizer;inlineQueue;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||O,this.options.tokenizer=this.options.tokenizer||new y,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let t={other:m,block:B.normal,inline:M.normal};this.options.pedantic?(t.block=B.pedantic,t.inline=M.pedantic):this.options.gfm&&(t.block=B.gfm,this.options.breaks?t.inline=M.breaks:t.inline=M.gfm),this.tokenizer.rules=t}static get rules(){return{block:B,inline:M}}static lex(e,t){return new a(t).lex(e)}static lexInline(e,t){return new a(t).inlineTokens(e)}lex(e){e=e.replace(m.carriageReturn,` -`),this.blockTokens(e,this.tokens);for(let t=0;t(r=s.call({lexer:this},e,t))?(e=e.substring(r.raw.length),t.push(r),!0):!1))continue;if(r=this.tokenizer.space(e)){e=e.substring(r.raw.length);let s=t.at(-1);r.raw.length===1&&s!==void 0?s.raw+=` -`:t.push(r);continue}if(r=this.tokenizer.code(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="paragraph"||s?.type==="text"?(s.raw+=(s.raw.endsWith(` -`)?"":` -`)+r.raw,s.text+=` -`+r.text,this.inlineQueue.at(-1).src=s.text):t.push(r);continue}if(r=this.tokenizer.fences(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.heading(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.hr(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.blockquote(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.list(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.html(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.def(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="paragraph"||s?.type==="text"?(s.raw+=(s.raw.endsWith(` -`)?"":` -`)+r.raw,s.text+=` -`+r.raw,this.inlineQueue.at(-1).src=s.text):this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title},t.push(r));continue}if(r=this.tokenizer.table(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.lheading(e)){e=e.substring(r.raw.length),t.push(r);continue}let i=e;if(this.options.extensions?.startBlock){let s=1/0,o=e.slice(1),l;this.options.extensions.startBlock.forEach(u=>{l=u.call({lexer:this},o),typeof l=="number"&&l>=0&&(s=Math.min(s,l))}),s<1/0&&s>=0&&(i=e.substring(0,s+1))}if(this.state.top&&(r=this.tokenizer.paragraph(i))){let s=t.at(-1);n&&s?.type==="paragraph"?(s.raw+=(s.raw.endsWith(` -`)?"":` -`)+r.raw,s.text+=` -`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=s.text):t.push(r),n=i.length!==e.length,e=e.substring(r.raw.length);continue}if(r=this.tokenizer.text(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="text"?(s.raw+=(s.raw.endsWith(` -`)?"":` -`)+r.raw,s.text+=` -`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=s.text):t.push(r);continue}if(e){let s="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(s);break}else throw new Error(s)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,r=null;if(this.tokens.links){let o=Object.keys(this.tokens.links);if(o.length>0)for(;(r=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)o.includes(r[0].slice(r[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,r.index)+"["+"a".repeat(r[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(r=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,r.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(r=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)n=n.slice(0,r.index)+"["+"a".repeat(r[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);n=this.options.hooks?.emStrongMask?.call({lexer:this},n)??n;let i=!1,s="";for(;e;){i||(s=""),i=!1;let o;if(this.options.extensions?.inline?.some(u=>(o=u.call({lexer:this},e,t))?(e=e.substring(o.raw.length),t.push(o),!0):!1))continue;if(o=this.tokenizer.escape(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.tag(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.link(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(o.raw.length);let u=t.at(-1);o.type==="text"&&u?.type==="text"?(u.raw+=o.raw,u.text+=o.text):t.push(o);continue}if(o=this.tokenizer.emStrong(e,n,s)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.codespan(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.br(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.del(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.autolink(e)){e=e.substring(o.raw.length),t.push(o);continue}if(!this.state.inLink&&(o=this.tokenizer.url(e))){e=e.substring(o.raw.length),t.push(o);continue}let l=e;if(this.options.extensions?.startInline){let u=1/0,p=e.slice(1),c;this.options.extensions.startInline.forEach(f=>{c=f.call({lexer:this},p),typeof c=="number"&&c>=0&&(u=Math.min(u,c))}),u<1/0&&u>=0&&(l=e.substring(0,u+1))}if(o=this.tokenizer.inlineText(l)){e=e.substring(o.raw.length),o.raw.slice(-1)!=="_"&&(s=o.raw.slice(-1)),i=!0;let u=t.at(-1);u?.type==="text"?(u.raw+=o.raw,u.text+=o.text):t.push(o);continue}if(e){let u="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(u);break}else throw new Error(u)}}return t}};var P=class{options;parser;constructor(e){this.options=e||O}space(e){return""}code({text:e,lang:t,escaped:n}){let r=(t||"").match(m.notSpaceStart)?.[0],i=e.replace(m.endingNewline,"")+` -`;return r?'
'+(n?i:w(i,!0))+`
-`:"
"+(n?i:w(i,!0))+`
-`}blockquote({tokens:e}){return`
-${this.parser.parse(e)}
-`}html({text:e}){return e}def(e){return""}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)} -`}hr(e){return`
-`}list(e){let t=e.ordered,n=e.start,r="";for(let o=0;o -`+r+" -`}listitem(e){let t="";if(e.task){let n=this.checkbox({checked:!!e.checked});e.loose?e.tokens[0]?.type==="paragraph"?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&e.tokens[0].tokens[0].type==="text"&&(e.tokens[0].tokens[0].text=n+" "+w(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • -`}checkbox({checked:e}){return"'}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    -`}table(e){let t="",n="";for(let i=0;i${r}`),` - -`+t+` -`+r+`
    -`}tablerow({text:e}){return` -${e} -`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+` -`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${w(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){let r=this.parser.parseInline(n),i=V(e);if(i===null)return r;e=i;let s='
    ",s}image({href:e,title:t,text:n,tokens:r}){r&&(n=this.parser.parseInline(r,this.parser.textRenderer));let i=V(e);if(i===null)return w(n);e=i;let s=`${n}{let o=i[s].flat(1/0);n=n.concat(this.walkTokens(o,t))}):i.tokens&&(n=n.concat(this.walkTokens(i.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let r={...n};if(r.async=this.defaults.async||r.async||!1,n.extensions&&(n.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let s=t.renderers[i.name];s?t.renderers[i.name]=function(...o){let l=i.renderer.apply(this,o);return l===!1&&(l=s.apply(this,o)),l}:t.renderers[i.name]=i.renderer}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let s=t[i.level];s?s.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level==="block"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level==="inline"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]))}"childTokens"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens)}),r.extensions=t),n.renderer){let i=this.defaults.renderer||new P(this.defaults);for(let s in n.renderer){if(!(s in i))throw new Error(`renderer '${s}' does not exist`);if(["options","parser"].includes(s))continue;let o=s,l=n.renderer[o],u=i[o];i[o]=(...p)=>{let c=l.apply(i,p);return c===!1&&(c=u.apply(i,p)),c||""}}r.renderer=i}if(n.tokenizer){let i=this.defaults.tokenizer||new y(this.defaults);for(let s in n.tokenizer){if(!(s in i))throw new Error(`tokenizer '${s}' does not exist`);if(["options","rules","lexer"].includes(s))continue;let o=s,l=n.tokenizer[o],u=i[o];i[o]=(...p)=>{let c=l.apply(i,p);return c===!1&&(c=u.apply(i,p)),c}}r.tokenizer=i}if(n.hooks){let i=this.defaults.hooks||new S;for(let s in n.hooks){if(!(s in i))throw new Error(`hook '${s}' does not exist`);if(["options","block"].includes(s))continue;let o=s,l=n.hooks[o],u=i[o];S.passThroughHooks.has(s)?i[o]=p=>{if(this.defaults.async&&S.passThroughHooksRespectAsync.has(s))return Promise.resolve(l.call(i,p)).then(f=>u.call(i,f));let c=l.call(i,p);return u.call(i,c)}:i[o]=(...p)=>{let c=l.apply(i,p);return c===!1&&(c=u.apply(i,p)),c}}r.hooks=i}if(n.walkTokens){let i=this.defaults.walkTokens,s=n.walkTokens;r.walkTokens=function(o){let l=[];return l.push(s.call(this,o)),i&&(l=l.concat(i.call(this,o))),l}}this.defaults={...this.defaults,...r}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return x.lex(e,t??this.defaults)}parser(e,t){return b.parse(e,t??this.defaults)}parseMarkdown(e){return(n,r)=>{let i={...r},s={...this.defaults,...i},o=this.onError(!!s.silent,!!s.async);if(this.defaults.async===!0&&i.async===!1)return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===null)return o(new Error("marked(): input parameter is undefined or null"));if(typeof n!="string")return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));s.hooks&&(s.hooks.options=s,s.hooks.block=e);let l=s.hooks?s.hooks.provideLexer():e?x.lex:x.lexInline,u=s.hooks?s.hooks.provideParser():e?b.parse:b.parseInline;if(s.async)return Promise.resolve(s.hooks?s.hooks.preprocess(n):n).then(p=>l(p,s)).then(p=>s.hooks?s.hooks.processAllTokens(p):p).then(p=>s.walkTokens?Promise.all(this.walkTokens(p,s.walkTokens)).then(()=>p):p).then(p=>u(p,s)).then(p=>s.hooks?s.hooks.postprocess(p):p).catch(o);try{s.hooks&&(n=s.hooks.preprocess(n));let p=l(n,s);s.hooks&&(p=s.hooks.processAllTokens(p)),s.walkTokens&&this.walkTokens(p,s.walkTokens);let c=u(p,s);return s.hooks&&(c=s.hooks.postprocess(c)),c}catch(p){return o(p)}}}onError(e,t){return n=>{if(n.message+=` -Please report this to https://github.com/markedjs/marked.`,e){let r="

    An error occurred:

    "+w(n.message+"",!0)+"
    ";return t?Promise.resolve(r):r}if(t)return Promise.reject(n);throw n}}};var L=new A;function d(a,e){return L.parse(a,e)}d.options=d.setOptions=function(a){return L.setOptions(a),d.defaults=L.defaults,N(d.defaults),d};d.getDefaults=_;d.defaults=O;d.use=function(...a){return L.use(...a),d.defaults=L.defaults,N(d.defaults),d};d.walkTokens=function(a,e){return L.walkTokens(a,e)};d.parseInline=L.parseInline;d.Parser=b;d.parser=b.parse;d.Renderer=P;d.TextRenderer=$;d.Lexer=x;d.lexer=x.lex;d.Tokenizer=y;d.Hooks=S;d.parse=d;var it=d.options,ot=d.setOptions,at=d.use,lt=d.walkTokens,ut=d.parseInline,pt=d,ct=b.parse,ht=x.lex; - -if(__exports != exports)module.exports = exports;return module.exports})); -//# sourceMappingURL=marked.umd.js.map diff --git a/src/vendor/purify.min.js b/src/vendor/purify.min.js deleted file mode 100644 index b472a86..0000000 --- a/src/vendor/purify.min.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! @license DOMPurify 3.2.4 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.4/LICENSE */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=R(Array.prototype.forEach),m=R(Array.prototype.lastIndexOf),p=R(Array.prototype.pop),f=R(Array.prototype.push),d=R(Array.prototype.splice),h=R(String.prototype.toLowerCase),g=R(String.prototype.toString),T=R(String.prototype.match),y=R(String.prototype.replace),E=R(String.prototype.indexOf),A=R(String.prototype.trim),_=R(Object.prototype.hasOwnProperty),S=R(RegExp.prototype.test),b=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:h;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function O(e){for(let t=0;t/gm),G=a(/\$\{[\w\W]*/gm),Y=a(/^data-[\-\w.\u00B7-\uFFFF]+$/),j=a(/^aria-[\-\w]+$/),X=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),q=a(/^(?:\w+script|data):/i),$=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),K=a(/^html$/i),V=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var Z=Object.freeze({__proto__:null,ARIA_ATTR:j,ATTR_WHITESPACE:$,CUSTOM_ELEMENT:V,DATA_ATTR:Y,DOCTYPE_NAME:K,ERB_EXPR:W,IS_ALLOWED_URI:X,IS_SCRIPT_OR_DATA:q,MUSTACHE_EXPR:B,TMPLIT_EXPR:G});const J=1,Q=3,ee=7,te=8,ne=9,oe=function(){return"undefined"==typeof window?null:window};var re=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:oe();const o=e=>t(e);if(o.version="3.2.4",o.removed=[],!n||!n.document||n.document.nodeType!==ne||!n.Element)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:R,Element:O,NodeFilter:B,NamedNodeMap:W=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:G,DOMParser:Y,trustedTypes:j}=n,q=O.prototype,$=v(q,"cloneNode"),V=v(q,"remove"),re=v(q,"nextSibling"),ie=v(q,"childNodes"),ae=v(q,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let le,ce="";const{implementation:se,createNodeIterator:ue,createDocumentFragment:me,getElementsByTagName:pe}=r,{importNode:fe}=a;let de={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};o.isSupported="function"==typeof e&&"function"==typeof ae&&se&&void 0!==se.createHTMLDocument;const{MUSTACHE_EXPR:he,ERB_EXPR:ge,TMPLIT_EXPR:Te,DATA_ATTR:ye,ARIA_ATTR:Ee,IS_SCRIPT_OR_DATA:Ae,ATTR_WHITESPACE:_e,CUSTOM_ELEMENT:Se}=Z;let{IS_ALLOWED_URI:be}=Z,Ne=null;const Re=w({},[...L,...C,...x,...k,...U]);let we=null;const Oe=w({},[...z,...P,...H,...F]);let De=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ve=null,Le=null,Ce=!0,xe=!0,Me=!1,ke=!0,Ie=!1,Ue=!0,ze=!1,Pe=!1,He=!1,Fe=!1,Be=!1,We=!1,Ge=!0,Ye=!1,je=!0,Xe=!1,qe={},$e=null;const Ke=w({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ve=null;const Ze=w({},["audio","video","img","source","image","track"]);let Je=null;const Qe=w({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),et="http://www.w3.org/1998/Math/MathML",tt="http://www.w3.org/2000/svg",nt="http://www.w3.org/1999/xhtml";let ot=nt,rt=!1,it=null;const at=w({},[et,tt,nt],g);let lt=w({},["mi","mo","mn","ms","mtext"]),ct=w({},["annotation-xml"]);const st=w({},["title","style","font","a","script"]);let ut=null;const mt=["application/xhtml+xml","text/html"];let pt=null,ft=null;const dt=r.createElement("form"),ht=function(e){return e instanceof RegExp||e instanceof Function},gt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!ft||ft!==e){if(e&&"object"==typeof e||(e={}),e=D(e),ut=-1===mt.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,pt="application/xhtml+xml"===ut?g:h,Ne=_(e,"ALLOWED_TAGS")?w({},e.ALLOWED_TAGS,pt):Re,we=_(e,"ALLOWED_ATTR")?w({},e.ALLOWED_ATTR,pt):Oe,it=_(e,"ALLOWED_NAMESPACES")?w({},e.ALLOWED_NAMESPACES,g):at,Je=_(e,"ADD_URI_SAFE_ATTR")?w(D(Qe),e.ADD_URI_SAFE_ATTR,pt):Qe,Ve=_(e,"ADD_DATA_URI_TAGS")?w(D(Ze),e.ADD_DATA_URI_TAGS,pt):Ze,$e=_(e,"FORBID_CONTENTS")?w({},e.FORBID_CONTENTS,pt):Ke,ve=_(e,"FORBID_TAGS")?w({},e.FORBID_TAGS,pt):{},Le=_(e,"FORBID_ATTR")?w({},e.FORBID_ATTR,pt):{},qe=!!_(e,"USE_PROFILES")&&e.USE_PROFILES,Ce=!1!==e.ALLOW_ARIA_ATTR,xe=!1!==e.ALLOW_DATA_ATTR,Me=e.ALLOW_UNKNOWN_PROTOCOLS||!1,ke=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,Ie=e.SAFE_FOR_TEMPLATES||!1,Ue=!1!==e.SAFE_FOR_XML,ze=e.WHOLE_DOCUMENT||!1,Fe=e.RETURN_DOM||!1,Be=e.RETURN_DOM_FRAGMENT||!1,We=e.RETURN_TRUSTED_TYPE||!1,He=e.FORCE_BODY||!1,Ge=!1!==e.SANITIZE_DOM,Ye=e.SANITIZE_NAMED_PROPS||!1,je=!1!==e.KEEP_CONTENT,Xe=e.IN_PLACE||!1,be=e.ALLOWED_URI_REGEXP||X,ot=e.NAMESPACE||nt,lt=e.MATHML_TEXT_INTEGRATION_POINTS||lt,ct=e.HTML_INTEGRATION_POINTS||ct,De=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&ht(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(De.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&ht(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(De.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(De.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Ie&&(xe=!1),Be&&(Fe=!0),qe&&(Ne=w({},U),we=[],!0===qe.html&&(w(Ne,L),w(we,z)),!0===qe.svg&&(w(Ne,C),w(we,P),w(we,F)),!0===qe.svgFilters&&(w(Ne,x),w(we,P),w(we,F)),!0===qe.mathMl&&(w(Ne,k),w(we,H),w(we,F))),e.ADD_TAGS&&(Ne===Re&&(Ne=D(Ne)),w(Ne,e.ADD_TAGS,pt)),e.ADD_ATTR&&(we===Oe&&(we=D(we)),w(we,e.ADD_ATTR,pt)),e.ADD_URI_SAFE_ATTR&&w(Je,e.ADD_URI_SAFE_ATTR,pt),e.FORBID_CONTENTS&&($e===Ke&&($e=D($e)),w($e,e.FORBID_CONTENTS,pt)),je&&(Ne["#text"]=!0),ze&&w(Ne,["html","head","body"]),Ne.table&&(w(Ne,["tbody"]),delete ve.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw b('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw b('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');le=e.TRUSTED_TYPES_POLICY,ce=le.createHTML("")}else void 0===le&&(le=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(j,c)),null!==le&&"string"==typeof ce&&(ce=le.createHTML(""));i&&i(e),ft=e}},Tt=w({},[...C,...x,...M]),yt=w({},[...k,...I]),Et=function(e){f(o.removed,{element:e});try{ae(e).removeChild(e)}catch(t){V(e)}},At=function(e,t){try{f(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){f(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e)if(Fe||Be)try{Et(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},_t=function(e){let t=null,n=null;if(He)e=""+e;else{const t=T(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===ut&&ot===nt&&(e=''+e+"");const o=le?le.createHTML(e):e;if(ot===nt)try{t=(new Y).parseFromString(o,ut)}catch(e){}if(!t||!t.documentElement){t=se.createDocument(ot,"template",null);try{t.documentElement.innerHTML=rt?ce:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),ot===nt?pe.call(t,ze?"html":"body")[0]:ze?t.documentElement:i},St=function(e){return ue.call(e.ownerDocument||e,e,B.SHOW_ELEMENT|B.SHOW_COMMENT|B.SHOW_TEXT|B.SHOW_PROCESSING_INSTRUCTION|B.SHOW_CDATA_SECTION,null)},bt=function(e){return e instanceof G&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof W)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},Nt=function(e){return"function"==typeof R&&e instanceof R};function Rt(e,t,n){u(e,(e=>{e.call(o,t,n,ft)}))}const wt=function(e){let t=null;if(Rt(de.beforeSanitizeElements,e,null),bt(e))return Et(e),!0;const n=pt(e.nodeName);if(Rt(de.uponSanitizeElement,e,{tagName:n,allowedTags:Ne}),e.hasChildNodes()&&!Nt(e.firstElementChild)&&S(/<[/\w]/g,e.innerHTML)&&S(/<[/\w]/g,e.textContent))return Et(e),!0;if(e.nodeType===ee)return Et(e),!0;if(Ue&&e.nodeType===te&&S(/<[/\w]/g,e.data))return Et(e),!0;if(!Ne[n]||ve[n]){if(!ve[n]&&Dt(n)){if(De.tagNameCheck instanceof RegExp&&S(De.tagNameCheck,n))return!1;if(De.tagNameCheck instanceof Function&&De.tagNameCheck(n))return!1}if(je&&!$e[n]){const t=ae(e)||e.parentNode,n=ie(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o){const r=$(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,re(e))}}}return Et(e),!0}return e instanceof O&&!function(e){let t=ae(e);t&&t.tagName||(t={namespaceURI:ot,tagName:"template"});const n=h(e.tagName),o=h(t.tagName);return!!it[e.namespaceURI]&&(e.namespaceURI===tt?t.namespaceURI===nt?"svg"===n:t.namespaceURI===et?"svg"===n&&("annotation-xml"===o||lt[o]):Boolean(Tt[n]):e.namespaceURI===et?t.namespaceURI===nt?"math"===n:t.namespaceURI===tt?"math"===n&&ct[o]:Boolean(yt[n]):e.namespaceURI===nt?!(t.namespaceURI===tt&&!ct[o])&&!(t.namespaceURI===et&&!lt[o])&&!yt[n]&&(st[n]||!Tt[n]):!("application/xhtml+xml"!==ut||!it[e.namespaceURI]))}(e)?(Et(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!S(/<\/no(script|embed|frames)/i,e.innerHTML)?(Ie&&e.nodeType===Q&&(t=e.textContent,u([he,ge,Te],(e=>{t=y(t,e," ")})),e.textContent!==t&&(f(o.removed,{element:e.cloneNode()}),e.textContent=t)),Rt(de.afterSanitizeElements,e,null),!1):(Et(e),!0)},Ot=function(e,t,n){if(Ge&&("id"===t||"name"===t)&&(n in r||n in dt))return!1;if(xe&&!Le[t]&&S(ye,t));else if(Ce&&S(Ee,t));else if(!we[t]||Le[t]){if(!(Dt(e)&&(De.tagNameCheck instanceof RegExp&&S(De.tagNameCheck,e)||De.tagNameCheck instanceof Function&&De.tagNameCheck(e))&&(De.attributeNameCheck instanceof RegExp&&S(De.attributeNameCheck,t)||De.attributeNameCheck instanceof Function&&De.attributeNameCheck(t))||"is"===t&&De.allowCustomizedBuiltInElements&&(De.tagNameCheck instanceof RegExp&&S(De.tagNameCheck,n)||De.tagNameCheck instanceof Function&&De.tagNameCheck(n))))return!1}else if(Je[t]);else if(S(be,y(n,_e,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==E(n,"data:")||!Ve[e]){if(Me&&!S(Ae,y(n,_e,"")));else if(n)return!1}else;return!0},Dt=function(e){return"annotation-xml"!==e&&T(e,Se)},vt=function(e){Rt(de.beforeSanitizeAttributes,e,null);const{attributes:t}=e;if(!t||bt(e))return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:we,forceKeepAttr:void 0};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=pt(a);let m="value"===a?c:A(c);if(n.attrName=s,n.attrValue=m,n.keepAttr=!0,n.forceKeepAttr=void 0,Rt(de.uponSanitizeAttribute,e,n),m=n.attrValue,!Ye||"id"!==s&&"name"!==s||(At(a,e),m="user-content-"+m),Ue&&S(/((--!?|])>)|<\/(style|title)/i,m)){At(a,e);continue}if(n.forceKeepAttr)continue;if(At(a,e),!n.keepAttr)continue;if(!ke&&S(/\/>/i,m)){At(a,e);continue}Ie&&u([he,ge,Te],(e=>{m=y(m,e," ")}));const f=pt(e.nodeName);if(Ot(f,s,m)){if(le&&"object"==typeof j&&"function"==typeof j.getAttributeType)if(l);else switch(j.getAttributeType(f,s)){case"TrustedHTML":m=le.createHTML(m);break;case"TrustedScriptURL":m=le.createScriptURL(m)}try{l?e.setAttributeNS(l,a,m):e.setAttribute(a,m),bt(e)?Et(e):p(o.removed)}catch(e){}}}Rt(de.afterSanitizeAttributes,e,null)},Lt=function e(t){let n=null;const o=St(t);for(Rt(de.beforeSanitizeShadowDOM,t,null);n=o.nextNode();)Rt(de.uponSanitizeShadowNode,n,null),wt(n),vt(n),n.content instanceof s&&e(n.content);Rt(de.afterSanitizeShadowDOM,t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(rt=!e,rt&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Nt(e)){if("function"!=typeof e.toString)throw b("toString is not a function");if("string"!=typeof(e=e.toString()))throw b("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Pe||gt(t),o.removed=[],"string"==typeof e&&(Xe=!1),Xe){if(e.nodeName){const t=pt(e.nodeName);if(!Ne[t]||ve[t])throw b("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof R)n=_t("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===J&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!Fe&&!Ie&&!ze&&-1===e.indexOf("<"))return le&&We?le.createHTML(e):e;if(n=_t(e),!n)return Fe?null:We?ce:""}n&&He&&Et(n.firstChild);const c=St(Xe?e:n);for(;i=c.nextNode();)wt(i),vt(i),i.content instanceof s&&Lt(i.content);if(Xe)return e;if(Fe){if(Be)for(l=me.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(we.shadowroot||we.shadowrootmode)&&(l=fe.call(a,l,!0)),l}let m=ze?n.outerHTML:n.innerHTML;return ze&&Ne["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&S(K,n.ownerDocument.doctype.name)&&(m="\n"+m),Ie&&u([he,ge,Te],(e=>{m=y(m,e," ")})),le&&We?le.createHTML(m):m},o.setConfig=function(){gt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Pe=!0},o.clearConfig=function(){ft=null,Pe=!1},o.isValidAttribute=function(e,t,n){ft||gt({});const o=pt(e),r=pt(t);return Ot(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&f(de[e],t)},o.removeHook=function(e,t){if(void 0!==t){const n=m(de[e],t);return-1===n?void 0:d(de[e],n,1)[0]}return p(de[e])},o.removeHooks=function(e){de[e]=[]},o.removeAllHooks=function(){de={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},o}();return re})); -//# sourceMappingURL=purify.min.js.map