From 196b543beeb93784e4a4be9a7af53d29681e3240 Mon Sep 17 00:00:00 2001 From: Hein Date: Mon, 20 Jul 2026 22:50:55 +0200 Subject: [PATCH] feat(ui): add identity management for tenants and users * Implement tenant and user creation in IdentityPage * Add API calls for managing tenants and users * Introduce tenant-scoped API requests * Update sidebar to include identity navigation * Create BooleanStatusBadge component for key status --- configs/config.example.yaml | 2 + configs/dev.yaml | 2 + configs/docker.yaml | 2 + doc/llm/log/20260720_19.md | 19 + doc/llm/log/20260720_21.md | 3 + doc/llm/log/20260720_22.md | 3 + docs/per-user-tenancy-plan.md | 40 +- go.mod | 2 +- go.sum | 4 +- internal/app/admin_identity.go | 364 +++++ internal/app/app.go | 11 + internal/app/resolvespec_admin.go | 8 +- internal/app/resolvespec_models_generated.go | 4 + internal/app/ui_assets.go | 4 +- internal/auth/keyring.go | 115 +- internal/auth/keyring_test.go | 20 + internal/auth/middleware.go | 6 +- internal/config/config.go | 2 + .../sql_public_agent_guardrails.go | 2 + .../generatedmodels/sql_public_agent_parts.go | 2 + .../sql_public_agent_personas.go | 2 + .../sql_public_agent_skills.go | 5 +- .../sql_public_agent_traits.go | 2 + .../sql_public_api_key_assignments.go | 66 + .../sql_public_character_arcs.go | 2 + .../sql_public_chat_histories.go | 3 +- .../generatedmodels/sql_public_learnings.go | 3 +- .../sql_public_managed_api_keys.go | 59 + .../generatedmodels/sql_public_persona_arc.go | 2 +- internal/generatedmodels/sql_public_plans.go | 3 +- .../generatedmodels/sql_public_projects.go | 5 +- .../sql_public_stored_files.go | 3 +- .../sql_public_tenant_users.go | 63 + .../generatedmodels/sql_public_tenants.go | 73 + .../generatedmodels/sql_public_thoughts.go | 3 +- internal/store/files.go | 6 +- internal/store/plans.go | 1 - internal/store/projects.go | 10 +- internal/store/skills.go | 40 +- internal/store/thoughts.go | 28 +- migrations/020_generated_schema.sql | 1455 +++++++++++++++-- migrations/120_identity_tenancy.sql | 31 + migrations/130_tenant_foreign_keys.sql | 53 + .../140_rename_tenant_key_to_tenant_id.sql | 44 + schema/agent_personas.dbml | 22 +- schema/core.dbml | 12 +- schema/files.dbml | 4 +- schema/identity.dbml | 33 + schema/meta.dbml | 8 +- schema/plans.dbml | 4 +- schema/skills.dbml | 10 +- ui/package.json | 20 +- ui/pnpm-lock.yaml | 825 +++++----- ui/src/api.ts | 87 +- .../components/identity/IdentityPage.svelte | 196 +++ .../shared/BooleanStatusBadge.svelte | 14 + ui/src/components/shell/AdminShell.svelte | 9 + ui/src/components/shell/AppSidebar.svelte | 47 + .../components/thoughts/ThoughtsPage.svelte | 1 - ui/src/main.ts | 3 + ui/src/tenantScope.ts | 66 + ui/src/types.ts | 32 +- .../ResolveSpec/pkg/resolvespec/handler.go | 6 +- vendor/modules.txt | 2 +- 64 files changed, 3363 insertions(+), 615 deletions(-) create mode 100644 doc/llm/log/20260720_19.md create mode 100644 doc/llm/log/20260720_21.md create mode 100644 doc/llm/log/20260720_22.md create mode 100644 internal/app/admin_identity.go create mode 100644 internal/generatedmodels/sql_public_api_key_assignments.go create mode 100644 internal/generatedmodels/sql_public_managed_api_keys.go create mode 100644 internal/generatedmodels/sql_public_tenant_users.go create mode 100644 internal/generatedmodels/sql_public_tenants.go create mode 100644 migrations/120_identity_tenancy.sql create mode 100644 migrations/130_tenant_foreign_keys.sql create mode 100644 migrations/140_rename_tenant_key_to_tenant_id.sql create mode 100644 schema/identity.dbml create mode 100644 ui/src/components/identity/IdentityPage.svelte create mode 100644 ui/src/components/shared/BooleanStatusBadge.svelte create mode 100644 ui/src/tenantScope.ts diff --git a/configs/config.example.yaml b/configs/config.example.yaml index d13fb5d..a0f101d 100644 --- a/configs/config.example.yaml +++ b/configs/config.example.yaml @@ -23,6 +23,8 @@ auth: keys: - id: "local-client" value: "replace-me" + tenant_id: "local" + superadmin: true description: "main local client key" oauth: clients: diff --git a/configs/dev.yaml b/configs/dev.yaml index 1413ce7..9eb2b61 100644 --- a/configs/dev.yaml +++ b/configs/dev.yaml @@ -21,6 +21,8 @@ auth: keys: - id: "local-client" value: "replace-me" + tenant_id: "local" + superadmin: true description: "main local client key" oauth: clients: diff --git a/configs/docker.yaml b/configs/docker.yaml index bdacc04..9fcf966 100644 --- a/configs/docker.yaml +++ b/configs/docker.yaml @@ -21,6 +21,8 @@ auth: keys: - id: "local-client" value: "replace-me" + tenant_id: "local" + superadmin: true description: "main local client key" oauth: clients: diff --git a/doc/llm/log/20260720_19.md b/doc/llm/log/20260720_19.md new file mode 100644 index 0000000..b9288d5 --- /dev/null +++ b/doc/llm/log/20260720_19.md @@ -0,0 +1,19 @@ +# Tenant identity management + +The previous tenancy implementation used every API-key ID as its tenant key. This meant that two keys could not share data and old unscoped rows could not be reached by any key after tenancy was enabled. + +Implemented a separate tenant mapping: configured and managed API keys now resolve to an assigned tenant ID, while unassigned configured keys retain their historical key-ID tenant boundary for compatibility. Added tenant, tenant-user, key-assignment, and managed-secret records, with a one-time, explicit legacy adoption endpoint that assigns only `NULL` tenant-key rows to a selected tenant. + +The admin UI now has an Identity page for tenants, users, configured-key assignment, managed-key creation (secret shown once), disabling keys, and legacy adoption. Managed secrets are stored only as SHA-256 hashes. The AMCS MCP capture-thought tool was unavailable in this session, so this local log is the required fallback summary. + +Configured keys also accept an optional `auth.keys[].tenant_id`. This supplies the initial tenant boundary at startup; an explicit assignment saved through the Identity UI overrides it. + +Tenant-owned root records now reference `tenants(id)` through `tenant_id`; tenant scoped resources include skills, guardrails, personas, parts, traits, and character arcs. Tenant-selection in the UI is sent as `X-AMCS-Tenant-ID` for admin/ResolveSpec requests. + +With explicit approval that tenancy data is disposable, the compatibility migration now renames the ownership column from `tenant_key` to `tenant_id` across the historical migration chain. + +Tenant selection now centrally filters every tenant-owned ResolveSpec read, update, and delete in the admin UI, and injects the selected `tenant_id` on create requests. + +Identity mutations now require a configured API key with `superadmin: true`; tenants themselves remain database-managed. + +Fixed the admin sidebar tenant-scope selector flicker. Its focus handler reloaded the tenant list and disabled the native select while its picker was opening; tenants now load on sidebar mount only, so a selection remains open and usable. Validated with `pnpm check` (0 errors, 0 warnings). diff --git a/doc/llm/log/20260720_21.md b/doc/llm/log/20260720_21.md new file mode 100644 index 0000000..8460bce --- /dev/null +++ b/doc/llm/log/20260720_21.md @@ -0,0 +1,3 @@ +# ResolveSpec array scanning + +Diagnosed `/api/rs/public/agent_skills` failing to scan `tags text[]`. The generated Bun model correctly uses `sqltypes.SqlStringArray`, which implements `sql.Scanner`, but ResolveSpec's read path supplied a separate scan destination instead of scanning the query's registered Bun model. Updated the vendored ResolveSpec handler to call `ScanModel`, and for single-row reads to set the single record as the query model first. This preserves Bun's model field scanner for PostgreSQL arrays. diff --git a/doc/llm/log/20260720_22.md b/doc/llm/log/20260720_22.md new file mode 100644 index 0000000..10402fa --- /dev/null +++ b/doc/llm/log/20260720_22.md @@ -0,0 +1,3 @@ +# Per-user tenancy progress review + +Reviewed `docs/per-user-tenancy-plan.md` against the staged worktree. Identity, tenant mappings, schema/model migrations, and primary project/thought/file/catalog scoping are present. The plan's named hardening targets—learnings, plans, chat histories, thought-learning links, and project persona joins—still have no tenant helper predicates. Only auth/keyring tests are staged; the plan's cross-tenant store/tool and migration test matrix has not yet been added or run. diff --git a/docs/per-user-tenancy-plan.md b/docs/per-user-tenancy-plan.md index 227340f..ce9decf 100644 --- a/docs/per-user-tenancy-plan.md +++ b/docs/per-user-tenancy-plan.md @@ -7,9 +7,9 @@ Gitea issue #10 asks AMCS to isolate memories by user, tenant, or workspace. The Observed code paths: - Authentication enters through `internal/auth/middleware.go`. API-key auth uses the configured header, defaulting to `x-brain-key`; bearer tokens can resolve through OAuth `TokenStore` or API keyring; HTTP Basic resolves OAuth client credentials. -- `internal/auth/middleware.go` writes both `auth.key_id` and `tenancy.tenant_key` into the request context. The tenant key is intentionally opaque and currently equals the authenticated key id or OAuth client id. +- `internal/auth/middleware.go` writes both `auth.key_id` and `tenancy.tenant_id` into the request context. The tenant key is intentionally opaque and currently equals the authenticated key id or OAuth client id. - `internal/tenancy/tenancy.go` exposes `WithTenantKey` and `KeyFromContext` only; callers should not infer user semantics from the tenant string. -- Schema already has `tenant_key` on `projects`, `thoughts`, `stored_files`, `learnings`, `plans`, and `chat_histories` in `schema/*.dbml`. +- Schema already has `tenant_id` on `projects`, `thoughts`, `stored_files`, `learnings`, `plans`, and `chat_histories` in `schema/*.dbml`. - `internal/store/tenancy.go` provides helper functions for appending tenant predicates to SQL. - Tenant scoping is already present in project, thought, and stored-file store paths. Some other project-owned domains still need explicit tenant enforcement. @@ -17,9 +17,9 @@ Observed code paths: Use the authenticated principal id as the tenant boundary: -1. API key: resolve token through `auth.Keyring.Lookup`; use returned key id as `tenant_key`. -2. OAuth bearer token: resolve token through `TokenStore.Lookup`; use returned client id/key id as `tenant_key`. -3. OAuth Basic client credentials: resolve through `OAuthRegistry.Lookup`; use returned client id as `tenant_key`. +1. API key: resolve token through `auth.Keyring.Lookup`; use returned key id as `tenant_id`. +2. OAuth bearer token: resolve token through `TokenStore.Lookup`; use returned client id/key id as `tenant_id`. +3. OAuth Basic client credentials: resolve through `OAuthRegistry.Lookup`; use returned client id as `tenant_id`. 4. Unauthenticated requests must not get tenant context and must not reach protected MCP/API handlers. Do not store raw API keys or bearer tokens in tenant columns. Store only stable configured key ids/client ids. Yes, it is less flashy than inventing an account service before breakfast, but it keeps the trust boundary small and auditable. @@ -28,7 +28,7 @@ Do not store raw API keys or bearer tokens in tenant columns. Store only stable Source-of-truth DBML changes: -- Add nullable `tenant_key text` to all user-owned tables: +- Add nullable `tenant_id text` to all user-owned tables: - `projects` - `thoughts` - `stored_files` @@ -36,9 +36,9 @@ Source-of-truth DBML changes: - `plans` - `chat_histories` - Add tenant indexes: - - single-column `tenant_key` for direct filtering - - `(tenant_key, name)` unique on `projects`, replacing global `projects.name` uniqueness - - `(tenant_key, project_id)` on `thoughts` for common project-scoped memory lookups + - single-column `tenant_id` for direct filtering + - `(tenant_id, name)` unique on `projects`, replacing global `projects.name` uniqueness + - `(tenant_id, project_id)` on `thoughts` for common project-scoped memory lookups - Regenerate SQL migrations and generated models from DBML; do not hand-edit generated Go models except as a temporary debugging step. Important follow-up: `agent_skills`, `agent_guardrails`, `agent_personas`, `agent_parts`, traits, and arcs are currently global catalogs. Keep them global unless product requirements say skills/personas are private per tenant. Project join tables inherit protection through tenant-scoped project ids, but direct join queries must verify the project belongs to the tenant. @@ -49,9 +49,9 @@ All request-facing store methods that touch tenant-owned rows must include tenan Rules: -- Inserts must populate `tenant_key` from context. -- Gets/updates/deletes by id or guid must add `and tenant_key = $n`. -- Lists/searches must add `tenant_key = $n` before other filters. +- Inserts must populate `tenant_id` from context. +- Gets/updates/deletes by id or guid must add `and tenant_id = $n`. +- Lists/searches must add `tenant_id = $n` before other filters. - Joins must scope the tenant-owned root table. Example: project summaries should count only thoughts that belong to the same tenant as the project, not merely thoughts with the same `project_id`. - Background maintenance jobs must either run per tenant, carry tenant context explicitly, or intentionally operate cross-tenant with an internal-only code path documented in the job. @@ -74,24 +74,24 @@ Paths needing audit/hardening: Migration approach for existing single-tenant installs: -1. Add nullable `tenant_key` columns first; do not make them `not null` initially because existing deployments have historical rows without a principal. -2. Backfill existing rows to a configured default tenant only if the instance enables multi-tenant mode or defines `auth.default_tenant_key`. Otherwise leave `NULL` rows visible only to unauthenticated/internal single-tenant contexts. -3. Replace global project-name uniqueness with `(tenant_key, name)` uniqueness. For PostgreSQL, preserve legacy `NULL` semantics carefully: multiple null-tenant projects with the same name may be possible unless a partial unique index is added for null tenant rows. +1. Add nullable `tenant_id` columns first; do not make them `not null` initially because existing deployments have historical rows without a principal. +2. Backfill existing rows to a configured default tenant only if the instance enables multi-tenant mode or defines `auth.default_tenant_id`. Otherwise leave `NULL` rows visible only to unauthenticated/internal single-tenant contexts. +3. Replace global project-name uniqueness with `(tenant_id, name)` uniqueness. For PostgreSQL, preserve legacy `NULL` semantics carefully: multiple null-tenant projects with the same name may be possible unless a partial unique index is added for null tenant rows. 4. Add indexes concurrently where practical for production-sized tables. 5. Document that after enabling auth-backed tenancy, legacy null-tenant data is not visible to authenticated tenants unless backfilled. Recommended config addition: -- `auth.default_tenant_key` or `tenancy.default_key` for one-time/self-hosted backfill and development. +- `auth.default_tenant_id` or `tenancy.default_key` for one-time/self-hosted backfill and development. - Optional `tenancy.mode: single|authenticated` so operators can keep current single-user behavior deliberately instead of discovering isolation by accident. An accident in auth is just a breach with better branding. ## Authorization checks Authorization is row ownership by tenant key: -- A request may only see or mutate rows whose `tenant_key` equals the authenticated tenant key. +- A request may only see or mutate rows whose `tenant_id` equals the authenticated tenant key. - Cross-tenant ids/gids should behave as not found, not forbidden, to avoid existence leaks. -- Tenant key is server-derived only. Ignore any client-provided `tenant_key` fields on tool/API inputs. +- Tenant key is server-derived only. Ignore any client-provided `tenant_id` fields on tool/API inputs. - Project id references in create/update operations must be validated against the same tenant before use. This prevents attaching a new thought/file/plan to another tenant's project id. - Relationship operations must verify both endpoints are in the same tenant before creating links. - Global catalogs can be read across tenants only if intentionally shared; project-specific associations must be tenant-checked through the project. @@ -117,7 +117,7 @@ HTTP/API boundaries: UI: - Project selector/list should naturally show tenant-scoped projects. -- Admin tables for projects/thoughts/files/learnings/plans/chat histories must not show `tenant_key` as an editable field. +- Admin tables for projects/thoughts/files/learnings/plans/chat histories must not show `tenant_id` as an editable field. - If a tenant switcher is ever added, it must map to a server-side authenticated principal or admin impersonation path, not a client-side query parameter. Obviously. ## Test cases @@ -143,7 +143,7 @@ Minimum test matrix: Assumptions: - The first tenancy boundary is authenticated principal id, not human account, org, or workspace. This is consistent with the current auth system and avoids adding an account model prematurely. -- Null `tenant_key` remains the compatibility path for existing single-tenant/internal flows. +- Null `tenant_id` remains the compatibility path for existing single-tenant/internal flows. - Global skills/personas/guardrails remain shared catalogs until a separate product decision makes them tenant-private. Blockers/decisions needed: diff --git a/go.mod b/go.mod index 075fb60..7a36bbf 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.1 require ( git.warky.dev/wdevs/relspecgo v1.0.62 - github.com/bitechdev/ResolveSpec v1.1.26 + github.com/bitechdev/ResolveSpec v1.1.27 github.com/google/jsonschema-go v0.4.3 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.9.2 diff --git a/go.sum b/go.sum index 70e432c..da006dc 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bitechdev/ResolveSpec v1.1.26 h1:/OYc1Mjcfm4Qxq8Xy5UY/32l5cSqkNe8ieeOqIEbK5o= -github.com/bitechdev/ResolveSpec v1.1.26/go.mod h1:GF51sMRCWbAyri2WNae3IZAFM/2s6DG6i3eTTrobbVs= +github.com/bitechdev/ResolveSpec v1.1.27 h1:qugBwR3Qoy4tNKhAyJsFszZE+kRR0gzl5w42XE3/scY= +github.com/bitechdev/ResolveSpec v1.1.27/go.mod h1:GF51sMRCWbAyri2WNae3IZAFM/2s6DG6i3eTTrobbVs= github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c h1:6Gpm9YYUEQx2T9zMsYolQhr6sjwwGtFitSA0pQsa7a8= github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= diff --git a/internal/app/admin_identity.go b/internal/app/admin_identity.go new file mode 100644 index 0000000..0d6beb9 --- /dev/null +++ b/internal/app/admin_identity.go @@ -0,0 +1,364 @@ +package app + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "strings" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + + "git.warky.dev/wdevs/amcs/internal/auth" +) + +type identityAdmin struct { + pool *pgxpool.Pool + keyring *auth.Keyring + logger *slog.Logger +} + +func newIdentityAdmin(pool *pgxpool.Pool, keyring *auth.Keyring, logger *slog.Logger) *identityAdmin { + return &identityAdmin{pool: pool, keyring: keyring, logger: logger} +} + +func loadIdentityKeyring(ctx context.Context, pool *pgxpool.Pool, keyring *auth.Keyring) error { + if keyring == nil { + return nil + } + rows, err := pool.Query(ctx, `select a.key_id, a.tenant_id, a.enabled, coalesce(m.secret_hash, '') from api_key_assignments a left join managed_api_keys m on m.key_id = a.key_id`) + if err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var keyID, tenantID, hash string + var enabled bool + if err := rows.Scan(&keyID, &tenantID, &enabled, &hash); err != nil { + return err + } + keyring.AssignTenant(keyID, tenantID) + if hash != "" { + keyring.AddManaged(keyID, hash, enabled) + } + } + return rows.Err() +} + +// ensureConfiguredTenants makes a tenant referenced in static YAML visible to +// the admin UI as well as to the authentication middleware. +func ensureConfiguredTenants(ctx context.Context, pool *pgxpool.Pool, keyring *auth.Keyring) error { + if keyring == nil { + return nil + } + for _, key := range keyring.ConfiguredKeys() { + tenantID := strings.TrimSpace(key.TenantID) + if tenantID == "" { + continue + } + if _, err := pool.Exec(ctx, `insert into tenants (id, name) values ($1, $1) on conflict (id) do nothing`, tenantID); err != nil { + return err + } + } + return nil +} + +func (a *identityAdmin) handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + keyID, ok := auth.KeyIDFromContext(r.Context()) + if !ok || a.keyring == nil || !a.keyring.IsSuperadmin(keyID) { + writeJSON(w, http.StatusForbidden, map[string]string{"error": "superadmin API key required"}) + return + } + } + path := strings.TrimPrefix(r.URL.Path, "/api/admin/identity") + switch { + case r.Method == http.MethodGet && path == "": + a.list(w, r) + case r.Method == http.MethodPost && path == "/tenants": + a.createTenant(w, r) + case r.Method == http.MethodPost && path == "/users": + a.createUser(w, r) + case r.Method == http.MethodPost && path == "/keys": + a.createKey(w, r) + case r.Method == http.MethodPatch && strings.HasPrefix(path, "/keys/"): + a.updateKey(w, r, strings.TrimPrefix(path, "/keys/")) + case r.Method == http.MethodPost && strings.HasPrefix(path, "/tenants/") && strings.HasSuffix(path, "/adopt-legacy"): + a.adoptLegacy(w, r, strings.TrimSuffix(strings.TrimPrefix(path, "/tenants/"), "/adopt-legacy")) + default: + http.NotFound(w, r) + } + }) +} + +type tenantDTO struct { + ID string `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` +} +type userDTO struct { + ID string `json:"id"` + TenantID string `json:"tenant_id"` + Name string `json:"name"` + Email *string `json:"email,omitempty"` + CreatedAt time.Time `json:"created_at"` +} +type keyDTO struct { + ID string `json:"id"` + TenantID string `json:"tenant_id"` + UserID *string `json:"user_id,omitempty"` + Description string `json:"description"` + Source string `json:"source"` + Enabled bool `json:"enabled"` + CreatedAt time.Time `json:"created_at"` +} + +func (a *identityAdmin) list(w http.ResponseWriter, r *http.Request) { + result := struct { + Tenants []tenantDTO `json:"tenants"` + Users []userDTO `json:"users"` + Keys []keyDTO `json:"keys"` + }{Tenants: []tenantDTO{}, Users: []userDTO{}, Keys: []keyDTO{}} + rows, err := a.pool.Query(r.Context(), `select id, name, created_at from tenants order by name`) + if err != nil { + identityError(w, err) + return + } + defer rows.Close() + for rows.Next() { + var x tenantDTO + if err := rows.Scan(&x.ID, &x.Name, &x.CreatedAt); err != nil { + identityError(w, err) + return + } + result.Tenants = append(result.Tenants, x) + } + rows, err = a.pool.Query(r.Context(), `select id, tenant_id, name, email, created_at from tenant_users order by name`) + if err != nil { + identityError(w, err) + return + } + defer rows.Close() + for rows.Next() { + var x userDTO + if err := rows.Scan(&x.ID, &x.TenantID, &x.Name, &x.Email, &x.CreatedAt); err != nil { + identityError(w, err) + return + } + result.Users = append(result.Users, x) + } + configured := make(map[string]authKey) + if a.keyring != nil { + for _, key := range a.keyring.ConfiguredKeys() { + configured[key.ID] = authKey{description: key.Description} + } + } + rows, err = a.pool.Query(r.Context(), `select key_id, tenant_id, user_id, description, source, enabled, created_at from api_key_assignments order by key_id`) + if err != nil { + identityError(w, err) + return + } + defer rows.Close() + for rows.Next() { + var x keyDTO + if err := rows.Scan(&x.ID, &x.TenantID, &x.UserID, &x.Description, &x.Source, &x.Enabled, &x.CreatedAt); err != nil { + identityError(w, err) + return + } + result.Keys = append(result.Keys, x) + delete(configured, x.ID) + } + for id, key := range configured { + result.Keys = append(result.Keys, keyDTO{ID: id, Description: key.description, Source: "configured", Enabled: true}) + } + writeJSON(w, http.StatusOK, result) +} + +type authKey struct{ description string } + +func (a *identityAdmin) createTenant(w http.ResponseWriter, r *http.Request) { + var body struct { + Name string `json:"name"` + } + if !decodeJSON(w, r, &body) { + return + } + body.Name = strings.TrimSpace(body.Name) + if body.Name == "" { + badRequest(w, "name is required") + return + } + x := tenantDTO{ID: newIdentityID(), Name: body.Name} + err := a.pool.QueryRow(r.Context(), `insert into tenants (id,name) values ($1,$2) returning created_at`, x.ID, x.Name).Scan(&x.CreatedAt) + if err != nil { + identityError(w, err) + return + } + writeJSON(w, http.StatusCreated, x) +} +func (a *identityAdmin) createUser(w http.ResponseWriter, r *http.Request) { + var body struct { + TenantID, Name string + Email *string `json:"email"` + } + if !decodeJSON(w, r, &body) { + return + } + body.TenantID = strings.TrimSpace(body.TenantID) + body.Name = strings.TrimSpace(body.Name) + if body.TenantID == "" || body.Name == "" { + badRequest(w, "tenant_id and name are required") + return + } + x := userDTO{ID: newIdentityID(), TenantID: body.TenantID, Name: body.Name, Email: body.Email} + err := a.pool.QueryRow(r.Context(), `insert into tenant_users (id,tenant_id,name,email) values ($1,$2,$3,$4) returning created_at`, x.ID, x.TenantID, x.Name, x.Email).Scan(&x.CreatedAt) + if err != nil { + identityError(w, err) + return + } + writeJSON(w, http.StatusCreated, x) +} +func (a *identityAdmin) createKey(w http.ResponseWriter, r *http.Request) { + var body struct { + TenantID string `json:"tenant_id"` + UserID *string `json:"user_id"` + Description string `json:"description"` + } + if !decodeJSON(w, r, &body) { + return + } + body.TenantID = strings.TrimSpace(body.TenantID) + if body.TenantID == "" { + badRequest(w, "tenant_id is required") + return + } + secret, hash, err := auth.GenerateSecret() + if err != nil { + identityError(w, err) + return + } + x := keyDTO{ID: newIdentityID(), TenantID: body.TenantID, UserID: body.UserID, Description: strings.TrimSpace(body.Description), Source: "managed", Enabled: true} + tx, err := a.pool.Begin(r.Context()) + if err != nil { + identityError(w, err) + return + } + defer tx.Rollback(r.Context()) + if err = tx.QueryRow(r.Context(), `insert into api_key_assignments (key_id,tenant_id,user_id,description,source,enabled) values ($1,$2,$3,$4,'managed',true) returning created_at`, x.ID, x.TenantID, x.UserID, x.Description).Scan(&x.CreatedAt); err == nil { + _, err = tx.Exec(r.Context(), `insert into managed_api_keys (key_id,secret_hash) values ($1,$2)`, x.ID, hash) + } + if err == nil { + err = tx.Commit(r.Context()) + } + if err != nil { + identityError(w, err) + return + } + a.keyring.AddManaged(x.ID, hash, true) + a.keyring.AssignTenant(x.ID, x.TenantID) + writeJSON(w, http.StatusCreated, struct { + Key keyDTO `json:"key"` + Secret string `json:"secret"` + }{x, secret}) +} +func (a *identityAdmin) updateKey(w http.ResponseWriter, r *http.Request, keyID string) { + var body struct { + TenantID string `json:"tenant_id"` + UserID *string `json:"user_id"` + Description *string `json:"description"` + Enabled *bool `json:"enabled"` + } + if !decodeJSON(w, r, &body) { + return + } + body.TenantID = strings.TrimSpace(body.TenantID) + if body.TenantID == "" { + badRequest(w, "tenant_id is required") + return + } + if !a.keyring.IsConfigured(keyID) { + var exists bool + if err := a.pool.QueryRow(r.Context(), `select exists(select 1 from managed_api_keys where key_id=$1)`, keyID).Scan(&exists); err != nil { + identityError(w, err) + return + } + if !exists { + badRequest(w, "unknown key id") + return + } + } + var x keyDTO + err := a.pool.QueryRow(r.Context(), `insert into api_key_assignments (key_id,tenant_id,user_id,description,source,enabled) values ($1,$2,$3,coalesce($4,''),case when $6 then 'configured' else 'managed' end,coalesce($5,true)) on conflict (key_id) do update set tenant_id=excluded.tenant_id,user_id=excluded.user_id,description=coalesce($4,api_key_assignments.description),enabled=coalesce($5,api_key_assignments.enabled) returning key_id,tenant_id,user_id,description,source,enabled,created_at`, keyID, body.TenantID, body.UserID, body.Description, body.Enabled, a.keyring.IsConfigured(keyID)).Scan(&x.ID, &x.TenantID, &x.UserID, &x.Description, &x.Source, &x.Enabled, &x.CreatedAt) + if err != nil { + identityError(w, err) + return + } + a.keyring.AssignTenant(x.ID, x.TenantID) + a.keyring.SetManagedEnabled(x.ID, x.Enabled) + writeJSON(w, http.StatusOK, x) +} +func (a *identityAdmin) adoptLegacy(w http.ResponseWriter, r *http.Request, tenantID string) { + tenantID = strings.TrimSpace(tenantID) + if tenantID == "" { + badRequest(w, "tenant id is required") + return + } + tables := []string{"projects", "thoughts", "stored_files", "learnings", "plans", "chat_histories"} + tx, err := a.pool.Begin(r.Context()) + if err != nil { + identityError(w, err) + return + } + defer tx.Rollback(r.Context()) + var exists bool + if err = tx.QueryRow(r.Context(), `select exists(select 1 from tenants where id=$1)`, tenantID).Scan(&exists); err == nil && !exists { + badRequest(w, "tenant does not exist") + return + } + for _, table := range tables { + if _, err = tx.Exec(r.Context(), fmt.Sprintf("update %s set tenant_id=$1 where tenant_id is null", table), tenantID); err != nil { + identityError(w, err) + return + } + } + if err = tx.Commit(r.Context()); err != nil { + identityError(w, err) + return + } + w.WriteHeader(http.StatusNoContent) +} +func newIdentityID() string { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + panic(err) + } + return hex.EncodeToString(b) +} +func decodeJSON(w http.ResponseWriter, r *http.Request, v any) bool { + defer r.Body.Close() + if err := json.NewDecoder(r.Body).Decode(v); err != nil { + badRequest(w, "invalid JSON") + return false + } + return true +} +func writeJSON(w http.ResponseWriter, status int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(v) +} +func badRequest(w http.ResponseWriter, message string) { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": message}) +} +func identityError(w http.ResponseWriter, err error) { + if a, ok := err.(interface{ SQLState() string }); ok && a.SQLState() == "23505" { + badRequest(w, "that value already exists") + return + } + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "identity operation failed"}) +} diff --git a/internal/app/app.go b/internal/app/app.go index ed0b7b8..96c6183 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -91,6 +91,14 @@ func Run(ctx context.Context, configPath string) error { if err != nil { return err } + } else { + keyring = auth.NewManagedKeyring() + } + if err := ensureConfiguredTenants(ctx, db.Pool(), keyring); err != nil { + return fmt.Errorf("create configured tenants: %w", err) + } + if err := loadIdentityKeyring(ctx, db.Pool(), keyring); err != nil { + return fmt.Errorf("load identity key assignments: %w", err) } tokenStore = auth.NewTokenStore(0) if len(cfg.Auth.OAuth.Clients) > 0 { @@ -192,6 +200,7 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st enrichmentRetryer := tools.NewEnrichmentRetryer(context.Background(), db, bgMetadata, cfg.Capture, cfg.AI.Metadata.Timeout, activeProjects, logger) backfillTool := tools.NewBackfillTool(db, bgEmbeddings, activeProjects, logger) adminActions := newAdminActions(backfillTool, enrichmentRetryer, logger) + identityAdmin := newIdentityAdmin(db.Pool(), keyring, logger) toolSet := mcpserver.ToolSet{ Capture: tools.NewCaptureTool(db, embeddings, cfg.Capture, activeProjects, enrichmentRetryer, backfillTool), @@ -246,6 +255,8 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st mux.HandleFunc("/api/oauth/token", oauthTokenHandler(oauthRegistry, tokenStore, authCodes, logger)) mux.Handle("/api/admin/actions/backfill", authMiddleware(adminActions.backfillHandler())) mux.Handle("/api/admin/actions/retry-metadata", authMiddleware(adminActions.retryMetadataHandler())) + mux.Handle("/api/admin/identity", authMiddleware(identityAdmin.handler())) + mux.Handle("/api/admin/identity/", authMiddleware(identityAdmin.handler())) mux.HandleFunc("/favicon.ico", serveFavicon) mux.HandleFunc("/images/project.jpg", serveHomeImage) mux.HandleFunc("/images/icon.png", serveIcon) diff --git a/internal/app/resolvespec_admin.go b/internal/app/resolvespec_admin.go index d0756b2..0d05330 100644 --- a/internal/app/resolvespec_admin.go +++ b/internal/app/resolvespec_admin.go @@ -10,6 +10,7 @@ import ( "github.com/uptrace/bunrouter" "git.warky.dev/wdevs/amcs/internal/store" + "git.warky.dev/wdevs/amcs/internal/tenancy" ) func registerResolveSpecAdminRoutes(mux *http.ServeMux, db *store.DB, middleware func(http.Handler) http.Handler, logger *slog.Logger) error { @@ -45,7 +46,12 @@ func registerResolveSpecAdminRoutes(mux *http.ServeMux, db *store.DB, middleware rsMount.ServeHTTP(w, r) return } - middleware(rsMount).ServeHTTP(w, r) + middleware(http.HandlerFunc(func(w http.ResponseWriter, authenticated *http.Request) { + if tenantID := strings.TrimSpace(authenticated.Header.Get("X-AMCS-Tenant-ID")); tenantID != "" { + authenticated = authenticated.WithContext(tenancy.WithTenantKey(authenticated.Context(), tenantID)) + } + rsMount.ServeHTTP(w, authenticated) + })).ServeHTTP(w, r) }) mux.Handle("/api/rs/", protectedRSMount) diff --git a/internal/app/resolvespec_models_generated.go b/internal/app/resolvespec_models_generated.go index 2c9b4ba..ff52132 100644 --- a/internal/app/resolvespec_models_generated.go +++ b/internal/app/resolvespec_models_generated.go @@ -14,12 +14,14 @@ func resolveSpecModels() []resolveSpecModel { {schema: "public", entity: "agent_personas", model: generatedmodels.ModelPublicAgentPersonas{}}, {schema: "public", entity: "agent_skills", model: generatedmodels.ModelPublicAgentSkills{}}, {schema: "public", entity: "agent_traits", model: generatedmodels.ModelPublicAgentTraits{}}, + {schema: "public", entity: "api_key_assignments", model: generatedmodels.ModelPublicAPIKeyAssignments{}}, {schema: "public", entity: "arc_stage_parts", model: generatedmodels.ModelPublicArcStageParts{}}, {schema: "public", entity: "arc_stages", model: generatedmodels.ModelPublicArcStages{}}, {schema: "public", entity: "character_arcs", model: generatedmodels.ModelPublicCharacterArcs{}}, {schema: "public", entity: "chat_histories", model: generatedmodels.ModelPublicChatHistories{}}, {schema: "public", entity: "embeddings", model: generatedmodels.ModelPublicEmbeddings{}}, {schema: "public", entity: "learnings", model: generatedmodels.ModelPublicLearnings{}}, + {schema: "public", entity: "managed_api_keys", model: generatedmodels.ModelPublicManagedAPIKeys{}}, {schema: "public", entity: "oauth_clients", model: generatedmodels.ModelPublicOauthClients{}}, {schema: "public", entity: "persona_arc", model: generatedmodels.ModelPublicPersonaArc{}}, {schema: "public", entity: "plan_dependencies", model: generatedmodels.ModelPublicPlanDependencies{}}, @@ -32,6 +34,8 @@ func resolveSpecModels() []resolveSpecModel { {schema: "public", entity: "project_skills", model: generatedmodels.ModelPublicProjectSkills{}}, {schema: "public", entity: "projects", model: generatedmodels.ModelPublicProjects{}}, {schema: "public", entity: "stored_files", model: generatedmodels.ModelPublicStoredFiles{}}, + {schema: "public", entity: "tenant_users", model: generatedmodels.ModelPublicTenantUsers{}}, + {schema: "public", entity: "tenants", model: generatedmodels.ModelPublicTenants{}}, {schema: "public", entity: "thought_learning_links", model: generatedmodels.ModelPublicThoughtLearningLinks{}}, {schema: "public", entity: "thought_links", model: generatedmodels.ModelPublicThoughtLinks{}}, {schema: "public", entity: "thoughts", model: generatedmodels.ModelPublicThoughts{}}, diff --git a/internal/app/ui_assets.go b/internal/app/ui_assets.go index 430c073..7d3f7d0 100644 --- a/internal/app/ui_assets.go +++ b/internal/app/ui_assets.go @@ -7,8 +7,8 @@ import ( var ( //go:embed ui/dist - uiFiles embed.FS - uiDistFS fs.FS + uiFiles embed.FS + uiDistFS fs.FS indexHTML []byte ) diff --git a/internal/auth/keyring.go b/internal/auth/keyring.go index 56c3135..a75548f 100644 --- a/internal/auth/keyring.go +++ b/internal/auth/keyring.go @@ -1,14 +1,27 @@ package auth import ( + "crypto/rand" + "crypto/sha256" "crypto/subtle" + "encoding/hex" "fmt" + "strings" + "sync" "git.warky.dev/wdevs/amcs/internal/config" ) type Keyring struct { - keys []config.APIKey + mu sync.RWMutex + keys []config.APIKey + tenantsByKeyID map[string]string + managed map[string]managedKey +} + +type managedKey struct { + secretHash string + enabled bool } func NewKeyring(keys []config.APIKey) (*Keyring, error) { @@ -16,14 +29,112 @@ func NewKeyring(keys []config.APIKey) (*Keyring, error) { return nil, fmt.Errorf("keyring requires at least one key") } - return &Keyring{keys: append([]config.APIKey(nil), keys...)}, nil + tenantsByKeyID := make(map[string]string) + for _, key := range keys { + if tenantID := strings.TrimSpace(key.TenantID); tenantID != "" { + tenantsByKeyID[key.ID] = tenantID + } + } + return &Keyring{keys: append([]config.APIKey(nil), keys...), tenantsByKeyID: tenantsByKeyID, managed: make(map[string]managedKey)}, nil +} + +// NewManagedKeyring is used when API credentials are administered in the +// database rather than supplied through static configuration. +func NewManagedKeyring() *Keyring { + return &Keyring{tenantsByKeyID: make(map[string]string), managed: make(map[string]managedKey)} } func (k *Keyring) Lookup(value string) (string, bool) { + k.mu.RLock() + defer k.mu.RUnlock() for _, key := range k.keys { if subtle.ConstantTimeCompare([]byte(key.Value), []byte(value)) == 1 { return key.ID, true } } + hash := secretHash(value) + for keyID, key := range k.managed { + if key.enabled && subtle.ConstantTimeCompare([]byte(key.secretHash), []byte(hash)) == 1 { + return keyID, true + } + } return "", false } + +// TenantForKey returns the tenant boundary assigned to keyID. Unassigned +// configured keys retain the historical key-ID boundary for compatibility. +func (k *Keyring) TenantForKey(keyID string) string { + k.mu.RLock() + defer k.mu.RUnlock() + if tenantID := k.tenantsByKeyID[keyID]; tenantID != "" { + return tenantID + } + return keyID +} + +func (k *Keyring) AssignTenant(keyID, tenantID string) { + k.mu.Lock() + defer k.mu.Unlock() + if tenantID == "" { + delete(k.tenantsByKeyID, keyID) + return + } + k.tenantsByKeyID[keyID] = tenantID +} + +func (k *Keyring) AddManaged(keyID, secretHash string, enabled bool) { + k.mu.Lock() + defer k.mu.Unlock() + k.managed[keyID] = managedKey{secretHash: secretHash, enabled: enabled} +} + +func (k *Keyring) ConfiguredKeys() []config.APIKey { + k.mu.RLock() + defer k.mu.RUnlock() + return append([]config.APIKey(nil), k.keys...) +} + +func (k *Keyring) IsConfigured(keyID string) bool { + k.mu.RLock() + defer k.mu.RUnlock() + for _, key := range k.keys { + if key.ID == keyID { + return true + } + } + return false +} + +func (k *Keyring) IsSuperadmin(keyID string) bool { + k.mu.RLock() + defer k.mu.RUnlock() + for _, key := range k.keys { + if key.ID == keyID { + return key.Superadmin + } + } + return false +} + +func (k *Keyring) SetManagedEnabled(keyID string, enabled bool) { + k.mu.Lock() + defer k.mu.Unlock() + if key, ok := k.managed[keyID]; ok { + key.enabled = enabled + k.managed[keyID] = key + } +} + +func GenerateSecret() (string, string, error) { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", "", err + } + secret := "amcs_" + hex.EncodeToString(b) + return secret, secretHash(secret), nil +} + +func secretHash(secret string) string { + sum := sha256.Sum256([]byte(secret)) + return hex.EncodeToString(sum[:]) +} diff --git a/internal/auth/keyring_test.go b/internal/auth/keyring_test.go index 3297fee..a64c73b 100644 --- a/internal/auth/keyring_test.go +++ b/internal/auth/keyring_test.go @@ -36,6 +36,26 @@ func TestNewKeyringAndLookup(t *testing.T) { } } +func TestConfiguredKeyUsesTenantID(t *testing.T) { + keyring, err := NewKeyring([]config.APIKey{{ID: "agent-key", Value: "secret", TenantID: "acme"}}) + if err != nil { + t.Fatalf("NewKeyring() error = %v", err) + } + if got := keyring.TenantForKey("agent-key"); got != "acme" { + t.Fatalf("TenantForKey() = %q, want acme", got) + } +} + +func TestConfiguredKeySuperadmin(t *testing.T) { + keyring, err := NewKeyring([]config.APIKey{{ID: "operator", Value: "secret", Superadmin: true}}) + if err != nil { + t.Fatalf("NewKeyring() error = %v", err) + } + if !keyring.IsSuperadmin("operator") || keyring.IsSuperadmin("missing") { + t.Fatal("IsSuperadmin() did not return the configured role") + } +} + func TestMiddlewareAllowsHeaderAuthAndSetsContext(t *testing.T) { keyring, err := NewKeyring([]config.APIKey{{ID: "client-a", Value: "secret"}}) if err != nil { diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go index 836d7d4..3880d24 100644 --- a/internal/auth/middleware.go +++ b/internal/auth/middleware.go @@ -53,7 +53,11 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg } withTenant := func(ctx context.Context, keyID string) context.Context { ctx = context.WithValue(ctx, keyIDContextKey, keyID) - return tenancy.WithTenantKey(ctx, keyID) + tenantID := keyID + if keyring != nil { + tenantID = keyring.TenantForKey(keyID) + } + return tenancy.WithTenantKey(ctx, tenantID) } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/config/config.go b/internal/config/config.go index 281205a..8f8dad0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -53,6 +53,8 @@ type AuthConfig struct { type APIKey struct { ID string `yaml:"id"` Value string `yaml:"value"` + TenantID string `yaml:"tenant_id"` + Superadmin bool `yaml:"superadmin"` Description string `yaml:"description"` } diff --git a/internal/generatedmodels/sql_public_agent_guardrails.go b/internal/generatedmodels/sql_public_agent_guardrails.go index 3a614c9..58fca21 100644 --- a/internal/generatedmodels/sql_public_agent_guardrails.go +++ b/internal/generatedmodels/sql_public_agent_guardrails.go @@ -17,7 +17,9 @@ type ModelPublicAgentGuardrails struct { Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"` Severity sql_types.SqlString `bun:"severity,type:text,default:'medium',notnull," json:"severity"` Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"` + TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"` UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"` + RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants RelGuardrailIDPublicAgentPersonaGuardrails []*ModelPublicAgentPersonaGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicagentpersonaguardrails,omitempty"` // Has many ModelPublicAgentPersonaGuardrails RelGuardrailIDPublicPlanGuardrails []*ModelPublicPlanGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicplanguardrails,omitempty"` // Has many ModelPublicPlanGuardrails RelGuardrailIDPublicProjectGuardrails []*ModelPublicProjectGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicprojectguardrails,omitempty"` // Has many ModelPublicProjectGuardrails diff --git a/internal/generatedmodels/sql_public_agent_parts.go b/internal/generatedmodels/sql_public_agent_parts.go index 5c62578..3cb4802 100644 --- a/internal/generatedmodels/sql_public_agent_parts.go +++ b/internal/generatedmodels/sql_public_agent_parts.go @@ -18,7 +18,9 @@ type ModelPublicAgentParts struct { PartType sql_types.SqlString `bun:"part_type,type:text,notnull," json:"part_type"` Summary sql_types.SqlString `bun:"summary,type:text,notnull," json:"summary"` Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"` + TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"` UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"` + RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants RelPartIDPublicAgentPersonaParts []*ModelPublicAgentPersonaParts `bun:"rel:has-many,join:id=part_id" json:"relpartidpublicagentpersonaparts,omitempty"` // Has many ModelPublicAgentPersonaParts RelPartIDPublicArcStageParts []*ModelPublicArcStageParts `bun:"rel:has-many,join:id=part_id" json:"relpartidpublicarcstageparts,omitempty"` // Has many ModelPublicArcStageParts } diff --git a/internal/generatedmodels/sql_public_agent_personas.go b/internal/generatedmodels/sql_public_agent_personas.go index 2e1bdcf..bf545d9 100644 --- a/internal/generatedmodels/sql_public_agent_personas.go +++ b/internal/generatedmodels/sql_public_agent_personas.go @@ -20,7 +20,9 @@ type ModelPublicAgentPersonas struct { Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"` Summary sql_types.SqlString `bun:"summary,type:text,notnull," json:"summary"` Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"` + TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"` UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"` + RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants RelPersonaIDPublicAgentPersonaParts []*ModelPublicAgentPersonaParts `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonaparts,omitempty"` // Has many ModelPublicAgentPersonaParts RelPersonaIDPublicAgentPersonaSkills []*ModelPublicAgentPersonaSkills `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonaskills,omitempty"` // Has many ModelPublicAgentPersonaSkills RelPersonaIDPublicProjectPersonas []*ModelPublicProjectPersonas `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicprojectpersonas,omitempty"` // Has many ModelPublicProjectPersonas diff --git a/internal/generatedmodels/sql_public_agent_skills.go b/internal/generatedmodels/sql_public_agent_skills.go index 6aa174f..0859215 100644 --- a/internal/generatedmodels/sql_public_agent_skills.go +++ b/internal/generatedmodels/sql_public_agent_skills.go @@ -3,6 +3,7 @@ package generatedmodels import ( "fmt" + sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes" "github.com/uptrace/bun" ) @@ -19,8 +20,10 @@ type ModelPublicAgentSkills struct { LanguageTags sql_types.SqlStringArray `bun:"language_tags,type:text[],default:'{}',notnull," json:"language_tags"` LibraryTags sql_types.SqlStringArray `bun:"library_tags,type:text[],default:'{}',notnull," json:"library_tags"` Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"` - Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"` + Tags sql_types.SqlStringArray `bun:"tags,array,type:text[],default:'{}',notnull," json:"tags"` + TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"` UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"` + RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants RelSkillIDPublicAgentPersonaSkills []*ModelPublicAgentPersonaSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicagentpersonaskills,omitempty"` // Has many ModelPublicAgentPersonaSkills RelRelatedSkillIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=related_skill_id" json:"relrelatedskillidpubliclearnings,omitempty"` // Has many ModelPublicLearnings RelSkillIDPublicPlanSkills []*ModelPublicPlanSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicplanskills,omitempty"` // Has many ModelPublicPlanSkills diff --git a/internal/generatedmodels/sql_public_agent_traits.go b/internal/generatedmodels/sql_public_agent_traits.go index 08a7c74..7fed10b 100644 --- a/internal/generatedmodels/sql_public_agent_traits.go +++ b/internal/generatedmodels/sql_public_agent_traits.go @@ -16,8 +16,10 @@ type ModelPublicAgentTraits struct { Instruction sql_types.SqlString `bun:"instruction,type:text,default:'',notnull," json:"instruction"` Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"` Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"` + TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"` TraitType sql_types.SqlString `bun:"trait_type,type:text,notnull," json:"trait_type"` UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"` + RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants RelTraitIDPublicAgentPersonaTraits []*ModelPublicAgentPersonaTraits `bun:"rel:has-many,join:id=trait_id" json:"reltraitidpublicagentpersonatraits,omitempty"` // Has many ModelPublicAgentPersonaTraits } diff --git a/internal/generatedmodels/sql_public_api_key_assignments.go b/internal/generatedmodels/sql_public_api_key_assignments.go new file mode 100644 index 0000000..6715733 --- /dev/null +++ b/internal/generatedmodels/sql_public_api_key_assignments.go @@ -0,0 +1,66 @@ +// Code generated by relspecgo. DO NOT EDIT. +package generatedmodels + +import ( + sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes" + "github.com/uptrace/bun" +) + +type ModelPublicAPIKeyAssignments struct { + bun.BaseModel `bun:"table:public.api_key_assignments,alias:api_key_assignments"` + KeyID sql_types.SqlString `bun:"key_id,type:text,pk," json:"key_id"` + CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"` + Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"` + Enabled bool `bun:"enabled,type:boolean,default:true,notnull," json:"enabled"` + Source sql_types.SqlString `bun:"source,type:text,notnull," json:"source"` + TenantID sql_types.SqlString `bun:"tenant_id,type:text,notnull," json:"tenant_id"` + UserID sql_types.SqlString `bun:"user_id,type:text,nullzero," json:"user_id"` + RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants + RelUserID *ModelPublicTenantUsers `bun:"rel:has-one,join:user_id=id" json:"reluserid,omitempty"` // Has one ModelPublicTenantUsers + RelKeyIDPublicManagedAPIKeys []*ModelPublicManagedAPIKeys `bun:"rel:has-many,join:key_id=key_id" json:"relkeyidpublicmanagedapikeys,omitempty"` // Has many ModelPublicManagedAPIKeys +} + +// TableName returns the table name for ModelPublicAPIKeyAssignments +func (m ModelPublicAPIKeyAssignments) TableName() string { + return "public.api_key_assignments" +} + +// TableNameOnly returns the table name without schema for ModelPublicAPIKeyAssignments +func (m ModelPublicAPIKeyAssignments) TableNameOnly() string { + return "api_key_assignments" +} + +// SchemaName returns the schema name for ModelPublicAPIKeyAssignments +func (m ModelPublicAPIKeyAssignments) SchemaName() string { + return "public" +} + +// GetID returns the primary key value +func (m ModelPublicAPIKeyAssignments) GetID() string { + return m.KeyID.String() +} + +// GetIDStr returns the primary key as a string +func (m ModelPublicAPIKeyAssignments) GetIDStr() string { + return m.KeyID.String() +} + +// SetID sets the primary key value +func (m ModelPublicAPIKeyAssignments) SetID(newid string) { + m.UpdateID(newid) +} + +// UpdateID updates the primary key value +func (m *ModelPublicAPIKeyAssignments) UpdateID(newid string) { + m.KeyID.FromString(newid) +} + +// GetIDName returns the name of the primary key column +func (m ModelPublicAPIKeyAssignments) GetIDName() string { + return "key_id" +} + +// GetPrefix returns the table prefix +func (m ModelPublicAPIKeyAssignments) GetPrefix() string { + return "AKA" +} diff --git a/internal/generatedmodels/sql_public_character_arcs.go b/internal/generatedmodels/sql_public_character_arcs.go index a70043e..def552d 100644 --- a/internal/generatedmodels/sql_public_character_arcs.go +++ b/internal/generatedmodels/sql_public_character_arcs.go @@ -14,7 +14,9 @@ type ModelPublicCharacterArcs struct { Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"` Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"` Summary sql_types.SqlString `bun:"summary,type:text,default:'',notnull," json:"summary"` + TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"` UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"` + RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants RelArcIDPublicArcStages []*ModelPublicArcStages `bun:"rel:has-many,join:id=arc_id" json:"relarcidpublicarcstages,omitempty"` // Has many ModelPublicArcStages RelArcIDPublicPersonaArcs []*ModelPublicPersonaArc `bun:"rel:has-many,join:id=arc_id" json:"relarcidpublicpersonaarcs,omitempty"` // Has many ModelPublicPersonaArc } diff --git a/internal/generatedmodels/sql_public_chat_histories.go b/internal/generatedmodels/sql_public_chat_histories.go index a3b3979..b2da930 100644 --- a/internal/generatedmodels/sql_public_chat_histories.go +++ b/internal/generatedmodels/sql_public_chat_histories.go @@ -19,10 +19,11 @@ type ModelPublicChatHistories struct { ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"` SessionID sql_types.SqlString `bun:"session_id,type:text,notnull," json:"session_id"` Summary sql_types.SqlString `bun:"summary,type:text,nullzero," json:"summary"` - TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero," json:"tenant_key"` + TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"` Title sql_types.SqlString `bun:"title,type:text,nullzero," json:"title"` UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"` RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects + RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants } // TableName returns the table name for ModelPublicChatHistories diff --git a/internal/generatedmodels/sql_public_learnings.go b/internal/generatedmodels/sql_public_learnings.go index 1f839f8..a67b96e 100644 --- a/internal/generatedmodels/sql_public_learnings.go +++ b/internal/generatedmodels/sql_public_learnings.go @@ -30,13 +30,14 @@ type ModelPublicLearnings struct { Summary sql_types.SqlString `bun:"summary,type:text,notnull," json:"summary"` SupersedesLearningID sql_types.SqlInt64 `bun:"supersedes_learning_id,type:bigint,nullzero," json:"supersedes_learning_id"` Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"` - TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero," json:"tenant_key"` + TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"` UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"` RelDuplicateOfLearningID *ModelPublicLearnings `bun:"rel:has-one,join:duplicate_of_learning_id=id" json:"relduplicateoflearningid,omitempty"` // Has one ModelPublicLearnings RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects RelRelatedSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:related_skill_id=id" json:"relrelatedskillid,omitempty"` // Has one ModelPublicAgentSkills RelRelatedThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:related_thought_id=id" json:"relrelatedthoughtid,omitempty"` // Has one ModelPublicThoughts RelSupersedesLearningID *ModelPublicLearnings `bun:"rel:has-one,join:supersedes_learning_id=id" json:"relsupersedeslearningid,omitempty"` // Has one ModelPublicLearnings + RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants RelLearningIDPublicThoughtLearningLinks []*ModelPublicThoughtLearningLinks `bun:"rel:has-many,join:id=learning_id" json:"rellearningidpublicthoughtlearninglinks,omitempty"` // Has many ModelPublicThoughtLearningLinks } diff --git a/internal/generatedmodels/sql_public_managed_api_keys.go b/internal/generatedmodels/sql_public_managed_api_keys.go new file mode 100644 index 0000000..e35da5f --- /dev/null +++ b/internal/generatedmodels/sql_public_managed_api_keys.go @@ -0,0 +1,59 @@ +// Code generated by relspecgo. DO NOT EDIT. +package generatedmodels + +import ( + sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes" + "github.com/uptrace/bun" +) + +type ModelPublicManagedAPIKeys struct { + bun.BaseModel `bun:"table:public.managed_api_keys,alias:managed_api_keys"` + KeyID sql_types.SqlString `bun:"key_id,type:text,pk," json:"key_id"` + SecretHash sql_types.SqlString `bun:"secret_hash,type:text,notnull," json:"secret_hash"` + RelKeyID *ModelPublicAPIKeyAssignments `bun:"rel:has-one,join:key_id=key_id" json:"relkeyid,omitempty"` // Has one ModelPublicAPIKeyAssignments +} + +// TableName returns the table name for ModelPublicManagedAPIKeys +func (m ModelPublicManagedAPIKeys) TableName() string { + return "public.managed_api_keys" +} + +// TableNameOnly returns the table name without schema for ModelPublicManagedAPIKeys +func (m ModelPublicManagedAPIKeys) TableNameOnly() string { + return "managed_api_keys" +} + +// SchemaName returns the schema name for ModelPublicManagedAPIKeys +func (m ModelPublicManagedAPIKeys) SchemaName() string { + return "public" +} + +// GetID returns the primary key value +func (m ModelPublicManagedAPIKeys) GetID() string { + return m.KeyID.String() +} + +// GetIDStr returns the primary key as a string +func (m ModelPublicManagedAPIKeys) GetIDStr() string { + return m.KeyID.String() +} + +// SetID sets the primary key value +func (m ModelPublicManagedAPIKeys) SetID(newid string) { + m.UpdateID(newid) +} + +// UpdateID updates the primary key value +func (m *ModelPublicManagedAPIKeys) UpdateID(newid string) { + m.KeyID.FromString(newid) +} + +// GetIDName returns the name of the primary key column +func (m ModelPublicManagedAPIKeys) GetIDName() string { + return "key_id" +} + +// GetPrefix returns the table prefix +func (m ModelPublicManagedAPIKeys) GetPrefix() string { + return "MAK" +} diff --git a/internal/generatedmodels/sql_public_persona_arc.go b/internal/generatedmodels/sql_public_persona_arc.go index 5e07d42..213ace3 100644 --- a/internal/generatedmodels/sql_public_persona_arc.go +++ b/internal/generatedmodels/sql_public_persona_arc.go @@ -10,9 +10,9 @@ import ( type ModelPublicPersonaArc struct { bun.BaseModel `bun:"table:public.persona_arc,alias:persona_arc"` ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"` - PersonaID int64 `bun:"persona_id,type:bigint,pk," json:"persona_id"` ArcID int64 `bun:"arc_id,type:bigint,notnull," json:"arc_id"` CurrentStageID int64 `bun:"current_stage_id,type:bigint,notnull," json:"current_stage_id"` + PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"` UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"` RelArcID *ModelPublicCharacterArcs `bun:"rel:has-one,join:arc_id=id" json:"relarcid,omitempty"` // Has one ModelPublicCharacterArcs RelCurrentStageID *ModelPublicArcStages `bun:"rel:has-one,join:current_stage_id=id" json:"relcurrentstageid,omitempty"` // Has one ModelPublicArcStages diff --git a/internal/generatedmodels/sql_public_plans.go b/internal/generatedmodels/sql_public_plans.go index 431afed..cebfe98 100644 --- a/internal/generatedmodels/sql_public_plans.go +++ b/internal/generatedmodels/sql_public_plans.go @@ -23,11 +23,12 @@ type ModelPublicPlans struct { Status sql_types.SqlString `bun:"status,type:text,default:'draft',notnull," json:"status"` // draft, active, blocked, completed, cancelled, superseded SupersedesPlanID sql_types.SqlInt64 `bun:"supersedes_plan_id,type:bigint,nullzero," json:"supersedes_plan_id"` Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"` - TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero," json:"tenant_key"` + TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"` Title sql_types.SqlString `bun:"title,type:text,notnull," json:"title"` UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"` RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects RelSupersedesPlanID *ModelPublicPlans `bun:"rel:has-one,join:supersedes_plan_id=id" json:"relsupersedesplanid,omitempty"` // Has one ModelPublicPlans + RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants RelDependsOnPlanIDPublicPlanDependencies []*ModelPublicPlanDependencies `bun:"rel:has-many,join:id=depends_on_plan_id" json:"reldependsonplanidpublicplandependencies,omitempty"` // Has many ModelPublicPlanDependencies RelPlanIDPublicPlanDependencies []*ModelPublicPlanDependencies `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplandependencies,omitempty"` // Has many ModelPublicPlanDependencies RelPlanAIDPublicPlanRelatedPlans []*ModelPublicPlanRelatedPlans `bun:"rel:has-many,join:id=plan_a_id" json:"relplanaidpublicplanrelatedplans,omitempty"` // Has many ModelPublicPlanRelatedPlans diff --git a/internal/generatedmodels/sql_public_projects.go b/internal/generatedmodels/sql_public_projects.go index f1f16cb..2480d33 100644 --- a/internal/generatedmodels/sql_public_projects.go +++ b/internal/generatedmodels/sql_public_projects.go @@ -14,8 +14,9 @@ type ModelPublicProjects struct { Description sql_types.SqlString `bun:"description,type:text,nullzero," json:"description"` GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"` LastActiveAt sql_types.SqlTimeStamp `bun:"last_active_at,type:timestamptz,default:now(),nullzero," json:"last_active_at"` - Name sql_types.SqlString `bun:"name,type:text,notnull,unique:uidx_projects_tenant_key_name," json:"name"` - TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero,unique:uidx_projects_tenant_key_name," json:"tenant_key"` + Name sql_types.SqlString `bun:"name,type:text,notnull,unique:uidx_projects_tenant_id_name," json:"name"` + TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero,unique:uidx_projects_tenant_id_name," json:"tenant_id"` + RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants RelProjectIDPublicProjectPersonas []*ModelPublicProjectPersonas `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicprojectpersonas,omitempty"` // Has many ModelPublicProjectPersonas RelProjectIDPublicThoughts []*ModelPublicThoughts `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicthoughts,omitempty"` // Has many ModelPublicThoughts RelProjectIDPublicStoredFiles []*ModelPublicStoredFiles `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicstoredfiles,omitempty"` // Has many ModelPublicStoredFiles diff --git a/internal/generatedmodels/sql_public_stored_files.go b/internal/generatedmodels/sql_public_stored_files.go index b00c57e..91bd4e5 100644 --- a/internal/generatedmodels/sql_public_stored_files.go +++ b/internal/generatedmodels/sql_public_stored_files.go @@ -20,10 +20,11 @@ type ModelPublicStoredFiles struct { ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"` Sha256 sql_types.SqlString `bun:"sha256,type:text,notnull," json:"sha256"` SizeBytes int64 `bun:"size_bytes,type:bigint,notnull," json:"size_bytes"` - TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero," json:"tenant_key"` + TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"` ThoughtID sql_types.SqlInt64 `bun:"thought_id,type:bigint,nullzero," json:"thought_id"` UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"` RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects + RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants RelThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:thought_id=id" json:"relthoughtid,omitempty"` // Has one ModelPublicThoughts } diff --git a/internal/generatedmodels/sql_public_tenant_users.go b/internal/generatedmodels/sql_public_tenant_users.go new file mode 100644 index 0000000..1de2b18 --- /dev/null +++ b/internal/generatedmodels/sql_public_tenant_users.go @@ -0,0 +1,63 @@ +// Code generated by relspecgo. DO NOT EDIT. +package generatedmodels + +import ( + sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes" + "github.com/uptrace/bun" +) + +type ModelPublicTenantUsers struct { + bun.BaseModel `bun:"table:public.tenant_users,alias:tenant_users"` + ID sql_types.SqlString `bun:"id,type:text,pk," json:"id"` + CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"` + Email sql_types.SqlString `bun:"email,type:text,nullzero,unique:uidx_tenant_users_tenant_id_email," json:"email"` + Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"` + TenantID sql_types.SqlString `bun:"tenant_id,type:text,notnull,unique:uidx_tenant_users_tenant_id_email," json:"tenant_id"` + RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants + RelUserIDPublicAPIKeyAssignments []*ModelPublicAPIKeyAssignments `bun:"rel:has-many,join:id=user_id" json:"reluseridpublicapikeyassignments,omitempty"` // Has many ModelPublicAPIKeyAssignments +} + +// TableName returns the table name for ModelPublicTenantUsers +func (m ModelPublicTenantUsers) TableName() string { + return "public.tenant_users" +} + +// TableNameOnly returns the table name without schema for ModelPublicTenantUsers +func (m ModelPublicTenantUsers) TableNameOnly() string { + return "tenant_users" +} + +// SchemaName returns the schema name for ModelPublicTenantUsers +func (m ModelPublicTenantUsers) SchemaName() string { + return "public" +} + +// GetID returns the primary key value +func (m ModelPublicTenantUsers) GetID() string { + return m.ID.String() +} + +// GetIDStr returns the primary key as a string +func (m ModelPublicTenantUsers) GetIDStr() string { + return m.ID.String() +} + +// SetID sets the primary key value +func (m ModelPublicTenantUsers) SetID(newid string) { + m.UpdateID(newid) +} + +// UpdateID updates the primary key value +func (m *ModelPublicTenantUsers) UpdateID(newid string) { + m.ID.FromString(newid) +} + +// GetIDName returns the name of the primary key column +func (m ModelPublicTenantUsers) GetIDName() string { + return "id" +} + +// GetPrefix returns the table prefix +func (m ModelPublicTenantUsers) GetPrefix() string { + return "TUE" +} diff --git a/internal/generatedmodels/sql_public_tenants.go b/internal/generatedmodels/sql_public_tenants.go new file mode 100644 index 0000000..ff616ea --- /dev/null +++ b/internal/generatedmodels/sql_public_tenants.go @@ -0,0 +1,73 @@ +// Code generated by relspecgo. DO NOT EDIT. +package generatedmodels + +import ( + sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes" + "github.com/uptrace/bun" +) + +type ModelPublicTenants struct { + bun.BaseModel `bun:"table:public.tenants,alias:tenants"` + ID sql_types.SqlString `bun:"id,type:text,pk," json:"id"` + CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"` + Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"` + RelTenantIDPublicAgentPersonas []*ModelPublicAgentPersonas `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagentpersonas,omitempty"` // Has many ModelPublicAgentPersonas + RelTenantIDPublicAgentParts []*ModelPublicAgentParts `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagentparts,omitempty"` // Has many ModelPublicAgentParts + RelTenantIDPublicAgentTraits []*ModelPublicAgentTraits `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagenttraits,omitempty"` // Has many ModelPublicAgentTraits + RelTenantIDPublicCharacterArcs []*ModelPublicCharacterArcs `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpubliccharacterarcs,omitempty"` // Has many ModelPublicCharacterArcs + RelTenantIDPublicThoughts []*ModelPublicThoughts `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicthoughts,omitempty"` // Has many ModelPublicThoughts + RelTenantIDPublicProjects []*ModelPublicProjects `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicprojects,omitempty"` // Has many ModelPublicProjects + RelTenantIDPublicStoredFiles []*ModelPublicStoredFiles `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicstoredfiles,omitempty"` // Has many ModelPublicStoredFiles + RelTenantIDPublicTenantUsers []*ModelPublicTenantUsers `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublictenantusers,omitempty"` // Has many ModelPublicTenantUsers + RelTenantIDPublicAPIKeyAssignments []*ModelPublicAPIKeyAssignments `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicapikeyassignments,omitempty"` // Has many ModelPublicAPIKeyAssignments + RelTenantIDPublicChatHistories []*ModelPublicChatHistories `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicchathistories,omitempty"` // Has many ModelPublicChatHistories + RelTenantIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpubliclearnings,omitempty"` // Has many ModelPublicLearnings + RelTenantIDPublicPlans []*ModelPublicPlans `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicplans,omitempty"` // Has many ModelPublicPlans + RelTenantIDPublicAgentSkills []*ModelPublicAgentSkills `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagentskills,omitempty"` // Has many ModelPublicAgentSkills + RelTenantIDPublicAgentGuardrails []*ModelPublicAgentGuardrails `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagentguardrails,omitempty"` // Has many ModelPublicAgentGuardrails +} + +// TableName returns the table name for ModelPublicTenants +func (m ModelPublicTenants) TableName() string { + return "public.tenants" +} + +// TableNameOnly returns the table name without schema for ModelPublicTenants +func (m ModelPublicTenants) TableNameOnly() string { + return "tenants" +} + +// SchemaName returns the schema name for ModelPublicTenants +func (m ModelPublicTenants) SchemaName() string { + return "public" +} + +// GetID returns the primary key value +func (m ModelPublicTenants) GetID() string { + return m.ID.String() +} + +// GetIDStr returns the primary key as a string +func (m ModelPublicTenants) GetIDStr() string { + return m.ID.String() +} + +// SetID sets the primary key value +func (m ModelPublicTenants) SetID(newid string) { + m.UpdateID(newid) +} + +// UpdateID updates the primary key value +func (m *ModelPublicTenants) UpdateID(newid string) { + m.ID.FromString(newid) +} + +// GetIDName returns the name of the primary key column +func (m ModelPublicTenants) GetIDName() string { + return "id" +} + +// GetPrefix returns the table prefix +func (m ModelPublicTenants) GetPrefix() string { + return "TEN" +} diff --git a/internal/generatedmodels/sql_public_thoughts.go b/internal/generatedmodels/sql_public_thoughts.go index b8d5584..e152548 100644 --- a/internal/generatedmodels/sql_public_thoughts.go +++ b/internal/generatedmodels/sql_public_thoughts.go @@ -16,9 +16,10 @@ type ModelPublicThoughts struct { GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"` Metadata sql_types.SqlJSONB `bun:"metadata,type:jsonb,default:{}::jsonb,nullzero," json:"metadata"` ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"` - TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero," json:"tenant_key"` + TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"` UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),nullzero," json:"updated_at"` RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects + RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants RelFromIDPublicThoughtLinks []*ModelPublicThoughtLinks `bun:"rel:has-many,join:id=from_id" json:"relfromidpublicthoughtlinks,omitempty"` // Has many ModelPublicThoughtLinks RelToIDPublicThoughtLinks []*ModelPublicThoughtLinks `bun:"rel:has-many,join:id=to_id" json:"reltoidpublicthoughtlinks,omitempty"` // Has many ModelPublicThoughtLinks RelThoughtIDPublicThoughtLearningLinks []*ModelPublicThoughtLearningLinks `bun:"rel:has-many,join:id=thought_id" json:"relthoughtidpublicthoughtlearninglinks,omitempty"` // Has many ModelPublicThoughtLearningLinks diff --git a/internal/store/files.go b/internal/store/files.go index 279e512..b98545d 100644 --- a/internal/store/files.go +++ b/internal/store/files.go @@ -15,7 +15,7 @@ import ( func (db *DB) InsertStoredFile(ctx context.Context, file thoughttypes.StoredFile) (thoughttypes.StoredFile, error) { row := db.pool.QueryRow(ctx, ` - insert into stored_files (thought_id, project_id, tenant_key, name, media_type, kind, encoding, size_bytes, sha256, content) + insert into stored_files (thought_id, project_id, tenant_id, name, media_type, kind, encoding, size_bytes, sha256, content) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) returning id, guid, thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, created_at, updated_at `, file.ThoughtID, file.ProjectID, tenantKeyPtr(ctx), file.Name, file.MediaType, file.Kind, file.Encoding, file.SizeBytes, file.SHA256, file.Content) @@ -46,7 +46,7 @@ func (db *DB) GetStoredFile(ctx context.Context, id uuid.UUID) (thoughttypes.Sto row := db.pool.QueryRow(ctx, ` select id, guid, thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, content, created_at, updated_at from stored_files - where guid = $1`+tenantSQL(ctx, &args, "tenant_key"), args...) + where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...) var model generatedmodels.ModelPublicStoredFiles if err := row.Scan( @@ -77,7 +77,7 @@ func (db *DB) ListStoredFiles(ctx context.Context, filter thoughttypes.StoredFil args := make([]any, 0, 4) conditions := make([]string, 0, 3) - addTenantCondition(ctx, &args, &conditions, "tenant_key") + addTenantCondition(ctx, &args, &conditions, "tenant_id") if filter.ThoughtID != nil { args = append(args, *filter.ThoughtID) conditions = append(conditions, fmt.Sprintf("thought_id = $%d", len(args))) diff --git a/internal/store/plans.go b/internal/store/plans.go index 77603cd..77aab02 100644 --- a/internal/store/plans.go +++ b/internal/store/plans.go @@ -475,4 +475,3 @@ func canonicalPlanPair(a, b int64) (int64, int64) { } return b, a } - diff --git a/internal/store/projects.go b/internal/store/projects.go index 24d5b1f..b74d2ac 100644 --- a/internal/store/projects.go +++ b/internal/store/projects.go @@ -14,7 +14,7 @@ import ( func (db *DB) CreateProject(ctx context.Context, name, description string) (thoughttypes.Project, error) { row := db.pool.QueryRow(ctx, ` - insert into projects (name, description, tenant_key) + insert into projects (name, description, tenant_id) values ($1, $2, $3) returning id, guid, name, description, created_at, last_active_at `, name, description, tenantKeyPtr(ctx)) @@ -49,7 +49,7 @@ func (db *DB) getProjectByGUID(ctx context.Context, id uuid.UUID) (thoughttypes. row := db.pool.QueryRow(ctx, ` select id, guid, name, description, created_at, last_active_at from projects - where guid = $1`+tenantSQL(ctx, &args, "tenant_key"), args...) + where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...) return scanProject(row) } @@ -58,7 +58,7 @@ func (db *DB) getProjectByName(ctx context.Context, name string) (thoughttypes.P row := db.pool.QueryRow(ctx, ` select id, guid, name, description, created_at, last_active_at from projects - where name = $1`+tenantSQL(ctx, &args, "tenant_key"), args...) + where name = $1`+tenantSQL(ctx, &args, "tenant_id"), args...) return scanProject(row) } @@ -78,7 +78,7 @@ func (db *DB) ListProjects(ctx context.Context) ([]thoughttypes.ProjectSummary, where := "" if key, ok := tenantKey(ctx); ok { args = append(args, key) - where = "where p.tenant_key = $1" + where = "where p.tenant_id = $1" } rows, err := db.pool.Query(ctx, ` select p.id, p.guid, p.name, p.description, p.created_at, p.last_active_at, count(t.id) as thought_count @@ -113,7 +113,7 @@ func (db *DB) ListProjects(ctx context.Context) ([]thoughttypes.ProjectSummary, func (db *DB) TouchProject(ctx context.Context, id int64) error { args := []any{id} - tag, err := db.pool.Exec(ctx, `update projects set last_active_at = now() where id = $1`+tenantSQL(ctx, &args, "tenant_key"), args...) + tag, err := db.pool.Exec(ctx, `update projects set last_active_at = now() where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...) if err != nil { return fmt.Errorf("touch project: %w", err) } diff --git a/internal/store/skills.go b/internal/store/skills.go index 0986412..70b329d 100644 --- a/internal/store/skills.go +++ b/internal/store/skills.go @@ -28,10 +28,10 @@ func (db *DB) AddSkill(ctx context.Context, skill ext.AgentSkill) (ext.AgentSkil skill.DomainTags = []string{} } row := db.pool.QueryRow(ctx, ` - insert into agent_skills (name, description, content, tags, language_tags, library_tags, framework_tags, domain_tags) - values ($1, $2, $3, $4, $5, $6, $7, $8) + insert into agent_skills (name, description, content, tenant_id, tags, language_tags, library_tags, framework_tags, domain_tags) + values ($1, $2, $3, $4, $5, $6, $7, $8, $9) returning id, guid, created_at, updated_at - `, skill.Name, skill.Description, skill.Content, skill.Tags, + `, skill.Name, skill.Description, skill.Content, tenantKeyPtr(ctx), skill.Tags, skill.LanguageTags, skill.LibraryTags, skill.FrameworkTags, skill.DomainTags) created := skill @@ -47,7 +47,8 @@ func (db *DB) AddSkill(ctx context.Context, skill ext.AgentSkill) (ext.AgentSkil } func (db *DB) RemoveSkill(ctx context.Context, id int64) error { - tag, err := db.pool.Exec(ctx, `delete from agent_skills where id = $1`, id) + args := []any{id} + tag, err := db.pool.Exec(ctx, `delete from agent_skills where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...) if err != nil { return fmt.Errorf("delete agent skill: %w", err) } @@ -60,9 +61,14 @@ func (db *DB) RemoveSkill(ctx context.Context, id int64) error { func (db *DB) ListSkills(ctx context.Context, tag string) ([]ext.AgentSkill, error) { q := `select id, name, description, content, tags::text[], language_tags::text[], library_tags::text[], framework_tags::text[], domain_tags::text[], created_at, updated_at from agent_skills` args := []any{} + conditions := []string{} if t := strings.TrimSpace(tag); t != "" { args = append(args, t) - q += fmt.Sprintf(" where $%d = any(tags) or $%d = any(language_tags) or $%d = any(library_tags) or $%d = any(framework_tags) or $%d = any(domain_tags)", len(args), len(args), len(args), len(args), len(args)) + conditions = append(conditions, fmt.Sprintf("($%d = any(tags) or $%d = any(language_tags) or $%d = any(library_tags) or $%d = any(framework_tags) or $%d = any(domain_tags))", len(args), len(args), len(args), len(args), len(args))) + } + addTenantCondition(ctx, &args, &conditions, "tenant_id") + if len(conditions) > 0 { + q += " where " + strings.Join(conditions, " and ") } q += " order by name" @@ -135,7 +141,8 @@ func normalizeSkillSlices(skill *ext.AgentSkill) { } func (db *DB) GetSkill(ctx context.Context, id int64) (ext.AgentSkill, error) { - row := db.pool.QueryRow(ctx, `select `+skillSelectCols+` from agent_skills where id = $1`, id) + args := []any{id} + row := db.pool.QueryRow(ctx, `select `+skillSelectCols+` from agent_skills where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...) s, err := scanSkill(row) if err != nil { return ext.AgentSkill{}, fmt.Errorf("get agent skill: %w", err) @@ -144,7 +151,8 @@ func (db *DB) GetSkill(ctx context.Context, id int64) (ext.AgentSkill, error) { } func (db *DB) GetSkillByName(ctx context.Context, name string) (ext.AgentSkill, error) { - row := db.pool.QueryRow(ctx, `select `+skillSelectCols+` from agent_skills where name = $1`, name) + args := []any{name} + row := db.pool.QueryRow(ctx, `select `+skillSelectCols+` from agent_skills where name = $1`+tenantSQL(ctx, &args, "tenant_id"), args...) s, err := scanSkill(row) if err != nil { return ext.AgentSkill{}, fmt.Errorf("get agent skill by name: %w", err) @@ -153,10 +161,10 @@ func (db *DB) GetSkillByName(ctx context.Context, name string) (ext.AgentSkill, } func (db *DB) GetGuardrailByName(ctx context.Context, name string) (ext.AgentGuardrail, error) { + args := []any{name} row := db.pool.QueryRow(ctx, ` select id, name, description, content, severity, tags::text[], created_at, updated_at - from agent_guardrails where name = $1 - `, name) + from agent_guardrails where name = $1`+tenantSQL(ctx, &args, "tenant_id"), args...) var model generatedmodels.ModelPublicAgentGuardrails var tags []string @@ -189,10 +197,10 @@ func (db *DB) AddGuardrail(ctx context.Context, g ext.AgentGuardrail) (ext.Agent g.Severity = "medium" } row := db.pool.QueryRow(ctx, ` - insert into agent_guardrails (name, description, content, severity, tags) - values ($1, $2, $3, $4, $5) + insert into agent_guardrails (name, description, content, severity, tenant_id, tags) + values ($1, $2, $3, $4, $5, $6) returning id, guid, created_at, updated_at - `, g.Name, g.Description, g.Content, g.Severity, g.Tags) + `, g.Name, g.Description, g.Content, g.Severity, tenantKeyPtr(ctx), g.Tags) created := g var model generatedmodels.ModelPublicAgentGuardrails @@ -207,7 +215,8 @@ func (db *DB) AddGuardrail(ctx context.Context, g ext.AgentGuardrail) (ext.Agent } func (db *DB) RemoveGuardrail(ctx context.Context, id int64) error { - tag, err := db.pool.Exec(ctx, `delete from agent_guardrails where id = $1`, id) + args := []any{id} + tag, err := db.pool.Exec(ctx, `delete from agent_guardrails where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...) if err != nil { return fmt.Errorf("delete agent guardrail: %w", err) } @@ -229,6 +238,7 @@ func (db *DB) ListGuardrails(ctx context.Context, tag, severity string) ([]ext.A args = append(args, s) conditions = append(conditions, fmt.Sprintf("severity = $%d", len(args))) } + addTenantCondition(ctx, &args, &conditions, "tenant_id") q := `select id, name, description, content, severity, tags::text[], created_at, updated_at from agent_guardrails` if len(conditions) > 0 { @@ -268,10 +278,10 @@ func (db *DB) ListGuardrails(ctx context.Context, tag, severity string) ([]ext.A } func (db *DB) GetGuardrail(ctx context.Context, id int64) (ext.AgentGuardrail, error) { + args := []any{id} row := db.pool.QueryRow(ctx, ` select id, name, description, content, severity, tags::text[], created_at, updated_at - from agent_guardrails where id = $1 - `, id) + from agent_guardrails where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...) var model generatedmodels.ModelPublicAgentGuardrails var tags []string diff --git a/internal/store/thoughts.go b/internal/store/thoughts.go index 0875f31..f872d5c 100644 --- a/internal/store/thoughts.go +++ b/internal/store/thoughts.go @@ -31,7 +31,7 @@ func (db *DB) InsertThought(ctx context.Context, thought thoughttypes.Thought, e }() row := tx.QueryRow(ctx, ` - insert into thoughts (content, metadata, project_id, tenant_key) + insert into thoughts (content, metadata, project_id, tenant_id) values ($1, $2::jsonb, $3, $4) returning id, guid, created_at, updated_at `, thought.Content, metadata, thought.ProjectID, tenantKeyPtr(ctx)) @@ -123,7 +123,7 @@ func (db *DB) ListThoughts(ctx context.Context, filter thoughttypes.ListFilter) args := make([]any, 0, 6) conditions := []string{} - addTenantCondition(ctx, &args, &conditions, "tenant_key") + addTenantCondition(ctx, &args, &conditions, "tenant_id") if !filter.IncludeArchived { conditions = append(conditions, "archived_at is null") } @@ -189,7 +189,7 @@ func (db *DB) Stats(ctx context.Context) (thoughttypes.ThoughtStats, error) { var total int statsArgs := []any{} statsConditions := []string{"archived_at is null"} - addTenantCondition(ctx, &statsArgs, &statsConditions, "tenant_key") + addTenantCondition(ctx, &statsArgs, &statsConditions, "tenant_id") if err := db.pool.QueryRow(ctx, `select count(*) from thoughts where `+strings.Join(statsConditions, " and "), statsArgs...).Scan(&total); err != nil { return thoughttypes.ThoughtStats{}, fmt.Errorf("count thoughts: %w", err) } @@ -241,7 +241,7 @@ func (db *DB) GetThought(ctx context.Context, id uuid.UUID) (thoughttypes.Though row := db.pool.QueryRow(ctx, ` select id, guid, content, metadata, project_id, archived_at, created_at, updated_at from thoughts - where guid = $1`+tenantSQL(ctx, &args, "tenant_key"), args...) + where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...) var model generatedmodels.ModelPublicThoughts if err := row.Scan(&model.ID, &model.GUID, &model.Content, &model.Metadata, &model.ProjectID, &model.ArchivedAt, &model.CreatedAt, &model.UpdatedAt); err != nil { @@ -264,7 +264,7 @@ func (db *DB) GetThoughtByID(ctx context.Context, id int64) (thoughttypes.Though row := db.pool.QueryRow(ctx, ` select id, guid, content, metadata, project_id, archived_at, created_at, updated_at from thoughts - where id = $1`+tenantSQL(ctx, &args, "tenant_key"), args...) + where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...) var model generatedmodels.ModelPublicThoughts if err := row.Scan(&model.ID, &model.GUID, &model.Content, &model.Metadata, &model.ProjectID, &model.ArchivedAt, &model.CreatedAt, &model.UpdatedAt); err != nil { @@ -303,7 +303,7 @@ func (db *DB) UpdateThought(ctx context.Context, id uuid.UUID, content string, e metadata = $3::jsonb, project_id = $4, updated_at = now() - where guid = $1`+tenantSQL(ctx, &args, "tenant_key"), args...) + where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...) if err != nil { return thoughttypes.Thought{}, fmt.Errorf("update thought: %w", err) } @@ -342,7 +342,7 @@ func (db *DB) UpdateThoughtMetadata(ctx context.Context, id int64, metadata thou update thoughts set metadata = $2::jsonb, updated_at = now() - where id = $1`+tenantSQL(ctx, &args, "tenant_key"), args...) + where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...) if err != nil { return thoughttypes.Thought{}, fmt.Errorf("update thought metadata: %w", err) } @@ -355,7 +355,7 @@ func (db *DB) UpdateThoughtMetadata(ctx context.Context, id int64, metadata thou func (db *DB) DeleteThought(ctx context.Context, id uuid.UUID) error { args := []any{id} - tag, err := db.pool.Exec(ctx, `delete from thoughts where guid = $1`+tenantSQL(ctx, &args, "tenant_key"), args...) + tag, err := db.pool.Exec(ctx, `delete from thoughts where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...) if err != nil { return fmt.Errorf("delete thought: %w", err) } @@ -367,7 +367,7 @@ func (db *DB) DeleteThought(ctx context.Context, id uuid.UUID) error { func (db *DB) ArchiveThought(ctx context.Context, id uuid.UUID) error { args := []any{id} - tag, err := db.pool.Exec(ctx, `update thoughts set archived_at = now(), updated_at = now() where guid = $1`+tenantSQL(ctx, &args, "tenant_key"), args...) + tag, err := db.pool.Exec(ctx, `update thoughts set archived_at = now(), updated_at = now() where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...) if err != nil { return fmt.Errorf("archive thought: %w", err) } @@ -446,7 +446,7 @@ func (db *DB) SearchSimilarThoughts(ctx context.Context, embedding []float32, em "1 - (e.embedding <=> $1) > $2", "e.model = $3", } - addTenantCondition(ctx, &args, &conditions, "t.tenant_key") + addTenantCondition(ctx, &args, &conditions, "t.tenant_id") if projectID != nil { args = append(args, *projectID) conditions = append(conditions, fmt.Sprintf("t.project_id = $%d", len(args))) @@ -495,7 +495,7 @@ func (db *DB) HasEmbeddingsForModel(ctx context.Context, model string, projectID "e.model = $1", "t.archived_at is null", } - addTenantCondition(ctx, &args, &conditions, "t.tenant_key") + addTenantCondition(ctx, &args, &conditions, "t.tenant_id") if projectID != nil { args = append(args, *projectID) conditions = append(conditions, fmt.Sprintf("t.project_id = $%d", len(args))) @@ -514,7 +514,7 @@ func (db *DB) HasEmbeddingsForModel(ctx context.Context, model string, projectID func (db *DB) ListThoughtsMissingEmbedding(ctx context.Context, model string, limit int, projectID *int64, includeArchived bool, olderThanDays int) ([]thoughttypes.Thought, error) { args := []any{model} conditions := []string{"e.id is null"} - addTenantCondition(ctx, &args, &conditions, "t.tenant_key") + addTenantCondition(ctx, &args, &conditions, "t.tenant_id") if !includeArchived { conditions = append(conditions, "t.archived_at is null") @@ -564,7 +564,7 @@ func (db *DB) ListThoughtsMissingEmbedding(ctx context.Context, model string, li func (db *DB) ListThoughtsForMetadataReparse(ctx context.Context, limit int, projectID *int64, includeArchived bool, olderThanDays int) ([]thoughttypes.Thought, error) { args := make([]any, 0, 3) conditions := make([]string, 0, 4) - addTenantCondition(ctx, &args, &conditions, "tenant_key") + addTenantCondition(ctx, &args, &conditions, "tenant_id") if !includeArchived { conditions = append(conditions, "archived_at is null") @@ -634,7 +634,7 @@ func (db *DB) SearchThoughtsText(ctx context.Context, query string, limit int, p "t.archived_at is null", "(to_tsvector('simple', t.content) || to_tsvector('simple', coalesce(p.name, ''))) @@ websearch_to_tsquery('simple', $1)", } - addTenantCondition(ctx, &args, &conditions, "t.tenant_key") + addTenantCondition(ctx, &args, &conditions, "t.tenant_id") if projectID != nil { args = append(args, *projectID) conditions = append(conditions, fmt.Sprintf("t.project_id = $%d", len(args))) diff --git a/migrations/020_generated_schema.sql b/migrations/020_generated_schema.sql index ca2084d..4999ac2 100644 --- a/migrations/020_generated_schema.sql +++ b/migrations/020_generated_schema.sql @@ -235,6 +235,7 @@ CREATE TABLE IF NOT EXISTS public.agent_personas ( name text NOT NULL, summary text NOT NULL, tags text[] NOT NULL DEFAULT '{}', + tenant_id text, updated_at timestamptz NOT NULL DEFAULT now() ); @@ -248,6 +249,7 @@ CREATE TABLE IF NOT EXISTS public.agent_parts ( part_type text NOT NULL, summary text NOT NULL, tags text[] NOT NULL DEFAULT '{}', + tenant_id text, updated_at timestamptz NOT NULL DEFAULT now() ); @@ -288,6 +290,7 @@ CREATE TABLE IF NOT EXISTS public.agent_traits ( instruction text NOT NULL DEFAULT '', name text NOT NULL, tags text[] NOT NULL DEFAULT '{}', + tenant_id text, trait_type text NOT NULL, updated_at timestamptz NOT NULL DEFAULT now() ); @@ -304,6 +307,7 @@ CREATE TABLE IF NOT EXISTS public.character_arcs ( id bigserial NOT NULL, name text NOT NULL, summary text NOT NULL DEFAULT '', + tenant_id text, updated_at timestamptz NOT NULL DEFAULT now() ); @@ -339,7 +343,7 @@ CREATE TABLE IF NOT EXISTS public.thoughts ( id bigserial NOT NULL, metadata jsonb DEFAULT '{}'::jsonb, project_id bigint, - tenant_key text, + tenant_id text, updated_at timestamptz DEFAULT now() ); @@ -350,7 +354,7 @@ CREATE TABLE IF NOT EXISTS public.projects ( id bigserial NOT NULL, last_active_at timestamptz DEFAULT now(), name text NOT NULL, - tenant_key text + tenant_id text ); CREATE TABLE IF NOT EXISTS public.thought_links ( @@ -392,11 +396,40 @@ CREATE TABLE IF NOT EXISTS public.stored_files ( project_id bigint, sha256 text NOT NULL, size_bytes bigint NOT NULL, - tenant_key text, + tenant_id text, thought_id bigint, updated_at timestamptz NOT NULL DEFAULT now() ); +CREATE TABLE IF NOT EXISTS public.tenants ( + created_at timestamptz NOT NULL DEFAULT now(), + id text NOT NULL, + name text NOT NULL +); + +CREATE TABLE IF NOT EXISTS public.tenant_users ( + created_at timestamptz NOT NULL DEFAULT now(), + email text, + id text NOT NULL, + name text NOT NULL, + tenant_id text NOT NULL +); + +CREATE TABLE IF NOT EXISTS public.api_key_assignments ( + created_at timestamptz NOT NULL DEFAULT now(), + description text NOT NULL DEFAULT '', + enabled boolean NOT NULL DEFAULT true, + key_id text NOT NULL, + source text NOT NULL, + tenant_id text NOT NULL, + user_id text +); + +CREATE TABLE IF NOT EXISTS public.managed_api_keys ( + key_id text NOT NULL, + secret_hash text NOT NULL +); + CREATE TABLE IF NOT EXISTS public.chat_histories ( agent_id text, channel text, @@ -408,7 +441,7 @@ CREATE TABLE IF NOT EXISTS public.chat_histories ( project_id bigint, session_id text NOT NULL, summary text, - tenant_key text, + tenant_id text, title text, updated_at timestamptz NOT NULL DEFAULT now() ); @@ -443,7 +476,7 @@ CREATE TABLE IF NOT EXISTS public.learnings ( summary text NOT NULL, supersedes_learning_id bigint, tags text[] NOT NULL DEFAULT '{}', - tenant_key text, + tenant_id text, updated_at timestamptz NOT NULL DEFAULT now() ); @@ -470,7 +503,7 @@ CREATE TABLE IF NOT EXISTS public.plans ( status text NOT NULL DEFAULT 'draft', supersedes_plan_id bigint, tags text[] NOT NULL DEFAULT '{}', - tenant_key text, + tenant_id text, title text NOT NULL, updated_at timestamptz NOT NULL DEFAULT now() ); @@ -515,6 +548,7 @@ CREATE TABLE IF NOT EXISTS public.agent_skills ( library_tags text[] NOT NULL DEFAULT '{}', name text NOT NULL, tags text[] NOT NULL DEFAULT '{}', + tenant_id text, updated_at timestamptz NOT NULL DEFAULT now() ); @@ -527,6 +561,7 @@ CREATE TABLE IF NOT EXISTS public.agent_guardrails ( name text NOT NULL, severity text NOT NULL DEFAULT 'medium', tags text[] NOT NULL DEFAULT '{}', + tenant_id text, updated_at timestamptz NOT NULL DEFAULT now() ); @@ -689,6 +724,19 @@ BEGIN END; $$; +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'agent_personas' + AND column_name = 'tenant_id' + ) THEN + ALTER TABLE public.agent_personas ADD COLUMN tenant_id text; + END IF; +END; +$$; + DO $$ BEGIN IF NOT EXISTS ( @@ -819,6 +867,19 @@ BEGIN END; $$; +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'agent_parts' + AND column_name = 'tenant_id' + ) THEN + ALTER TABLE public.agent_parts ADD COLUMN tenant_id text; + END IF; +END; +$$; + DO $$ BEGIN IF NOT EXISTS ( @@ -1144,6 +1205,19 @@ BEGIN END; $$; +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'agent_traits' + AND column_name = 'tenant_id' + ) THEN + ALTER TABLE public.agent_traits ADD COLUMN tenant_id text; + END IF; +END; +$$; + DO $$ BEGIN IF NOT EXISTS ( @@ -1274,6 +1348,19 @@ BEGIN END; $$; +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'character_arcs' + AND column_name = 'tenant_id' + ) THEN + ALTER TABLE public.character_arcs ADD COLUMN tenant_id text; + END IF; +END; +$$; + DO $$ BEGIN IF NOT EXISTS ( @@ -1579,9 +1666,9 @@ BEGIN SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'thoughts' - AND column_name = 'tenant_key' + AND column_name = 'tenant_id' ) THEN - ALTER TABLE public.thoughts ADD COLUMN tenant_key text; + ALTER TABLE public.thoughts ADD COLUMN tenant_id text; END IF; END; $$; @@ -1683,9 +1770,9 @@ BEGIN SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'projects' - AND column_name = 'tenant_key' + AND column_name = 'tenant_id' ) THEN - ALTER TABLE public.projects ADD COLUMN tenant_key text; + ALTER TABLE public.projects ADD COLUMN tenant_id text; END IF; END; $$; @@ -2073,9 +2160,9 @@ BEGIN SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'stored_files' - AND column_name = 'tenant_key' + AND column_name = 'tenant_id' ) THEN - ALTER TABLE public.stored_files ADD COLUMN tenant_key text; + ALTER TABLE public.stored_files ADD COLUMN tenant_id text; END IF; END; $$; @@ -2106,6 +2193,227 @@ BEGIN END; $$; +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'tenants' + AND column_name = 'created_at' + ) THEN + ALTER TABLE public.tenants ADD COLUMN created_at timestamptz NOT NULL DEFAULT now(); + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'tenants' + AND column_name = 'id' + ) THEN + ALTER TABLE public.tenants ADD COLUMN id text NOT NULL; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'tenants' + AND column_name = 'name' + ) THEN + ALTER TABLE public.tenants ADD COLUMN name text NOT NULL; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'tenant_users' + AND column_name = 'created_at' + ) THEN + ALTER TABLE public.tenant_users ADD COLUMN created_at timestamptz NOT NULL DEFAULT now(); + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'tenant_users' + AND column_name = 'email' + ) THEN + ALTER TABLE public.tenant_users ADD COLUMN email text; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'tenant_users' + AND column_name = 'id' + ) THEN + ALTER TABLE public.tenant_users ADD COLUMN id text NOT NULL; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'tenant_users' + AND column_name = 'name' + ) THEN + ALTER TABLE public.tenant_users ADD COLUMN name text NOT NULL; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'tenant_users' + AND column_name = 'tenant_id' + ) THEN + ALTER TABLE public.tenant_users ADD COLUMN tenant_id text NOT NULL; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'api_key_assignments' + AND column_name = 'created_at' + ) THEN + ALTER TABLE public.api_key_assignments ADD COLUMN created_at timestamptz NOT NULL DEFAULT now(); + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'api_key_assignments' + AND column_name = 'description' + ) THEN + ALTER TABLE public.api_key_assignments ADD COLUMN description text NOT NULL DEFAULT ''; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'api_key_assignments' + AND column_name = 'enabled' + ) THEN + ALTER TABLE public.api_key_assignments ADD COLUMN enabled boolean NOT NULL DEFAULT true; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'api_key_assignments' + AND column_name = 'key_id' + ) THEN + ALTER TABLE public.api_key_assignments ADD COLUMN key_id text NOT NULL; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'api_key_assignments' + AND column_name = 'source' + ) THEN + ALTER TABLE public.api_key_assignments ADD COLUMN source text NOT NULL; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'api_key_assignments' + AND column_name = 'tenant_id' + ) THEN + ALTER TABLE public.api_key_assignments ADD COLUMN tenant_id text NOT NULL; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'api_key_assignments' + AND column_name = 'user_id' + ) THEN + ALTER TABLE public.api_key_assignments ADD COLUMN user_id text; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'managed_api_keys' + AND column_name = 'key_id' + ) THEN + ALTER TABLE public.managed_api_keys ADD COLUMN key_id text NOT NULL; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'managed_api_keys' + AND column_name = 'secret_hash' + ) THEN + ALTER TABLE public.managed_api_keys ADD COLUMN secret_hash text NOT NULL; + END IF; +END; +$$; + DO $$ BEGIN IF NOT EXISTS ( @@ -2242,9 +2550,9 @@ BEGIN SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'chat_histories' - AND column_name = 'tenant_key' + AND column_name = 'tenant_id' ) THEN - ALTER TABLE public.chat_histories ADD COLUMN tenant_key text; + ALTER TABLE public.chat_histories ADD COLUMN tenant_id text; END IF; END; $$; @@ -2619,9 +2927,9 @@ BEGIN SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'learnings' - AND column_name = 'tenant_key' + AND column_name = 'tenant_id' ) THEN - ALTER TABLE public.learnings ADD COLUMN tenant_key text; + ALTER TABLE public.learnings ADD COLUMN tenant_id text; END IF; END; $$; @@ -2892,9 +3200,9 @@ BEGIN SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'plans' - AND column_name = 'tenant_key' + AND column_name = 'tenant_id' ) THEN - ALTER TABLE public.plans ADD COLUMN tenant_key text; + ALTER TABLE public.plans ADD COLUMN tenant_id text; END IF; END; $$; @@ -3276,6 +3584,19 @@ BEGIN END; $$; +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'agent_skills' + AND column_name = 'tenant_id' + ) THEN + ALTER TABLE public.agent_skills ADD COLUMN tenant_id text; + END IF; +END; +$$; + DO $$ BEGIN IF NOT EXISTS ( @@ -3393,6 +3714,19 @@ BEGIN END; $$; +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'agent_guardrails' + AND column_name = 'tenant_id' + ) THEN + ALTER TABLE public.agent_guardrails ADD COLUMN tenant_id text; + END IF; +END; +$$; + DO $$ BEGIN IF NOT EXISTS ( @@ -3777,6 +4111,29 @@ BEGIN END; $$; +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'agent_personas' + AND a.attname = 'tenant_id' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.agent_personas + ALTER COLUMN tenant_id TYPE text USING tenant_id::text; + END IF; +END; +$$; + DO $$ DECLARE current_type text; @@ -4007,6 +4364,29 @@ BEGIN END; $$; +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'agent_parts' + AND a.attname = 'tenant_id' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.agent_parts + ALTER COLUMN tenant_id TYPE text USING tenant_id::text; + END IF; +END; +$$; + DO $$ DECLARE current_type text; @@ -4582,6 +4962,29 @@ BEGIN END; $$; +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'agent_traits' + AND a.attname = 'tenant_id' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.agent_traits + ALTER COLUMN tenant_id TYPE text USING tenant_id::text; + END IF; +END; +$$; + DO $$ DECLARE current_type text; @@ -4812,6 +5215,29 @@ BEGIN END; $$; +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'character_arcs' + AND a.attname = 'tenant_id' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.character_arcs + ALTER COLUMN tenant_id TYPE text USING tenant_id::text; + END IF; +END; +$$; + DO $$ DECLARE current_type text; @@ -5352,14 +5778,14 @@ BEGIN JOIN pg_namespace n ON n.oid = t.relnamespace WHERE n.nspname = 'public' AND t.relname = 'thoughts' - AND a.attname = 'tenant_key' + AND a.attname = 'tenant_id' AND a.attnum > 0 AND NOT a.attisdropped; IF current_type IS NOT NULL AND current_type <> ALL(ARRAY['text']) THEN ALTER TABLE public.thoughts - ALTER COLUMN tenant_key TYPE text USING tenant_key::text; + ALTER COLUMN tenant_id TYPE text USING tenant_id::text; END IF; END; $$; @@ -5536,14 +5962,14 @@ BEGIN JOIN pg_namespace n ON n.oid = t.relnamespace WHERE n.nspname = 'public' AND t.relname = 'projects' - AND a.attname = 'tenant_key' + AND a.attname = 'tenant_id' AND a.attnum > 0 AND NOT a.attisdropped; IF current_type IS NOT NULL AND current_type <> ALL(ARRAY['text']) THEN ALTER TABLE public.projects - ALTER COLUMN tenant_key TYPE text USING tenant_key::text; + ALTER COLUMN tenant_id TYPE text USING tenant_id::text; END IF; END; $$; @@ -6226,14 +6652,14 @@ BEGIN JOIN pg_namespace n ON n.oid = t.relnamespace WHERE n.nspname = 'public' AND t.relname = 'stored_files' - AND a.attname = 'tenant_key' + AND a.attname = 'tenant_id' AND a.attnum > 0 AND NOT a.attisdropped; IF current_type IS NOT NULL AND current_type <> ALL(ARRAY['text']) THEN ALTER TABLE public.stored_files - ALTER COLUMN tenant_key TYPE text USING tenant_key::text; + ALTER COLUMN tenant_id TYPE text USING tenant_id::text; END IF; END; $$; @@ -6284,6 +6710,397 @@ BEGIN END; $$; +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'tenants' + AND a.attname = 'created_at' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['timestamptz', 'timestamp with time zone']) THEN + ALTER TABLE public.tenants + ALTER COLUMN created_at TYPE timestamptz USING created_at::timestamptz; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'tenants' + AND a.attname = 'id' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.tenants + ALTER COLUMN id TYPE text USING id::text; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'tenants' + AND a.attname = 'name' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.tenants + ALTER COLUMN name TYPE text USING name::text; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'tenant_users' + AND a.attname = 'created_at' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['timestamptz', 'timestamp with time zone']) THEN + ALTER TABLE public.tenant_users + ALTER COLUMN created_at TYPE timestamptz USING created_at::timestamptz; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'tenant_users' + AND a.attname = 'email' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.tenant_users + ALTER COLUMN email TYPE text USING email::text; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'tenant_users' + AND a.attname = 'id' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.tenant_users + ALTER COLUMN id TYPE text USING id::text; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'tenant_users' + AND a.attname = 'name' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.tenant_users + ALTER COLUMN name TYPE text USING name::text; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'tenant_users' + AND a.attname = 'tenant_id' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.tenant_users + ALTER COLUMN tenant_id TYPE text USING tenant_id::text; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'api_key_assignments' + AND a.attname = 'created_at' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['timestamptz', 'timestamp with time zone']) THEN + ALTER TABLE public.api_key_assignments + ALTER COLUMN created_at TYPE timestamptz USING created_at::timestamptz; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'api_key_assignments' + AND a.attname = 'description' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.api_key_assignments + ALTER COLUMN description TYPE text USING description::text; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'api_key_assignments' + AND a.attname = 'enabled' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['boolean']) THEN + ALTER TABLE public.api_key_assignments + ALTER COLUMN enabled TYPE boolean USING enabled::boolean; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'api_key_assignments' + AND a.attname = 'key_id' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.api_key_assignments + ALTER COLUMN key_id TYPE text USING key_id::text; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'api_key_assignments' + AND a.attname = 'source' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.api_key_assignments + ALTER COLUMN source TYPE text USING source::text; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'api_key_assignments' + AND a.attname = 'tenant_id' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.api_key_assignments + ALTER COLUMN tenant_id TYPE text USING tenant_id::text; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'api_key_assignments' + AND a.attname = 'user_id' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.api_key_assignments + ALTER COLUMN user_id TYPE text USING user_id::text; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'managed_api_keys' + AND a.attname = 'key_id' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.managed_api_keys + ALTER COLUMN key_id TYPE text USING key_id::text; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'managed_api_keys' + AND a.attname = 'secret_hash' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.managed_api_keys + ALTER COLUMN secret_hash TYPE text USING secret_hash::text; + END IF; +END; +$$; + DO $$ DECLARE current_type text; @@ -6525,14 +7342,14 @@ BEGIN JOIN pg_namespace n ON n.oid = t.relnamespace WHERE n.nspname = 'public' AND t.relname = 'chat_histories' - AND a.attname = 'tenant_key' + AND a.attname = 'tenant_id' AND a.attnum > 0 AND NOT a.attisdropped; IF current_type IS NOT NULL AND current_type <> ALL(ARRAY['text']) THEN ALTER TABLE public.chat_histories - ALTER COLUMN tenant_key TYPE text USING tenant_key::text; + ALTER COLUMN tenant_id TYPE text USING tenant_id::text; END IF; END; $$; @@ -7192,14 +8009,14 @@ BEGIN JOIN pg_namespace n ON n.oid = t.relnamespace WHERE n.nspname = 'public' AND t.relname = 'learnings' - AND a.attname = 'tenant_key' + AND a.attname = 'tenant_id' AND a.attnum > 0 AND NOT a.attisdropped; IF current_type IS NOT NULL AND current_type <> ALL(ARRAY['text']) THEN ALTER TABLE public.learnings - ALTER COLUMN tenant_key TYPE text USING tenant_key::text; + ALTER COLUMN tenant_id TYPE text USING tenant_id::text; END IF; END; $$; @@ -7675,14 +8492,14 @@ BEGIN JOIN pg_namespace n ON n.oid = t.relnamespace WHERE n.nspname = 'public' AND t.relname = 'plans' - AND a.attname = 'tenant_key' + AND a.attname = 'tenant_id' AND a.attnum > 0 AND NOT a.attisdropped; IF current_type IS NOT NULL AND current_type <> ALL(ARRAY['text']) THEN ALTER TABLE public.plans - ALTER COLUMN tenant_key TYPE text USING tenant_key::text; + ALTER COLUMN tenant_id TYPE text USING tenant_id::text; END IF; END; $$; @@ -8354,6 +9171,29 @@ BEGIN END; $$; +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'agent_skills' + AND a.attname = 'tenant_id' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.agent_skills + ALTER COLUMN tenant_id TYPE text USING tenant_id::text; + END IF; +END; +$$; + DO $$ DECLARE current_type text; @@ -8561,6 +9401,29 @@ BEGIN END; $$; +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'agent_guardrails' + AND a.attname = 'tenant_id' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['text']) THEN + ALTER TABLE public.agent_guardrails + ALTER COLUMN tenant_id TYPE text USING tenant_id::text; + END IF; +END; +$$; + DO $$ DECLARE current_type text; @@ -9299,7 +10162,7 @@ BEGIN ORDER BY cols.ord ), ARRAY[]::text[] - ) = ARRAY['id', 'persona_id'] + ) = ARRAY['id'] INTO current_pk_name, current_pk_matches FROM information_schema.table_constraints tc WHERE tc.table_schema = 'public' @@ -9315,7 +10178,7 @@ BEGIN -- Add the desired primary key only when no matching primary key already exists. IF current_pk_name IS NULL OR (NOT current_pk_matches AND current_pk_name IN ('persona_arc_pkey', 'public_persona_arc_pkey')) THEN - ALTER TABLE public.persona_arc ADD CONSTRAINT pk_public_persona_arc PRIMARY KEY (id, persona_id); + ALTER TABLE public.persona_arc ADD CONSTRAINT pk_public_persona_arc PRIMARY KEY (id); END IF; END; $$; @@ -9584,6 +10447,182 @@ BEGIN END; $$; +DO $$ +DECLARE + current_pk_name text; + current_pk_matches boolean := false; +BEGIN + SELECT tc.constraint_name, + COALESCE( + ARRAY( + SELECT a.attname::text + FROM pg_constraint c + JOIN pg_class t ON t.oid = c.conrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + JOIN unnest(c.conkey) WITH ORDINALITY AS cols(attnum, ord) + ON TRUE + JOIN pg_attribute a + ON a.attrelid = t.oid + AND a.attnum = cols.attnum + WHERE c.contype = 'p' + AND n.nspname = 'public' + AND t.relname = 'tenants' + ORDER BY cols.ord + ), + ARRAY[]::text[] + ) = ARRAY['id'] + INTO current_pk_name, current_pk_matches + FROM information_schema.table_constraints tc + WHERE tc.table_schema = 'public' + AND tc.table_name = 'tenants' + AND tc.constraint_type = 'PRIMARY KEY'; + + IF current_pk_name IS NOT NULL + AND NOT current_pk_matches + AND current_pk_name IN ('tenants_pkey', 'public_tenants_pkey') THEN + EXECUTE 'ALTER TABLE public.tenants DROP CONSTRAINT ' || quote_ident(current_pk_name) || ' CASCADE'; + END IF; + + -- Add the desired primary key only when no matching primary key already exists. + IF current_pk_name IS NULL + OR (NOT current_pk_matches AND current_pk_name IN ('tenants_pkey', 'public_tenants_pkey')) THEN + ALTER TABLE public.tenants ADD CONSTRAINT pk_public_tenants PRIMARY KEY (id); + END IF; +END; +$$; + +DO $$ +DECLARE + current_pk_name text; + current_pk_matches boolean := false; +BEGIN + SELECT tc.constraint_name, + COALESCE( + ARRAY( + SELECT a.attname::text + FROM pg_constraint c + JOIN pg_class t ON t.oid = c.conrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + JOIN unnest(c.conkey) WITH ORDINALITY AS cols(attnum, ord) + ON TRUE + JOIN pg_attribute a + ON a.attrelid = t.oid + AND a.attnum = cols.attnum + WHERE c.contype = 'p' + AND n.nspname = 'public' + AND t.relname = 'tenant_users' + ORDER BY cols.ord + ), + ARRAY[]::text[] + ) = ARRAY['id'] + INTO current_pk_name, current_pk_matches + FROM information_schema.table_constraints tc + WHERE tc.table_schema = 'public' + AND tc.table_name = 'tenant_users' + AND tc.constraint_type = 'PRIMARY KEY'; + + IF current_pk_name IS NOT NULL + AND NOT current_pk_matches + AND current_pk_name IN ('tenant_users_pkey', 'public_tenant_users_pkey') THEN + EXECUTE 'ALTER TABLE public.tenant_users DROP CONSTRAINT ' || quote_ident(current_pk_name) || ' CASCADE'; + END IF; + + -- Add the desired primary key only when no matching primary key already exists. + IF current_pk_name IS NULL + OR (NOT current_pk_matches AND current_pk_name IN ('tenant_users_pkey', 'public_tenant_users_pkey')) THEN + ALTER TABLE public.tenant_users ADD CONSTRAINT pk_public_tenant_users PRIMARY KEY (id); + END IF; +END; +$$; + +DO $$ +DECLARE + current_pk_name text; + current_pk_matches boolean := false; +BEGIN + SELECT tc.constraint_name, + COALESCE( + ARRAY( + SELECT a.attname::text + FROM pg_constraint c + JOIN pg_class t ON t.oid = c.conrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + JOIN unnest(c.conkey) WITH ORDINALITY AS cols(attnum, ord) + ON TRUE + JOIN pg_attribute a + ON a.attrelid = t.oid + AND a.attnum = cols.attnum + WHERE c.contype = 'p' + AND n.nspname = 'public' + AND t.relname = 'api_key_assignments' + ORDER BY cols.ord + ), + ARRAY[]::text[] + ) = ARRAY['key_id'] + INTO current_pk_name, current_pk_matches + FROM information_schema.table_constraints tc + WHERE tc.table_schema = 'public' + AND tc.table_name = 'api_key_assignments' + AND tc.constraint_type = 'PRIMARY KEY'; + + IF current_pk_name IS NOT NULL + AND NOT current_pk_matches + AND current_pk_name IN ('api_key_assignments_pkey', 'public_api_key_assignments_pkey') THEN + EXECUTE 'ALTER TABLE public.api_key_assignments DROP CONSTRAINT ' || quote_ident(current_pk_name) || ' CASCADE'; + END IF; + + -- Add the desired primary key only when no matching primary key already exists. + IF current_pk_name IS NULL + OR (NOT current_pk_matches AND current_pk_name IN ('api_key_assignments_pkey', 'public_api_key_assignments_pkey')) THEN + ALTER TABLE public.api_key_assignments ADD CONSTRAINT pk_public_api_key_assignments PRIMARY KEY (key_id); + END IF; +END; +$$; + +DO $$ +DECLARE + current_pk_name text; + current_pk_matches boolean := false; +BEGIN + SELECT tc.constraint_name, + COALESCE( + ARRAY( + SELECT a.attname::text + FROM pg_constraint c + JOIN pg_class t ON t.oid = c.conrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + JOIN unnest(c.conkey) WITH ORDINALITY AS cols(attnum, ord) + ON TRUE + JOIN pg_attribute a + ON a.attrelid = t.oid + AND a.attnum = cols.attnum + WHERE c.contype = 'p' + AND n.nspname = 'public' + AND t.relname = 'managed_api_keys' + ORDER BY cols.ord + ), + ARRAY[]::text[] + ) = ARRAY['key_id'] + INTO current_pk_name, current_pk_matches + FROM information_schema.table_constraints tc + WHERE tc.table_schema = 'public' + AND tc.table_name = 'managed_api_keys' + AND tc.constraint_type = 'PRIMARY KEY'; + + IF current_pk_name IS NOT NULL + AND NOT current_pk_matches + AND current_pk_name IN ('managed_api_keys_pkey', 'public_managed_api_keys_pkey') THEN + EXECUTE 'ALTER TABLE public.managed_api_keys DROP CONSTRAINT ' || quote_ident(current_pk_name) || ' CASCADE'; + END IF; + + -- Add the desired primary key only when no matching primary key already exists. + IF current_pk_name IS NULL + OR (NOT current_pk_matches AND current_pk_name IN ('managed_api_keys_pkey', 'public_managed_api_keys_pkey')) THEN + ALTER TABLE public.managed_api_keys ADD CONSTRAINT pk_public_managed_api_keys PRIMARY KEY (key_id); + END IF; +END; +$$; + DO $$ DECLARE current_pk_name text; @@ -10169,11 +11208,11 @@ CREATE INDEX IF NOT EXISTS idx_project_personas_project_id_persona_id CREATE INDEX IF NOT EXISTS idx_arc_stage_parts_stage_id_part_id ON public.arc_stage_parts USING btree (stage_id, part_id); -CREATE INDEX IF NOT EXISTS idx_thoughts_tenant_key_project_id - ON public.thoughts USING btree (tenant_key, project_id); +CREATE INDEX IF NOT EXISTS idx_thoughts_tenant_id_project_id + ON public.thoughts USING btree (tenant_id, project_id); -CREATE UNIQUE INDEX IF NOT EXISTS uidx_projects_tenant_key_name - ON public.projects USING btree (tenant_key, name); +CREATE UNIQUE INDEX IF NOT EXISTS uidx_projects_tenant_id_name + ON public.projects USING btree (tenant_id, name); CREATE INDEX IF NOT EXISTS idx_thought_links_from_id_to_id_relation ON public.thought_links USING btree (from_id, to_id, relation); @@ -10184,6 +11223,9 @@ CREATE UNIQUE INDEX IF NOT EXISTS uidx_thought_learning_links_thought_id_learnin CREATE UNIQUE INDEX IF NOT EXISTS uidx_embeddings_thought_id_model ON public.embeddings USING btree (thought_id, model); +CREATE UNIQUE INDEX IF NOT EXISTS uidx_tenant_users_tenant_id_email + ON public.tenant_users USING btree (tenant_id, email); + CREATE INDEX IF NOT EXISTS idx_learnings_details ON public.learnings USING gin (details gin_trgm_ops); @@ -10231,19 +11273,6 @@ BEGIN END; $$; -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM information_schema.table_constraints - WHERE table_schema = 'public' - AND table_name = 'agent_personas' - AND constraint_name = 'ukey_agent_personas_name' - ) THEN - ALTER TABLE public.agent_personas ADD CONSTRAINT ukey_agent_personas_name UNIQUE (name); - END IF; -END; -$$; - DO $$ BEGIN IF NOT EXISTS ( @@ -10257,19 +11286,6 @@ BEGIN END; $$; -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM information_schema.table_constraints - WHERE table_schema = 'public' - AND table_name = 'agent_parts' - AND constraint_name = 'ukey_agent_parts_name' - ) THEN - ALTER TABLE public.agent_parts ADD CONSTRAINT ukey_agent_parts_name UNIQUE (name); - END IF; -END; -$$; - DO $$ BEGIN IF NOT EXISTS ( @@ -10283,32 +11299,6 @@ BEGIN END; $$; -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM information_schema.table_constraints - WHERE table_schema = 'public' - AND table_name = 'agent_traits' - AND constraint_name = 'ukey_agent_traits_name' - ) THEN - ALTER TABLE public.agent_traits ADD CONSTRAINT ukey_agent_traits_name UNIQUE (name); - END IF; -END; -$$; - -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM information_schema.table_constraints - WHERE table_schema = 'public' - AND table_name = 'character_arcs' - AND constraint_name = 'ukey_character_arcs_name' - ) THEN - ALTER TABLE public.character_arcs ADD CONSTRAINT ukey_character_arcs_name UNIQUE (name); - END IF; -END; -$$; - DO $$ BEGIN IF NOT EXISTS ( @@ -10361,6 +11351,19 @@ BEGIN END; $$; +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'tenants' + AND constraint_name = 'ukey_tenants_name' + ) THEN + ALTER TABLE public.tenants ADD CONSTRAINT ukey_tenants_name UNIQUE (name); + END IF; +END; +$$; + DO $$ BEGIN IF NOT EXISTS ( @@ -10439,19 +11442,6 @@ BEGIN END; $$; -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM information_schema.table_constraints - WHERE table_schema = 'public' - AND table_name = 'agent_skills' - AND constraint_name = 'ukey_agent_skills_name' - ) THEN - ALTER TABLE public.agent_skills ADD CONSTRAINT ukey_agent_skills_name UNIQUE (name); - END IF; -END; -$$; - DO $$ BEGIN IF NOT EXISTS ( @@ -10465,22 +11455,41 @@ BEGIN END; $$; +-- Check constraints for schema: public +-- Foreign keys for schema: public DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.table_constraints WHERE table_schema = 'public' - AND table_name = 'agent_guardrails' - AND constraint_name = 'ukey_agent_guardrails_name' + AND table_name = 'agent_personas' + AND constraint_name = 'fk_agent_personas_tenant_id' ) THEN - ALTER TABLE public.agent_guardrails ADD CONSTRAINT ukey_agent_guardrails_name UNIQUE (name); + ALTER TABLE public.agent_personas + ADD CONSTRAINT fk_agent_personas_tenant_id + FOREIGN KEY (tenant_id) + REFERENCES public.tenants (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; END IF; END; -$$; - --- Check constraints for schema: public --- Foreign keys for schema: public -DO $$ +$$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'agent_parts' + AND constraint_name = 'fk_agent_parts_tenant_id' + ) THEN + ALTER TABLE public.agent_parts + ADD CONSTRAINT fk_agent_parts_tenant_id + FOREIGN KEY (tenant_id) + REFERENCES public.tenants (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.table_constraints @@ -10609,6 +11618,22 @@ BEGIN END IF; END; $$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'agent_traits' + AND constraint_name = 'fk_agent_traits_tenant_id' + ) THEN + ALTER TABLE public.agent_traits + ADD CONSTRAINT fk_agent_traits_tenant_id + FOREIGN KEY (tenant_id) + REFERENCES public.tenants (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.table_constraints @@ -10641,6 +11666,22 @@ BEGIN END IF; END; $$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'character_arcs' + AND constraint_name = 'fk_character_arcs_tenant_id' + ) THEN + ALTER TABLE public.character_arcs + ADD CONSTRAINT fk_character_arcs_tenant_id + FOREIGN KEY (tenant_id) + REFERENCES public.tenants (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.table_constraints @@ -10753,6 +11794,38 @@ BEGIN END IF; END; $$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'thoughts' + AND constraint_name = 'fk_thoughts_tenant_id' + ) THEN + ALTER TABLE public.thoughts + ADD CONSTRAINT fk_thoughts_tenant_id + FOREIGN KEY (tenant_id) + REFERENCES public.tenants (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'projects' + AND constraint_name = 'fk_projects_tenant_id' + ) THEN + ALTER TABLE public.projects + ADD CONSTRAINT fk_projects_tenant_id + FOREIGN KEY (tenant_id) + REFERENCES public.tenants (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.table_constraints @@ -10849,6 +11922,22 @@ BEGIN END IF; END; $$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'stored_files' + AND constraint_name = 'fk_stored_files_tenant_id' + ) THEN + ALTER TABLE public.stored_files + ADD CONSTRAINT fk_stored_files_tenant_id + FOREIGN KEY (tenant_id) + REFERENCES public.tenants (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.table_constraints @@ -10865,6 +11954,70 @@ BEGIN END IF; END; $$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'tenant_users' + AND constraint_name = 'fk_tenant_users_tenant_id' + ) THEN + ALTER TABLE public.tenant_users + ADD CONSTRAINT fk_tenant_users_tenant_id + FOREIGN KEY (tenant_id) + REFERENCES public.tenants (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'api_key_assignments' + AND constraint_name = 'fk_api_key_assignments_tenant_id' + ) THEN + ALTER TABLE public.api_key_assignments + ADD CONSTRAINT fk_api_key_assignments_tenant_id + FOREIGN KEY (tenant_id) + REFERENCES public.tenants (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'api_key_assignments' + AND constraint_name = 'fk_api_key_assignments_user_id' + ) THEN + ALTER TABLE public.api_key_assignments + ADD CONSTRAINT fk_api_key_assignments_user_id + FOREIGN KEY (user_id) + REFERENCES public.tenant_users (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'managed_api_keys' + AND constraint_name = 'fk_managed_api_keys_key_id' + ) THEN + ALTER TABLE public.managed_api_keys + ADD CONSTRAINT fk_managed_api_keys_key_id + FOREIGN KEY (key_id) + REFERENCES public.api_key_assignments (key_id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.table_constraints @@ -10881,6 +12034,22 @@ BEGIN END IF; END; $$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'chat_histories' + AND constraint_name = 'fk_chat_histories_tenant_id' + ) THEN + ALTER TABLE public.chat_histories + ADD CONSTRAINT fk_chat_histories_tenant_id + FOREIGN KEY (tenant_id) + REFERENCES public.tenants (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.table_constraints @@ -10961,6 +12130,22 @@ BEGIN END IF; END; $$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'learnings' + AND constraint_name = 'fk_learnings_tenant_id' + ) THEN + ALTER TABLE public.learnings + ADD CONSTRAINT fk_learnings_tenant_id + FOREIGN KEY (tenant_id) + REFERENCES public.tenants (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.table_constraints @@ -10993,6 +12178,22 @@ BEGIN END IF; END; $$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'plans' + AND constraint_name = 'fk_plans_tenant_id' + ) THEN + ALTER TABLE public.plans + ADD CONSTRAINT fk_plans_tenant_id + FOREIGN KEY (tenant_id) + REFERENCES public.tenants (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.table_constraints @@ -11121,6 +12322,38 @@ BEGIN END IF; END; $$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'agent_skills' + AND constraint_name = 'fk_agent_skills_tenant_id' + ) THEN + ALTER TABLE public.agent_skills + ADD CONSTRAINT fk_agent_skills_tenant_id + FOREIGN KEY (tenant_id) + REFERENCES public.tenants (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'agent_guardrails' + AND constraint_name = 'fk_agent_guardrails_tenant_id' + ) THEN + ALTER TABLE public.agent_guardrails + ADD CONSTRAINT fk_agent_guardrails_tenant_id + FOREIGN KEY (tenant_id) + REFERENCES public.tenants (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.table_constraints @@ -11801,6 +13034,10 @@ $$; + + + + diff --git a/migrations/120_identity_tenancy.sql b/migrations/120_identity_tenancy.sql new file mode 100644 index 0000000..53b3f93 --- /dev/null +++ b/migrations/120_identity_tenancy.sql @@ -0,0 +1,31 @@ +create table if not exists tenants ( + id text primary key, + name text not null unique, + created_at timestamptz not null default now() +); + +create table if not exists tenant_users ( + id text primary key, + tenant_id text not null references tenants(id), + name text not null, + email text, + created_at timestamptz not null default now(), + unique (tenant_id, email) +); + +create index if not exists tenant_users_tenant_id_idx on tenant_users (tenant_id); + +create table if not exists api_key_assignments ( + key_id text primary key, + tenant_id text not null references tenants(id), + user_id text references tenant_users(id), + description text not null default '', + source text not null check (source in ('configured', 'managed')), + enabled boolean not null default true, + created_at timestamptz not null default now() +); + +create table if not exists managed_api_keys ( + key_id text primary key references api_key_assignments(key_id) on delete cascade, + secret_hash text not null +); diff --git a/migrations/130_tenant_foreign_keys.sql b/migrations/130_tenant_foreign_keys.sql new file mode 100644 index 0000000..27acec3 --- /dev/null +++ b/migrations/130_tenant_foreign_keys.sql @@ -0,0 +1,53 @@ +-- Convert tenant_key from an opaque authentication value into a real tenant +-- relationship. Preserve legacy rows and create placeholders for historical +-- key IDs before adding foreign keys. +insert into tenants (id, name) +select tenant_key, tenant_key +from ( + select tenant_key from projects + union select tenant_key from thoughts + union select tenant_key from stored_files + union select tenant_key from learnings + union select tenant_key from plans + union select tenant_key from chat_histories +) tenant_keys +where tenant_key is not null and tenant_key <> '' +on conflict (id) do nothing; + +alter table agent_skills add column if not exists tenant_key text references tenants(id); +alter table agent_guardrails add column if not exists tenant_key text references tenants(id); +alter table agent_personas add column if not exists tenant_key text references tenants(id); +alter table agent_parts add column if not exists tenant_key text references tenants(id); +alter table agent_traits add column if not exists tenant_key text references tenants(id); +alter table character_arcs add column if not exists tenant_key text references tenants(id); + +alter table agent_skills drop constraint if exists ukey_agent_skills_name; +alter table agent_guardrails drop constraint if exists ukey_agent_guardrails_name; +alter table agent_personas drop constraint if exists ukey_agent_personas_name; +alter table agent_parts drop constraint if exists ukey_agent_parts_name; +alter table agent_traits drop constraint if exists ukey_agent_traits_name; +alter table character_arcs drop constraint if exists ukey_character_arcs_name; + +create unique index if not exists agent_skills_tenant_key_name_idx on agent_skills (coalesce(tenant_key, ''), name); +create unique index if not exists agent_guardrails_tenant_key_name_idx on agent_guardrails (coalesce(tenant_key, ''), name); +create unique index if not exists agent_personas_tenant_key_name_idx on agent_personas (coalesce(tenant_key, ''), name); +create unique index if not exists agent_parts_tenant_key_name_idx on agent_parts (coalesce(tenant_key, ''), name); +create unique index if not exists agent_traits_tenant_key_name_idx on agent_traits (coalesce(tenant_key, ''), name); +create unique index if not exists character_arcs_tenant_key_name_idx on character_arcs (coalesce(tenant_key, ''), name); + +create index if not exists agent_skills_tenant_key_idx on agent_skills (tenant_key); +create index if not exists agent_guardrails_tenant_key_idx on agent_guardrails (tenant_key); +create index if not exists agent_personas_tenant_key_idx on agent_personas (tenant_key); +create index if not exists agent_parts_tenant_key_idx on agent_parts (tenant_key); +create index if not exists agent_traits_tenant_key_idx on agent_traits (tenant_key); +create index if not exists character_arcs_tenant_key_idx on character_arcs (tenant_key); + +do $$ +declare table_name text; +begin + foreach table_name in array array['projects', 'thoughts', 'stored_files', 'learnings', 'plans', 'chat_histories'] + loop + execute format('alter table %I drop constraint if exists %I', table_name, table_name || '_tenant_key_fkey'); + execute format('alter table %I add constraint %I foreign key (tenant_key) references tenants(id)', table_name, table_name || '_tenant_key_fkey'); + end loop; +end $$; diff --git a/migrations/140_rename_tenant_key_to_tenant_id.sql b/migrations/140_rename_tenant_key_to_tenant_id.sql new file mode 100644 index 0000000..fb3d462 --- /dev/null +++ b/migrations/140_rename_tenant_key_to_tenant_id.sql @@ -0,0 +1,44 @@ +-- Tenant data is not in use yet, so replace the earlier opaque tenant_key +-- column name with the relational tenant_id convention everywhere. +do $$ +declare + tbl text; + con text; +begin + foreach tbl in array array[ + 'projects', 'thoughts', 'stored_files', 'learnings', 'plans', 'chat_histories', + 'agent_skills', 'agent_guardrails', 'agent_personas', 'agent_parts', + 'agent_traits', 'character_arcs' + ] loop + -- Fresh installs already have tenant_id from the regenerated schema; + -- discard that empty column so the historical migration chain converges. + if exists ( + select 1 from information_schema.columns + where table_schema = 'public' and table_name = tbl and column_name = 'tenant_id' + ) and exists ( + select 1 from information_schema.columns + where table_schema = 'public' and table_name = tbl and column_name = 'tenant_key' + ) then + execute format('alter table %I drop column tenant_id', tbl); + end if; + if exists ( + select 1 from information_schema.columns + where table_schema = 'public' and table_name = tbl and column_name = 'tenant_key' + ) then + execute format('alter table %I rename column tenant_key to tenant_id', tbl); + end if; + + for con in + select c.conname + from pg_constraint c + join pg_class r on r.oid = c.conrelid + join pg_namespace n on n.oid = r.relnamespace + join unnest(c.conkey) as k(attnum) on true + join pg_attribute a on a.attrelid = r.oid and a.attnum = k.attnum + where n.nspname = 'public' and r.relname = tbl and c.contype = 'f' and a.attname = 'tenant_id' + loop + execute format('alter table %I drop constraint %I', tbl, con); + end loop; + execute format('alter table %I add constraint %I foreign key (tenant_id) references tenants(id)', tbl, 'fk_' || tbl || '_tenant_id'); + end loop; +end $$; diff --git a/schema/agent_personas.dbml b/schema/agent_personas.dbml index 12f7d18..6b75a8d 100644 --- a/schema/agent_personas.dbml +++ b/schema/agent_personas.dbml @@ -1,7 +1,8 @@ Table agent_personas { id bigserial [pk] guid uuid [unique, not null, default: `gen_random_uuid()`] - name text [unique, not null] + name text [not null] + tenant_id text [ref: > tenants.id] description text [not null, default: ''] summary text [not null] detail text [not null, default: ''] @@ -11,12 +12,15 @@ Table agent_personas { tags "text[]" [not null, default: `'{}'`] created_at timestamptz [not null, default: `now()`] updated_at timestamptz [not null, default: `now()`] + + indexes { (tenant_id, name) [unique] tenant_id } } Table agent_parts { id bigserial [pk] guid uuid [unique, not null, default: `gen_random_uuid()`] - name text [unique, not null] + name text [not null] + tenant_id text [ref: > tenants.id] part_type text [not null] description text [not null, default: ''] summary text [not null] @@ -24,6 +28,8 @@ Table agent_parts { tags "text[]" [not null, default: `'{}'`] created_at timestamptz [not null, default: `now()`] updated_at timestamptz [not null, default: `now()`] + + indexes { (tenant_id, name) [unique] tenant_id } } Table agent_persona_parts { @@ -77,13 +83,16 @@ Table agent_persona_guardrails { Table agent_traits { id bigserial [pk] guid uuid [unique, not null, default: `gen_random_uuid()`] - name text [unique, not null] + name text [not null] + tenant_id text [ref: > tenants.id] trait_type text [not null] description text [not null, default: ''] instruction text [not null, default: ''] tags "text[]" [not null, default: `'{}'`] created_at timestamptz [not null, default: `now()`] updated_at timestamptz [not null, default: `now()`] + + indexes { (tenant_id, name) [unique] tenant_id } } Table agent_persona_traits { @@ -98,11 +107,14 @@ Table agent_persona_traits { Table character_arcs { id bigserial [pk] - name text [unique, not null] + name text [not null] + tenant_id text [ref: > tenants.id] description text [not null, default: ''] summary text [not null, default: ''] created_at timestamptz [not null, default: `now()`] updated_at timestamptz [not null, default: `now()`] + + indexes { (tenant_id, name) [unique] tenant_id } } Table arc_stages { @@ -127,7 +139,7 @@ Table arc_stage_parts { Table persona_arc { id bigserial [pk] - persona_id bigint [pk, ref: > agent_personas.id] + persona_id bigint [unique, not null, ref: > agent_personas.id] arc_id bigint [not null, ref: > character_arcs.id] current_stage_id bigint [not null, ref: > arc_stages.id] updated_at timestamptz [not null, default: `now()`] diff --git a/schema/core.dbml b/schema/core.dbml index 3006be7..f1983b1 100644 --- a/schema/core.dbml +++ b/schema/core.dbml @@ -6,12 +6,12 @@ Table thoughts { created_at timestamptz [default: `now()`] updated_at timestamptz [default: `now()`] project_id bigint [ref: > projects.id] - tenant_key text + tenant_id text [ref: > tenants.id] archived_at timestamptz indexes { - tenant_key - (tenant_key, project_id) + tenant_id + (tenant_id, project_id) } } @@ -20,13 +20,13 @@ Table projects { guid uuid [unique, not null, default: `gen_random_uuid()`] name text [not null] description text - tenant_key text + tenant_id text [ref: > tenants.id] created_at timestamptz [default: `now()`] last_active_at timestamptz [default: `now()`] indexes { - (tenant_key, name) [unique] - tenant_key + (tenant_id, name) [unique] + tenant_id } } diff --git a/schema/files.dbml b/schema/files.dbml index a4a8f8f..37b57ce 100644 --- a/schema/files.dbml +++ b/schema/files.dbml @@ -3,7 +3,7 @@ Table stored_files { guid uuid [unique, not null, default: `gen_random_uuid()`] thought_id bigint [ref: > thoughts.id] project_id bigint [ref: > projects.id] - tenant_key text + tenant_id text [ref: > tenants.id] name text [not null] media_type text [not null] kind text [not null, default: 'file'] @@ -17,7 +17,7 @@ Table stored_files { indexes { thought_id project_id - tenant_key + tenant_id sha256 } } diff --git a/schema/identity.dbml b/schema/identity.dbml new file mode 100644 index 0000000..b489e9b --- /dev/null +++ b/schema/identity.dbml @@ -0,0 +1,33 @@ +Table tenants { + id text [pk] + name text [not null, unique] + created_at timestamptz [not null, default: `now()`] +} + +Table tenant_users { + id text [pk] + tenant_id text [not null, ref: > tenants.id] + name text [not null] + email text + created_at timestamptz [not null, default: `now()`] + + Indexes { + tenant_id + (tenant_id, email) [unique] + } +} + +Table api_key_assignments { + key_id text [pk] + tenant_id text [not null, ref: > tenants.id] + user_id text [ref: > tenant_users.id] + description text [not null, default: ''] + source text [not null] + enabled boolean [not null, default: true] + created_at timestamptz [not null, default: `now()`] +} + +Table managed_api_keys { + key_id text [pk, ref: > api_key_assignments.key_id] + secret_hash text [not null] +} diff --git a/schema/meta.dbml b/schema/meta.dbml index 0c48b8e..a8409c2 100644 --- a/schema/meta.dbml +++ b/schema/meta.dbml @@ -6,7 +6,7 @@ Table chat_histories { channel text agent_id text project_id bigint [ref: > projects.id] - tenant_key text + tenant_id text [ref: > tenants.id] messages jsonb [not null, default: `'[]'`] summary text metadata jsonb [not null, default: `'{}'`] @@ -16,7 +16,7 @@ Table chat_histories { indexes { session_id project_id - tenant_key + tenant_id channel agent_id created_at @@ -48,7 +48,7 @@ Table learnings { source_type text source_ref text project_id bigint [ref: > projects.id] - tenant_key text + tenant_id text [ref: > tenants.id] related_thought_id bigint [ref: > thoughts.id] related_skill_id bigint [ref: > agent_skills.id] reviewed_by text @@ -61,7 +61,7 @@ Table learnings { indexes { project_id - tenant_key + tenant_id category area status diff --git a/schema/plans.dbml b/schema/plans.dbml index 03e5f06..3779bdf 100644 --- a/schema/plans.dbml +++ b/schema/plans.dbml @@ -6,7 +6,7 @@ Table plans { status text [not null, default: 'draft'] // draft, active, blocked, completed, cancelled, superseded priority text [not null, default: 'medium'] // low, medium, high, critical project_id bigint [ref: > projects.id] - tenant_key text + tenant_id text [ref: > tenants.id] owner text due_date timestamptz completed_at timestamptz @@ -19,7 +19,7 @@ Table plans { indexes { project_id - tenant_key + tenant_id status priority owner diff --git a/schema/skills.dbml b/schema/skills.dbml index 935b858..1fc8c1e 100644 --- a/schema/skills.dbml +++ b/schema/skills.dbml @@ -1,7 +1,8 @@ Table agent_skills { id bigserial [pk] guid uuid [unique, not null, default: `gen_random_uuid()`] - name text [unique, not null] + name text [not null] + tenant_id text [ref: > tenants.id] description text [not null, default: ''] content text [not null] tags "text[]" [not null, default: `'{}'`] @@ -11,18 +12,23 @@ Table agent_skills { domain_tags "text[]" [not null, default: `'{}'`] created_at timestamptz [not null, default: `now()`] updated_at timestamptz [not null, default: `now()`] + + indexes { (tenant_id, name) [unique] tenant_id } } Table agent_guardrails { id bigserial [pk] guid uuid [unique, not null, default: `gen_random_uuid()`] - name text [unique, not null] + name text [not null] + tenant_id text [ref: > tenants.id] description text [not null, default: ''] content text [not null] severity text [not null, default: 'medium'] tags "text[]" [not null, default: `'{}'`] created_at timestamptz [not null, default: `now()`] updated_at timestamptz [not null, default: `now()`] + + indexes { (tenant_id, name) [unique] tenant_id } } Table project_skills { diff --git a/ui/package.json b/ui/package.json index 46218e1..88bb921 100644 --- a/ui/package.json +++ b/ui/package.json @@ -11,22 +11,22 @@ "preview": "vite preview" }, "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^7.0.0", - "@tailwindcss/vite": "^4.2.4", - "@types/node": "^25.6.0", - "svelte": "^5.55.5", - "svelte-check": "^4.4.6", - "tailwindcss": "^4.2.4", + "@sveltejs/vite-plugin-svelte": "^7.2.0", + "@tailwindcss/vite": "^4.3.3", + "@types/node": "^26.1.1", + "svelte": "^5.56.6", + "svelte-check": "^4.7.3", + "tailwindcss": "^4.3.3", "typescript": "^6.0.3", - "vite": "^8.0.10" + "vite": "^8.1.5" }, "dependencies": { - "@sentry/svelte": "^10.51.0", + "@sentry/svelte": "^10.67.0", "@skeletonlabs/skeleton": "^4.15.2", "@skeletonlabs/skeleton-svelte": "^4.15.2", - "@tanstack/svelte-virtual": "^3.13.24", + "@tanstack/svelte-virtual": "^3.13.33", "@warkypublic/artemis-kit": "^1.0.10", "@warkypublic/resolvespec-js": "^1.0.1", - "@warkypublic/svelix": "^0.1.40" + "@warkypublic/svelix": "^0.2.5" } } \ No newline at end of file diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 63fdebf..ff7eb94 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -9,17 +9,17 @@ importers: .: dependencies: '@sentry/svelte': - specifier: ^10.51.0 - version: 10.51.0(svelte@5.55.5) + specifier: ^10.67.0 + version: 10.67.0(svelte@5.56.6(@typescript-eslint/types@8.58.0)) '@skeletonlabs/skeleton': specifier: ^4.15.2 - version: 4.15.2(tailwindcss@4.2.4) + version: 4.15.2(tailwindcss@4.3.3) '@skeletonlabs/skeleton-svelte': specifier: ^4.15.2 - version: 4.15.2(svelte@5.55.5) + version: 4.15.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)) '@tanstack/svelte-virtual': - specifier: ^3.13.24 - version: 3.13.24(svelte@5.55.5) + specifier: ^3.13.33 + version: 3.13.33(svelte@5.56.6(@typescript-eslint/types@8.58.0)) '@warkypublic/artemis-kit': specifier: ^1.0.10 version: 1.0.10 @@ -27,33 +27,33 @@ importers: specifier: ^1.0.1 version: 1.0.1 '@warkypublic/svelix': - specifier: ^0.1.40 - version: 0.1.40(highlight.js@11.8.0)(svelte@5.55.5)(unified@11.0.5) + specifier: ^0.2.5 + version: 0.2.5(highlight.js@11.8.0)(svelte@5.56.6(@typescript-eslint/types@8.58.0))(unified@11.0.5) devDependencies: '@sveltejs/vite-plugin-svelte': - specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.5)(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1)) + specifier: ^7.2.0 + version: 7.2.0(svelte@5.56.6(@typescript-eslint/types@8.58.0))(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)) '@tailwindcss/vite': - specifier: ^4.2.4 - version: 4.2.4(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1)) + specifier: ^4.3.3 + version: 4.3.3(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)) '@types/node': - specifier: ^25.6.0 - version: 25.6.0 + specifier: ^26.1.1 + version: 26.1.1 svelte: - specifier: ^5.55.5 - version: 5.55.5 + specifier: ^5.56.6 + version: 5.56.6(@typescript-eslint/types@8.58.0) svelte-check: - specifier: ^4.4.6 - version: 4.4.6(picomatch@4.0.4)(svelte@5.55.5)(typescript@6.0.3) + specifier: ^4.7.3 + version: 4.7.3(picomatch@4.0.5)(svelte@5.56.6(@typescript-eslint/types@8.58.0))(typescript@6.0.3) tailwindcss: - specifier: ^4.2.4 - version: 4.2.4 + specifier: ^4.3.3 + version: 4.3.3 typescript: specifier: ^6.0.3 version: 6.0.3 vite: - specifier: ^8.0.10 - version: 8.0.10(@types/node@25.6.0)(jiti@2.6.1) + specifier: ^8.1.5 + version: 8.1.5(@types/node@26.1.1)(jiti@2.7.0) packages: @@ -144,14 +144,14 @@ packages: resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} '@exodus/bytes@1.15.0': resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} @@ -171,8 +171,8 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - '@friendofsvelte/tipex@0.1.1': - resolution: {integrity: sha512-p+EgF9wHa1q1bkLhoFPmJ6KTJIdR5aWapzC+wFeLH0h2xzv8uMxmYsvydxS011HKs9s6rqYKIDQi7Ckj4uOVhQ==} + '@friendofsvelte/tipex@0.2.0': + resolution: {integrity: sha512-YWMaVvWVdoQSi8CLSeoJvQtJoubSy/v1V38wG2+mgS7VRGIbZyYC/CG7jlDdDT6OYff/UYsxl1kK32dZthjDKQ==} peerDependencies: svelte: ^5.0.0 @@ -202,14 +202,14 @@ packages: resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==} engines: {node: '>=12'} - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@oxc-project/types@0.127.0': - resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -217,130 +217,134 @@ packages: '@remirror/core-constants@3.0.0': resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} - '@rolldown/binding-android-arm64@1.0.0-rc.17': - resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.17': - resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.17': - resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.17': - resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': - resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': - resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': - resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': - resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': - resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': - resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': - resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': - resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': - resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': - resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': - resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.17': - resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - '@sentry-internal/browser-utils@10.51.0': - resolution: {integrity: sha512-lNKBS4P7RUvf1niojXQWe9bU3gnBUCbST4Dj0pSiyat1N96cXVyHkeE+uGxowD0RrVWhs+kGHiVX3FcmRWF6sA==} + '@sentry/browser-utils@10.67.0': + resolution: {integrity: sha512-HUzaf0xAnPAB+OHBkD7N1Py+CTbD5InHulQ/pdhX4JctWtxuwD8odMD1LzdPnW8J6gVHlDVvcVBR8mXMZYSLSw==} engines: {node: '>=18'} - '@sentry-internal/feedback@10.51.0': - resolution: {integrity: sha512-bCM95bcpphx28e6aU0bwRLxOgwosYsdNzezM1sM0pVOkb0TB3hDFRamramVDK+/Hp1o8qmRxS4c5w/A7YBZGkA==} + '@sentry/browser@10.67.0': + resolution: {integrity: sha512-/ZhsAvte4rYhg0A0RtSFFgAgXhyMOfQIeOAfMfptN+X6IVSYOfkA9jtrP+Ej4+6vlaUFWRir1HweF56y63dEEA==} engines: {node: '>=18'} - '@sentry-internal/replay-canvas@10.51.0': - resolution: {integrity: sha512-8PW1Pp+Yl3lPwYqhBCr5SgkuhDanu9ZLzUqD2bPKL/ElqbM2eDVIWxq4z4ZzePrmZa6IcCjTv6sVQJ7Z4dLyLA==} + '@sentry/conventions@0.16.0': + resolution: {integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==} + engines: {node: '>=14'} + + '@sentry/core@10.67.0': + resolution: {integrity: sha512-b6U3pJ8AUvN9aouq0vl+VZI8KT8RslBsfGMFuNwRr313zOmdmFJBZqTiUw9VGgJ2jGKxLO9alm9rlxBfX4hf+w==} engines: {node: '>=18'} - '@sentry-internal/replay@10.51.0': - resolution: {integrity: sha512-jCpI5HXSwK6ZT2HX70+mDRciAocHzSiDk4DTgvzV69Wvd+Ei5WLgE+d39eaEPsm8lUC0Ydntb5sJIB6uG9D4bw==} + '@sentry/feedback@10.67.0': + resolution: {integrity: sha512-I4ML2/SF3enwikb6ZSoRiqolQrx0zSzTSnUgwCmugICF/jpHW0th1pCray9R+t1Zzibw/Dpj4t/DNXaSDRa2MA==} engines: {node: '>=18'} - '@sentry/browser@10.51.0': - resolution: {integrity: sha512-Zdc0sKfenxUtW/OGhtJ7xHFN44bXR7YqxJ1zBDzlZfW0nTbeTTUZBq9z5NUw6qdS0Vs/i3V4qzAKTbRKWfqSEA==} + '@sentry/replay-canvas@10.67.0': + resolution: {integrity: sha512-neNA4T6MFtZzMdKYetiR+LZd9BNSd0q2szMn0wk+A15PqHE/IN7a34V6JZc9rCtmzB0wldh0eWGOBb49MSNKjA==} engines: {node: '>=18'} - '@sentry/core@10.51.0': - resolution: {integrity: sha512-Y45V/YXvVLEXmOdkbD1oG1gkRWFi9guCEGg3PlIlIpRjAbZUrvLGgjRJIc1E7XpSzmOnWbs5BbUxMv4PDaPj2w==} + '@sentry/replay@10.67.0': + resolution: {integrity: sha512-nkEUgPCR82EcyJkCf3XCE9H0R5KisCqyCAaSGxe7NpAoQbvASHx4MUNgXVAn+D0M494gvPZh6lFH7JgzqTcSqQ==} engines: {node: '>=18'} - '@sentry/svelte@10.51.0': - resolution: {integrity: sha512-2/OwIs+WXk+H/CAnWeiQMvXx5EG8LxCtqu4m+HdHtJTUQtERC388zNShZTJU9YYR71KoDOVAU0Q6B2sKNcssFA==} + '@sentry/svelte@10.67.0': + resolution: {integrity: sha512-qVAgbSnQkKJcBz2QX6qm5GQmgEEYoye29A8jYdofuQb6p1lbH0FPtbzT/95mqT/uayRAlswNnG+8JV4WfYCJ5w==} engines: {node: '>=18'} peerDependencies: svelte: 3.x || 4.x || 5.x @@ -386,17 +390,17 @@ packages: peerDependencies: tailwindcss: ^4.0.0 - '@svar-ui/core-locales@2.5.1': - resolution: {integrity: sha512-3VYDf/zdFVPKb8Zyf6u5IjM089WGOtNswPG8KT4KZNEoWiVIHblCJp1uXfzHERTFikNSVjllaT44S7M9nOhRsw==} + '@svar-ui/core-locales@2.6.0': + resolution: {integrity: sha512-Sma/BkR3we76Ix/67NmSh511yvtKVpi8LFK06ZbWSiC3xEsCSoKsJ5AtLmi8dxaXPa5HuCWI/jMwJsKRwQ0F2Q==} - '@svar-ui/grid-data-provider@2.6.2': - resolution: {integrity: sha512-wFmxuaWsodJvgCMyRIBm29X4ETSLxxZ1sKqw16TRt+THlulO7TB9bwCBzUAS46jhgtMahXT1Kyi3Yc4CfzLnIQ==} + '@svar-ui/grid-data-provider@2.7.1': + resolution: {integrity: sha512-smPXRTm8Qhb6xmLv403blTLnBgNvwie5UAE+b94Pqh2Fly3KnH7l2ZIH5qSfdnUXFBzZDBBxXvBgkuWrdW+yMg==} - '@svar-ui/grid-locales@2.6.2': - resolution: {integrity: sha512-K8HKXly8t0imBJC23e/ZRa+t3jLGf+S+QW4ryKoqVwhdqOi3ugndDPj5mYfNNpZEwox7aWhdLBiakP+QMs2ueA==} + '@svar-ui/grid-locales@2.7.1': + resolution: {integrity: sha512-TNcDdA/QbS/mPhrFJAifXGmq2IBzUsnmm2NhLn9Ps6LbBN5Sz9O+UcE2dzMxQW67XFDr0EJS1iQirqbf13MYNA==} - '@svar-ui/grid-store@2.6.2': - resolution: {integrity: sha512-M7VsTCAvpyzpE46lA0YN1ZQfp09o1O6TszKnVCuB7Fu87w4oB4NtkNKdoI8TXDEYY3z7MGv6eHQ3c/TXpsq1eQ==} + '@svar-ui/grid-store@2.7.1': + resolution: {integrity: sha512-H83DInIOFIefaZN3wNHfi81nnHM/hFsv7qY9pWYZMINWZ6sVWoEBSCksg8nGw00h/CsT0dMHlqtIOTe5xZl1kg==} '@svar-ui/lib-data-provider@1.7.2': resolution: {integrity: sha512-Od93Iiy0vBwVzgJm25WdizH3d4Xlli/HAA2kqoUj1pzeZ2SWBeFzvE59iihQz8NuIlfAtQJ6MMe168458T+2lQ==} @@ -404,31 +408,41 @@ packages: '@svar-ui/lib-dom@0.12.1': resolution: {integrity: sha512-srvDleQqR6stBBDijCgzDHMFTTffI6sv3XCpNPKmurrnE83L1MDNqezqg6j3+0dUVPd4AAEGOez2DvK6byblTQ==} + '@svar-ui/lib-dom@0.13.1': + resolution: {integrity: sha512-e9+RIEkppVkAUJqfHEr7KmiVXi3uf+qddr513h5mhgSx3PNYtG9eHwtI8OQf4JMc6Hvuu3y0iv+fL6jqE340yw==} + '@svar-ui/lib-state@1.9.6': resolution: {integrity: sha512-twoHa8wZvzdxtES68s6Wnp4Xi1WivScNaFFK6sJ6WDgqVYOzJw0ZhVVrdwHQgieLITTKl/DYh/U3nuqtMetKYw==} + '@svar-ui/lib-state@1.9.7': + resolution: {integrity: sha512-nxRdk+54m1RJjGT2yDH7NjxAM5KECxA39kdTZezEIj1ixykyefssB9BrgxKy2jIseYxJMtd6lPZvQ3SZXuvn2g==} + '@svar-ui/lib-svelte@0.5.2': resolution: {integrity: sha512-i+hHMT3zctaGbRpyMDREt9im7YfLpvGwFYvH1ntEp/kYAwUrR/sxMJlEEKIid+Yg2EeMUstGxmYQbkWoMpPG7A==} - '@svar-ui/svelte-core@2.5.1': - resolution: {integrity: sha512-snXNEYhWkg+7JQmhsS3xnN6UozfTj8+cQadKl3dsUyh3328F6NZZpKbUkXNArK95+OcueJZUXyExkDWv1NqmDA==} + '@svar-ui/svelte-core@2.6.0': + resolution: {integrity: sha512-5fnEm4FXjRpVZgU8EoOMisiZ0bkkj0gtP1RK66Pi5Jw9OWu+ytZleaeUyFkpBxWNPX9yENkqEEOMWUnitPYO6A==} - '@svar-ui/svelte-grid@2.6.2': - resolution: {integrity: sha512-iFOoYBZQnXMQUx2Ko80sjcDGBWvmKQSHb/pYesdYEewgvF5w3ZeUc0qG3FX/7EWthKADoIACzdMLVHDRY9j1ow==} + '@svar-ui/svelte-grid@2.7.1': + resolution: {integrity: sha512-vGsEJt46qEwhCH3rWBWlOygdVYsbr4qlMrqSdi7Mlut4GY+smkNuWUL2L/OcR1bki2f+VlCmeYybaDwPHSOjDQ==} - '@svar-ui/svelte-menu@2.5.1': - resolution: {integrity: sha512-2ysDlaWR9FrLPRjCd/KB9L9FQry6xR06krAGKPvfFeueXIo3ySuRks6lniNzbLMvKWHG6VOzrBlAAfn0FTeipw==} + '@svar-ui/svelte-menu@2.6.0': + resolution: {integrity: sha512-1sXgVKG4hUhkrnbfrSKvfp0M/OqMjbHBnoYRMZKNLs9z1hWGJpKqzZZkUZg8ahH40SfitMjK8XjEgGMFBwC/7g==} - '@svar-ui/svelte-toolbar@2.5.1': - resolution: {integrity: sha512-QAomfCKtX4tqFLLid/H/U5STqVwrFi5FKPGssDHO55rYlyk+q/v6TfkO3qJYQlFEveui7IgaoWHY9lGxIn8tXg==} + '@svar-ui/svelte-toolbar@2.6.0': + resolution: {integrity: sha512-/LHwQdo2FTzuQHdNXzmV0rsnKONAMHvb1M+8jSv6WCzfSBtggEEZ1voItl4EB3NzoBeI42zKxwuiskjXpkHyoQ==} - '@sveltejs/acorn-typescript@1.0.9': - resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} + '@sveltejs/acorn-typescript@1.0.11': + resolution: {integrity: sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==} peerDependencies: acorn: ^8.9.0 - '@sveltejs/vite-plugin-svelte@7.0.0': - resolution: {integrity: sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==} + '@sveltejs/load-config@0.2.0': + resolution: {integrity: sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==} + engines: {node: '>= 18.0.0'} + + '@sveltejs/vite-plugin-svelte@7.2.0': + resolution: {integrity: sha512-1SpkuMSRLfugrVX+IrKfE1RUegzo8AQzKQ6qQPfVzbcWi5IhuTPaKb5ZrLpucleFznkc4/RTeSPoRnGWFxX+EQ==} engines: {node: ^20.19 || ^22.12 || >=24} peerDependencies: svelte: ^5.46.4 @@ -437,69 +451,69 @@ packages: '@swc/helpers@0.5.21': resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} - '@tailwindcss/node@4.2.4': - resolution: {integrity: sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==} + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} - '@tailwindcss/oxide-android-arm64@4.2.4': - resolution: {integrity: sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==} + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.2.4': - resolution: {integrity: sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==} + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.2.4': - resolution: {integrity: sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==} + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.2.4': - resolution: {integrity: sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==} + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': - resolution: {integrity: sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': - resolution: {integrity: sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==} + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.2.4': - resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==} + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.2.4': - resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==} + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-x64-musl@4.2.4': - resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==} + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [musl] - '@tailwindcss/oxide-wasm32-wasi@4.2.4': - resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==} + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -510,34 +524,34 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': - resolution: {integrity: sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==} + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.2.4': - resolution: {integrity: sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==} + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.2.4': - resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==} + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} engines: {node: '>= 20'} - '@tailwindcss/vite@4.2.4': - resolution: {integrity: sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==} + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 - '@tanstack/svelte-virtual@3.13.24': - resolution: {integrity: sha512-Up3LOD5Cj+oJ3GuKfM1Li06jzzZMIZnRPmu3aik9rJQgk7jq7LgPo4yumfUw4+I4edjYfyPKSZnXGwZ9Vjlebw==} + '@tanstack/svelte-virtual@3.13.33': + resolution: {integrity: sha512-7UGtz/Nj9FKTKk65+IMBAbm8/IJG8Hq+0QqT/gWEI+X/xP5hPmiMnCbxkmeDz9hlJlLZHc1febzQZOKGJNFcSw==} peerDependencies: svelte: ^3.48.0 || ^4.0.0 || ^5.0.0 - '@tanstack/virtual-core@3.14.0': - resolution: {integrity: sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==} + '@tanstack/virtual-core@3.17.5': + resolution: {integrity: sha512-AXfBC3sq6PuYSwyxYORqqgHCNjPGAvKJvZuBBJ1klhztWBB5cgqgwsq8+fNfaQJG7/K4xYBja9S90QFn2zmQAg==} '@tiptap/core@2.27.2': resolution: {integrity: sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ==} @@ -698,8 +712,8 @@ packages: '@tiptap/starter-kit@2.27.2': resolution: {integrity: sha512-bb0gJvPoDuyRUQ/iuN52j1//EtWWttw+RXAv1uJxfR0uKf8X7uAqzaOOgwjknoCIDC97+1YHwpGdnRjpDkOBxw==} - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} @@ -731,8 +745,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@25.6.0': - resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + '@types/node@26.1.1': + resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -749,6 +763,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@warkypublic/artemis-kit@1.0.10': resolution: {integrity: sha512-qIgjcWqLyYfoKDUYt3Gm7PVe2S4AdjA46J1jPIff1p6wUP5WsHA8UfZq7pEdP6YNxqavv+h84oe1+HsJOoU6jQ==} @@ -758,8 +773,8 @@ packages: resolution: {integrity: sha512-uXP1HouxpOKXfwE6qpy0gCcrMPIgjDT53aVGkfork4QejRSunbKWSKKawW2nIm7RnyFhSjPILMXcnT5xUiXOew==} engines: {node: '>=18'} - '@warkypublic/svelix@0.1.40': - resolution: {integrity: sha512-Pn4T+VVI1Pfrtkbr61oqVyhaUTySU0r6gti9SeSqL+ZJeRfXAj+ZTuwlHzk0w9BxrwlWDnVinGyrpbSjnwb+iQ==} + '@warkypublic/svelix@0.2.5': + resolution: {integrity: sha512-XBtHqunz5FPicT7EFkm33mIiUj+3LE8Nc+j8E1C4E8WYzOhv+wvDhCzsx8vl2pQBNlAuB3RCJ5vPX/19JGgx2w==} peerDependencies: svelte: ^5.0.0 @@ -1012,8 +1027,8 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.6.4: - resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==} + devalue@5.8.2: + resolution: {integrity: sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -1025,8 +1040,8 @@ packages: dompurify@3.2.7: resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} - dompurify@3.4.1: - resolution: {integrity: sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==} + dompurify@3.4.12: + resolution: {integrity: sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==} emojilib@2.4.0: resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} @@ -1034,8 +1049,8 @@ packages: emoticon@4.1.0: resolution: {integrity: sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==} - enhanced-resolve@5.20.1: - resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + enhanced-resolve@5.24.3: + resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} engines: {node: '>=10.13.0'} entities@4.5.0: @@ -1061,8 +1076,13 @@ packages: esm-env@1.2.2: resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} - esrap@2.2.4: - resolution: {integrity: sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==} + esrap@2.3.0: + resolution: {integrity: sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -1158,19 +1178,19 @@ packages: is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - isomorphic-dompurify@3.10.0: - resolution: {integrity: sha512-Gj2duy4dACsP/FLPvwJ3+MXTlGtOo+O4yfpA0jdxuz/sZlbZzazGzScajOHRwH7PCy4j3bh5ibLGJY4/Rb5kGQ==} + isomorphic-dompurify@3.19.0: + resolution: {integrity: sha512-ynZ/6cVv4Trpt1FEd5TkHsHtLs+y/YQGb+ocIIVRdc+DkiYRbxlEL/IiSeSeQXwopQcG5FhoeIUJa1l26IgSpw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true jsbi@4.3.2: resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==} - jsdom@29.0.2: - resolution: {integrity: sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==} + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} peerDependencies: canvas: ^3.0.0 @@ -1182,6 +1202,10 @@ packages: resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} hasBin: true + katex@0.17.0: + resolution: {integrity: sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==} + hasBin: true + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -1432,8 +1456,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -1462,12 +1486,12 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} - postcss@8.5.12: - resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} + postcss@8.5.20: + resolution: {integrity: sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==} engines: {node: ^10 || ^12 || >=14} property-information@7.1.0: @@ -1593,8 +1617,8 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - rolldown@1.0.0-rc.17: - resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -1631,30 +1655,30 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - svelte-check@4.4.6: - resolution: {integrity: sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==} + svelte-check@4.7.3: + resolution: {integrity: sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.0 typescript: '>=5.0.0' - svelte@5.55.5: - resolution: {integrity: sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==} + svelte@5.56.6: + resolution: {integrity: sha512-p4HDLDogGHKRKCrgckQHNs5PEfXkju6JI5jTywueaKJI5hAdjPohEhRtQ0M1SWC/+TA73SPln+r7srr+7e4nZA==} engines: {node: '>=18'} symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - tailwindcss@4.2.4: - resolution: {integrity: sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==} + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} - tapable@2.3.2: - resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} tippy.js@6.3.7: @@ -1692,8 +1716,8 @@ packages: uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - undici-types@7.19.2: - resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} undici@7.25.0: resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} @@ -1744,13 +1768,13 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite@8.0.10: - resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==} + vite@8.1.5: + resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.0 + '@vitejs/devtools': ^0.3.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -1856,43 +1880,43 @@ snapshots: dependencies: css-tree: 3.2.1 - '@cartamd/plugin-anchor@2.2.0(carta-md@4.11.2(svelte@5.55.5))': + '@cartamd/plugin-anchor@2.2.0(carta-md@4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)))': dependencies: - carta-md: 4.11.2(svelte@5.55.5) + carta-md: 4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)) rehype-autolink-headings: 7.1.0 rehype-slug: 6.0.0 - '@cartamd/plugin-attachment@4.2.0(carta-md@4.11.2(svelte@5.55.5))': + '@cartamd/plugin-attachment@4.2.0(carta-md@4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)))': dependencies: - carta-md: 4.11.2(svelte@5.55.5) + carta-md: 4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)) - '@cartamd/plugin-code@4.2.0(carta-md@4.11.2(svelte@5.55.5))': + '@cartamd/plugin-code@4.2.0(carta-md@4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)))': dependencies: '@shikijs/rehype': 3.23.0 - carta-md: 4.11.2(svelte@5.55.5) + carta-md: 4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)) unified: 11.0.5 - '@cartamd/plugin-component@1.1.1(carta-md@4.11.2(svelte@5.55.5))(unified@11.0.5)': + '@cartamd/plugin-component@1.1.1(carta-md@4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)))(unified@11.0.5)': dependencies: - carta-md: 4.11.2(svelte@5.55.5) + carta-md: 4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)) esm-env: 1.2.2 rehype-parse: 9.0.1 unified: 11.0.5 unist-util-visit: 5.1.0 - '@cartamd/plugin-emoji@4.3.0(carta-md@4.11.2(svelte@5.55.5))': + '@cartamd/plugin-emoji@4.3.0(carta-md@4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)))': dependencies: bezier-easing: 2.1.0 - carta-md: 4.11.2(svelte@5.55.5) + carta-md: 4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)) node-emoji: 2.2.0 remark-emoji: 5.0.2 - '@cartamd/plugin-math@4.3.1(carta-md@4.11.2(svelte@5.55.5))(svelte@5.55.5)': + '@cartamd/plugin-math@4.3.1(carta-md@4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)))(svelte@5.56.6(@typescript-eslint/types@8.58.0))': dependencies: - carta-md: 4.11.2(svelte@5.55.5) + carta-md: 4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)) rehype-katex: 7.0.1 remark-math: 6.0.0 - svelte: 5.55.5 + svelte: 5.56.6(@typescript-eslint/types@8.58.0) transitivePeerDependencies: - supports-color @@ -1920,18 +1944,18 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} - '@emnapi/core@1.10.0': + '@emnapi/core@1.11.1': dependencies: - '@emnapi/wasi-threads': 1.2.1 + '@emnapi/wasi-threads': 1.2.2 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.10.0': + '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.1': + '@emnapi/wasi-threads@1.2.2': dependencies: tslib: 2.8.1 optional: true @@ -1949,7 +1973,7 @@ snapshots: '@floating-ui/utils@0.2.11': {} - '@friendofsvelte/tipex@0.1.1(highlight.js@11.8.0)(svelte@5.55.5)': + '@friendofsvelte/tipex@0.2.0(highlight.js@11.8.0)(svelte@5.56.6(@typescript-eslint/types@8.58.0))': dependencies: '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) '@tiptap/extension-code-block': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) @@ -1965,7 +1989,7 @@ snapshots: '@tiptap/starter-kit': 2.27.2 iconify-icon: 1.0.8 lowlight: 2.9.0 - svelte: 5.55.5 + svelte: 5.56.6(@typescript-eslint/types@8.58.0) transitivePeerDependencies: - highlight.js @@ -1998,104 +2022,110 @@ snapshots: dependencies: jsbi: 4.3.2 - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.1 + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 optional: true - '@oxc-project/types@0.127.0': {} + '@oxc-project/types@0.139.0': {} '@popperjs/core@2.11.8': {} '@remirror/core-constants@3.0.0': {} - '@rolldown/binding-android-arm64@1.0.0-rc.17': + '@rolldown/binding-android-arm64@1.1.5': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + '@rolldown/binding-darwin-arm64@1.1.5': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.17': + '@rolldown/binding-darwin-x64@1.1.5': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + '@rolldown/binding-freebsd-x64@1.1.5': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + '@rolldown/binding-linux-arm64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + '@rolldown/binding-linux-arm64-musl@1.1.5': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + '@rolldown/binding-linux-ppc64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + '@rolldown/binding-linux-s390x-gnu@1.1.5': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + '@rolldown/binding-linux-x64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + '@rolldown/binding-linux-x64-musl@1.1.5': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + '@rolldown/binding-openharmony-arm64@1.1.5': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + '@rolldown/binding-wasm32-wasi@1.1.5': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + '@rolldown/binding-win32-arm64-msvc@1.1.5': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + '@rolldown/binding-win32-x64-msvc@1.1.5': optional: true - '@rolldown/pluginutils@1.0.0-rc.17': {} + '@rolldown/pluginutils@1.0.1': {} - '@sentry-internal/browser-utils@10.51.0': + '@sentry/browser-utils@10.67.0': dependencies: - '@sentry/core': 10.51.0 + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.67.0 - '@sentry-internal/feedback@10.51.0': + '@sentry/browser@10.67.0': dependencies: - '@sentry/core': 10.51.0 + '@sentry/browser-utils': 10.67.0 + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.67.0 + '@sentry/feedback': 10.67.0 + '@sentry/replay': 10.67.0 + '@sentry/replay-canvas': 10.67.0 - '@sentry-internal/replay-canvas@10.51.0': + '@sentry/conventions@0.16.0': {} + + '@sentry/core@10.67.0': dependencies: - '@sentry-internal/replay': 10.51.0 - '@sentry/core': 10.51.0 + '@sentry/conventions': 0.16.0 - '@sentry-internal/replay@10.51.0': + '@sentry/feedback@10.67.0': dependencies: - '@sentry-internal/browser-utils': 10.51.0 - '@sentry/core': 10.51.0 + '@sentry/core': 10.67.0 - '@sentry/browser@10.51.0': + '@sentry/replay-canvas@10.67.0': dependencies: - '@sentry-internal/browser-utils': 10.51.0 - '@sentry-internal/feedback': 10.51.0 - '@sentry-internal/replay': 10.51.0 - '@sentry-internal/replay-canvas': 10.51.0 - '@sentry/core': 10.51.0 + '@sentry/core': 10.67.0 + '@sentry/replay': 10.67.0 - '@sentry/core@10.51.0': {} - - '@sentry/svelte@10.51.0(svelte@5.55.5)': + '@sentry/replay@10.67.0': dependencies: - '@sentry/browser': 10.51.0 - '@sentry/core': 10.51.0 + '@sentry/browser-utils': 10.67.0 + '@sentry/core': 10.67.0 + + '@sentry/svelte@10.67.0(svelte@5.56.6(@typescript-eslint/types@8.58.0))': + dependencies: + '@sentry/browser': 10.67.0 + '@sentry/core': 10.67.0 magic-string: 0.30.21 - svelte: 5.55.5 + svelte: 5.56.6(@typescript-eslint/types@8.58.0) '@shikijs/core@3.23.0': dependencies: @@ -2143,7 +2173,7 @@ snapshots: '@skeletonlabs/skeleton-common@4.15.2': {} - '@skeletonlabs/skeleton-svelte@4.15.2(svelte@5.55.5)': + '@skeletonlabs/skeleton-svelte@4.15.2(svelte@5.56.6(@typescript-eslint/types@8.58.0))': dependencies: '@internationalized/date': 3.12.0 '@skeletonlabs/skeleton-common': 4.15.2 @@ -2166,7 +2196,7 @@ snapshots: '@zag-js/rating-group': 1.39.1 '@zag-js/slider': 1.39.1 '@zag-js/steps': 1.39.1 - '@zag-js/svelte': 1.39.1(svelte@5.55.5) + '@zag-js/svelte': 1.39.1(svelte@5.56.6(@typescript-eslint/types@8.58.0)) '@zag-js/switch': 1.39.1 '@zag-js/tabs': 1.39.1 '@zag-js/tags-input': 1.39.1 @@ -2174,21 +2204,21 @@ snapshots: '@zag-js/toggle-group': 1.39.1 '@zag-js/tooltip': 1.39.1 '@zag-js/tree-view': 1.39.1 - svelte: 5.55.5 + svelte: 5.56.6(@typescript-eslint/types@8.58.0) - '@skeletonlabs/skeleton@4.15.2(tailwindcss@4.2.4)': + '@skeletonlabs/skeleton@4.15.2(tailwindcss@4.3.3)': dependencies: - tailwindcss: 4.2.4 + tailwindcss: 4.3.3 - '@svar-ui/core-locales@2.5.1': {} + '@svar-ui/core-locales@2.6.0': {} - '@svar-ui/grid-data-provider@2.6.2': + '@svar-ui/grid-data-provider@2.7.1': dependencies: '@svar-ui/lib-data-provider': 1.7.2 - '@svar-ui/grid-locales@2.6.2': {} + '@svar-ui/grid-locales@2.7.1': {} - '@svar-ui/grid-store@2.6.2': + '@svar-ui/grid-store@2.7.1': dependencies: '@svar-ui/lib-dom': 0.12.1 '@svar-ui/lib-state': 1.9.6 @@ -2199,131 +2229,137 @@ snapshots: '@svar-ui/lib-dom@0.12.1': {} + '@svar-ui/lib-dom@0.13.1': {} + '@svar-ui/lib-state@1.9.6': {} + '@svar-ui/lib-state@1.9.7': {} + '@svar-ui/lib-svelte@0.5.2': dependencies: - '@svar-ui/lib-state': 1.9.6 + '@svar-ui/lib-state': 1.9.7 - '@svar-ui/svelte-core@2.5.1': + '@svar-ui/svelte-core@2.6.0': dependencies: - '@svar-ui/core-locales': 2.5.1 - '@svar-ui/lib-dom': 0.12.1 + '@svar-ui/core-locales': 2.6.0 + '@svar-ui/lib-dom': 0.13.1 '@svar-ui/lib-svelte': 0.5.2 - '@svar-ui/svelte-grid@2.6.2': + '@svar-ui/svelte-grid@2.7.1': dependencies: - '@svar-ui/grid-data-provider': 2.6.2 - '@svar-ui/grid-locales': 2.6.2 - '@svar-ui/grid-store': 2.6.2 - '@svar-ui/lib-dom': 0.12.1 - '@svar-ui/lib-state': 1.9.6 + '@svar-ui/grid-data-provider': 2.7.1 + '@svar-ui/grid-locales': 2.7.1 + '@svar-ui/grid-store': 2.7.1 + '@svar-ui/lib-dom': 0.13.1 + '@svar-ui/lib-state': 1.9.7 '@svar-ui/lib-svelte': 0.5.2 - '@svar-ui/svelte-core': 2.5.1 - '@svar-ui/svelte-menu': 2.5.1 - '@svar-ui/svelte-toolbar': 2.5.1 + '@svar-ui/svelte-core': 2.6.0 + '@svar-ui/svelte-menu': 2.6.0 + '@svar-ui/svelte-toolbar': 2.6.0 - '@svar-ui/svelte-menu@2.5.1': + '@svar-ui/svelte-menu@2.6.0': dependencies: - '@svar-ui/lib-dom': 0.12.1 - '@svar-ui/svelte-core': 2.5.1 + '@svar-ui/lib-dom': 0.13.1 + '@svar-ui/svelte-core': 2.6.0 - '@svar-ui/svelte-toolbar@2.5.1': + '@svar-ui/svelte-toolbar@2.6.0': dependencies: - '@svar-ui/lib-dom': 0.12.1 - '@svar-ui/svelte-core': 2.5.1 + '@svar-ui/lib-dom': 0.13.1 + '@svar-ui/svelte-core': 2.6.0 - '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': + '@sveltejs/acorn-typescript@1.0.11(acorn@8.16.0)': dependencies: acorn: 8.16.0 - '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5)(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1))': + '@sveltejs/load-config@0.2.0': {} + + '@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.6(@typescript-eslint/types@8.58.0))(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0))': dependencies: deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 - svelte: 5.55.5 - vite: 8.0.10(@types/node@25.6.0)(jiti@2.6.1) - vitefu: 1.1.3(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1)) + svelte: 5.56.6(@typescript-eslint/types@8.58.0) + vite: 8.1.5(@types/node@26.1.1)(jiti@2.7.0) + vitefu: 1.1.3(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)) '@swc/helpers@0.5.21': dependencies: tslib: 2.8.1 - '@tailwindcss/node@4.2.4': + '@tailwindcss/node@4.3.3': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.20.1 - jiti: 2.6.1 + enhanced-resolve: 5.24.3 + jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.2.4 + tailwindcss: 4.3.3 - '@tailwindcss/oxide-android-arm64@4.2.4': + '@tailwindcss/oxide-android-arm64@4.3.3': optional: true - '@tailwindcss/oxide-darwin-arm64@4.2.4': + '@tailwindcss/oxide-darwin-arm64@4.3.3': optional: true - '@tailwindcss/oxide-darwin-x64@4.2.4': + '@tailwindcss/oxide-darwin-x64@4.3.3': optional: true - '@tailwindcss/oxide-freebsd-x64@4.2.4': + '@tailwindcss/oxide-freebsd-x64@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.2.4': + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.2.4': + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.2.4': + '@tailwindcss/oxide-linux-x64-musl@4.3.3': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.2.4': + '@tailwindcss/oxide-wasm32-wasi@4.3.3': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.2.4': + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': optional: true - '@tailwindcss/oxide@4.2.4': + '@tailwindcss/oxide@4.3.3': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.2.4 - '@tailwindcss/oxide-darwin-arm64': 4.2.4 - '@tailwindcss/oxide-darwin-x64': 4.2.4 - '@tailwindcss/oxide-freebsd-x64': 4.2.4 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.4 - '@tailwindcss/oxide-linux-arm64-gnu': 4.2.4 - '@tailwindcss/oxide-linux-arm64-musl': 4.2.4 - '@tailwindcss/oxide-linux-x64-gnu': 4.2.4 - '@tailwindcss/oxide-linux-x64-musl': 4.2.4 - '@tailwindcss/oxide-wasm32-wasi': 4.2.4 - '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4 - '@tailwindcss/oxide-win32-x64-msvc': 4.2.4 + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.2.4(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1))': + '@tailwindcss/vite@4.3.3(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0))': dependencies: - '@tailwindcss/node': 4.2.4 - '@tailwindcss/oxide': 4.2.4 - tailwindcss: 4.2.4 - vite: 8.0.10(@types/node@25.6.0)(jiti@2.6.1) + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 8.1.5(@types/node@26.1.1)(jiti@2.7.0) - '@tanstack/svelte-virtual@3.13.24(svelte@5.55.5)': + '@tanstack/svelte-virtual@3.13.33(svelte@5.56.6(@typescript-eslint/types@8.58.0))': dependencies: - '@tanstack/virtual-core': 3.14.0 - svelte: 5.55.5 + '@tanstack/virtual-core': 3.17.5 + svelte: 5.56.6(@typescript-eslint/types@8.58.0) - '@tanstack/virtual-core@3.14.0': {} + '@tanstack/virtual-core@3.17.5': {} '@tiptap/core@2.27.2(@tiptap/pm@2.27.2)': dependencies: @@ -2497,7 +2533,7 @@ snapshots: '@tiptap/extension-text-style': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) '@tiptap/pm': 2.27.2 - '@tybys/wasm-util@0.10.1': + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true @@ -2533,9 +2569,9 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@25.6.0': + '@types/node@26.1.1': dependencies: - undici-types: 7.19.2 + undici-types: 8.3.0 '@types/trusted-types@2.0.7': {} @@ -2543,7 +2579,8 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript-eslint/types@8.58.0': {} + '@typescript-eslint/types@8.58.0': + optional: true '@ungap/structured-clone@1.3.0': {} @@ -2556,25 +2593,25 @@ snapshots: dependencies: uuid: 13.0.0 - '@warkypublic/svelix@0.1.40(highlight.js@11.8.0)(svelte@5.55.5)(unified@11.0.5)': + '@warkypublic/svelix@0.2.5(highlight.js@11.8.0)(svelte@5.56.6(@typescript-eslint/types@8.58.0))(unified@11.0.5)': dependencies: - '@cartamd/plugin-anchor': 2.2.0(carta-md@4.11.2(svelte@5.55.5)) - '@cartamd/plugin-attachment': 4.2.0(carta-md@4.11.2(svelte@5.55.5)) - '@cartamd/plugin-code': 4.2.0(carta-md@4.11.2(svelte@5.55.5)) - '@cartamd/plugin-component': 1.1.1(carta-md@4.11.2(svelte@5.55.5))(unified@11.0.5) - '@cartamd/plugin-emoji': 4.3.0(carta-md@4.11.2(svelte@5.55.5)) - '@cartamd/plugin-math': 4.3.1(carta-md@4.11.2(svelte@5.55.5))(svelte@5.55.5) - '@friendofsvelte/tipex': 0.1.1(highlight.js@11.8.0)(svelte@5.55.5) + '@cartamd/plugin-anchor': 2.2.0(carta-md@4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0))) + '@cartamd/plugin-attachment': 4.2.0(carta-md@4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0))) + '@cartamd/plugin-code': 4.2.0(carta-md@4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0))) + '@cartamd/plugin-component': 1.1.1(carta-md@4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)))(unified@11.0.5) + '@cartamd/plugin-emoji': 4.3.0(carta-md@4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0))) + '@cartamd/plugin-math': 4.3.1(carta-md@4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)))(svelte@5.56.6(@typescript-eslint/types@8.58.0)) + '@friendofsvelte/tipex': 0.2.0(highlight.js@11.8.0)(svelte@5.56.6(@typescript-eslint/types@8.58.0)) '@js-temporal/polyfill': 0.5.1 - '@svar-ui/svelte-grid': 2.6.2 + '@svar-ui/svelte-grid': 2.7.1 '@warkypublic/artemis-kit': 1.0.10 '@warkypublic/resolvespec-js': 1.0.1 - carta-md: 4.11.2(svelte@5.55.5) + carta-md: 4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)) github-markdown-css: 5.9.0 - isomorphic-dompurify: 3.10.0 - katex: 0.16.45 + isomorphic-dompurify: 3.19.0 + katex: 0.17.0 monaco-editor: 0.55.1 - svelte: 5.55.5 + svelte: 5.56.6(@typescript-eslint/types@8.58.0) transitivePeerDependencies: - '@noble/hashes' - canvas @@ -2834,12 +2871,12 @@ snapshots: dependencies: proxy-compare: 3.0.1 - '@zag-js/svelte@1.39.1(svelte@5.55.5)': + '@zag-js/svelte@1.39.1(svelte@5.56.6(@typescript-eslint/types@8.58.0))': dependencies: '@zag-js/core': 1.39.1 '@zag-js/types': 1.39.1 '@zag-js/utils': 1.39.1 - svelte: 5.55.5 + svelte: 5.56.6(@typescript-eslint/types@8.58.0) '@zag-js/switch@1.39.1': dependencies: @@ -2927,7 +2964,7 @@ snapshots: dependencies: require-from-string: 2.0.2 - carta-md@4.11.2(svelte@5.55.5): + carta-md@4.11.2(svelte@5.56.6(@typescript-eslint/types@8.58.0)): dependencies: diff: 5.2.2 esm-env: 1.2.2 @@ -2936,7 +2973,7 @@ snapshots: remark-parse: 11.0.0 remark-rehype: 11.1.2 shiki: 3.23.0 - svelte: 5.55.5 + svelte: 5.56.6(@typescript-eslint/types@8.58.0) unified: 11.0.5 transitivePeerDependencies: - supports-color @@ -2993,7 +3030,7 @@ snapshots: detect-libc@2.1.2: {} - devalue@5.6.4: {} + devalue@5.8.2: {} devlop@1.1.0: dependencies: @@ -3005,7 +3042,7 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 - dompurify@3.4.1: + dompurify@3.4.12: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -3013,10 +3050,10 @@ snapshots: emoticon@4.1.0: {} - enhanced-resolve@5.20.1: + enhanced-resolve@5.24.3: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.2 + tapable: 2.3.3 entities@4.5.0: {} @@ -3030,9 +3067,10 @@ snapshots: esm-env@1.2.2: {} - esrap@2.2.4: + esrap@2.3.0(@typescript-eslint/types@8.58.0): dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + optionalDependencies: '@typescript-eslint/types': 8.58.0 extend@3.0.2: {} @@ -3041,9 +3079,9 @@ snapshots: dependencies: format: 0.2.2 - fdir@6.5.0(picomatch@4.0.4): + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: - picomatch: 4.0.4 + picomatch: 4.0.5 format@0.2.2: {} @@ -3160,19 +3198,19 @@ snapshots: dependencies: '@types/estree': 1.0.8 - isomorphic-dompurify@3.10.0: + isomorphic-dompurify@3.19.0: dependencies: - dompurify: 3.4.1 - jsdom: 29.0.2 + dompurify: 3.4.12 + jsdom: 29.1.1 transitivePeerDependencies: - '@noble/hashes' - canvas - jiti@2.6.1: {} + jiti@2.7.0: {} jsbi@4.3.2: {} - jsdom@29.0.2: + jsdom@29.1.1: dependencies: '@asamuzakjp/css-color': 5.1.11 '@asamuzakjp/dom-selector': 7.1.1 @@ -3202,6 +3240,10 @@ snapshots: dependencies: commander: 8.3.0 + katex@0.17.0: + dependencies: + commander: 8.3.0 + lightningcss-android-arm64@1.32.0: optional: true @@ -3626,7 +3668,7 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.11: {} + nanoid@3.3.16: {} node-emoji@2.2.0: dependencies: @@ -3657,11 +3699,11 @@ snapshots: picocolors@1.1.1: {} - picomatch@4.0.4: {} + picomatch@4.0.5: {} - postcss@8.5.12: + postcss@8.5.20: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -3880,26 +3922,26 @@ snapshots: require-from-string@2.0.2: {} - rolldown@1.0.0-rc.17: + rolldown@1.1.5: dependencies: - '@oxc-project/types': 0.127.0 - '@rolldown/pluginutils': 1.0.0-rc.17 + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.17 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.17 - '@rolldown/binding-darwin-x64': 1.0.0-rc.17 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.17 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 rope-sequence@1.3.4: {} @@ -3937,47 +3979,50 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 - svelte-check@4.4.6(picomatch@4.0.4)(svelte@5.55.5)(typescript@6.0.3): + svelte-check@4.7.3(picomatch@4.0.5)(svelte@5.56.6(@typescript-eslint/types@8.58.0))(typescript@6.0.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 + '@sveltejs/load-config': 0.2.0 chokidar: 4.0.3 - fdir: 6.5.0(picomatch@4.0.4) + fdir: 6.5.0(picomatch@4.0.5) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.55.5 + svelte: 5.56.6(@typescript-eslint/types@8.58.0) typescript: 6.0.3 transitivePeerDependencies: - picomatch - svelte@5.55.5: + svelte@5.56.6(@typescript-eslint/types@8.58.0): dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) + '@sveltejs/acorn-typescript': 1.0.11(acorn@8.16.0) '@types/estree': 1.0.8 '@types/trusted-types': 2.0.7 acorn: 8.16.0 aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.6.4 + devalue: 5.8.2 esm-env: 1.2.2 - esrap: 2.2.4 + esrap: 2.3.0(@typescript-eslint/types@8.58.0) is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' symbol-tree@3.2.4: {} - tailwindcss@4.2.4: {} + tailwindcss@4.3.3: {} - tapable@2.3.2: {} + tapable@2.3.3: {} - tinyglobby@0.2.16: + tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tippy.js@6.3.7: dependencies: @@ -4007,7 +4052,7 @@ snapshots: uc.micro@2.1.0: {} - undici-types@7.19.2: {} + undici-types@8.3.0: {} undici@7.25.0: {} @@ -4075,21 +4120,21 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1): + vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0): dependencies: lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.12 - rolldown: 1.0.0-rc.17 - tinyglobby: 0.2.16 + picomatch: 4.0.5 + postcss: 8.5.20 + rolldown: 1.1.5 + tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.6.0 + '@types/node': 26.1.1 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 2.7.0 - vitefu@1.1.3(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1)): + vitefu@1.1.3(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)): optionalDependencies: - vite: 8.0.10(@types/node@25.6.0)(jiti@2.6.1) + vite: 8.1.5(@types/node@26.1.1)(jiti@2.7.0) w3c-keyname@2.2.8: {} diff --git a/ui/src/api.ts b/ui/src/api.ts index a4666a1..cce63d6 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -1,8 +1,9 @@ import { GlobalStateStore } from './shellState'; +import { currentTenantID, tenantScopeHeaders } from './tenantScope'; function authHeaders(): HeadersInit { const token = GlobalStateStore.getState().session.authToken; - return token ? { Authorization: `Bearer ${token}` } : {}; + return { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...tenantScopeHeaders() }; } type ResolveSpecResponse = { @@ -18,6 +19,62 @@ type ResolveSpecFilter = { value?: unknown; }; +const TENANT_OWNED_RESOLVE_SPEC_ENTITIES = new Set([ + 'projects', + 'thoughts', + 'learnings', + 'plans', + 'stored_files', + 'agent_skills', + 'agent_guardrails', + 'agent_personas', + 'agent_parts', + 'agent_traits', + 'character_arcs', + 'chat_histories' +]); + +function resolveSpecEntity(path: string): string | null { + const match = path.match(/^\/api\/rs\/public\/([^/?]+)/); + return match?.[1] ?? null; +} + +function tenantScopedResolveSpecPayload( + path: string, + operation: 'read' | 'create' | 'update' | 'delete', + payload?: { data?: unknown; options?: unknown } +): { data?: unknown; options?: unknown } | undefined { + const entity = resolveSpecEntity(path); + const tenantID = currentTenantID(); + if (!entity || !tenantID || !TENANT_OWNED_RESOLVE_SPEC_ENTITIES.has(entity)) return payload; + + if (operation === 'create') { + const data = payload?.data; + return { + ...payload, + // A selected tenant is authoritative; do not permit a form value to + // accidentally create data in a different tenant. + data: data && typeof data === 'object' && !Array.isArray(data) ? { ...data, tenant_id: tenantID } : data + }; + } + + const existingOptions = payload?.options && typeof payload.options === 'object' && !Array.isArray(payload.options) + ? payload.options as Record + : {}; + const filters = Array.isArray(existingOptions.filters) + ? existingOptions.filters.filter((filter): filter is ResolveSpecFilter => + Boolean(filter) && typeof filter === 'object' && (filter as ResolveSpecFilter).column !== 'tenant_id') + : []; + + return { + ...payload, + options: { + ...existingOptions, + filters: [...filters, { column: 'tenant_id', operator: 'eq', value: tenantID }] + } + }; +} + function normalizeTags(value: unknown): string[] { if (Array.isArray(value)) { return value.map((tag) => String(tag).trim()).filter(Boolean); @@ -70,18 +127,29 @@ async function del(path: string): Promise { if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); } +async function patch(path: string, body: unknown): Promise { + const res = await fetch(path, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json', ...authHeaders() }, + body: JSON.stringify(body) + }); + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); + return res.json() as Promise; +} + async function rsCall( path: string, operation: 'read' | 'create' | 'update' | 'delete', payload?: { data?: unknown; options?: unknown } ): Promise { + const scopedPayload = tenantScopedResolveSpecPayload(path, operation, payload); const res = await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders() }, body: JSON.stringify({ operation, - ...(payload?.data !== undefined ? { data: payload.data } : {}), - ...(payload?.options !== undefined ? { options: payload.options } : {}) + ...(scopedPayload?.data !== undefined ? { data: scopedPayload.data } : {}), + ...(scopedPayload?.options !== undefined ? { options: scopedPayload.options } : {}) }) }); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); @@ -333,6 +401,19 @@ export const api = { dry_run: input?.dry_run ?? false }) }, + identity: { + get: () => get('/api/admin/identity'), + createTenant: (name: string) => + post('/api/admin/identity/tenants', { name }), + adoptLegacy: (id: string) => + post(`/api/admin/identity/tenants/${id}/adopt-legacy`, {}), + createUser: (data: { tenant_id: string; name: string; email: string }) => + post('/api/admin/identity/users', data), + createKey: (data: { tenant_id: string; user_id?: string; description: string }) => + post<{ key: import('./types').IdentityKey; secret: string }>('/api/admin/identity/keys', data), + updateKey: (id: string, data: { tenant_id: string; user_id?: string | null; description?: string; enabled?: boolean }) => + patch(`/api/admin/identity/keys/${id}`, data) + }, plans: { list: async (params?: { status?: string; priority?: string; project_id?: string; limit?: number }) => { const filters: ResolveSpecFilter[] = []; diff --git a/ui/src/components/identity/IdentityPage.svelte b/ui/src/components/identity/IdentityPage.svelte new file mode 100644 index 0000000..8d19388 --- /dev/null +++ b/ui/src/components/identity/IdentityPage.svelte @@ -0,0 +1,196 @@ + + +
+
+
+

Identity

+

Group multiple API keys under a tenant, optionally assigning each key to a user.

+
+ +
+ + {#if error}
{error}
{/if} + {#if message}
{message}
{/if} + {#if revealedSecret} +
+

New API key secret

+

Copy this value now. It is shown only once.

+ {revealedSecret} +
+ {/if} + +
+
{ event.preventDefault(); void createTenant(); }}> +

New tenant

+ + + +
+ +
{ event.preventDefault(); void createUser(); }}> +

New user

+ + + + + +
+ +
{ event.preventDefault(); void createKey(); }}> +

New managed key

+ + + + + + +
+
+ + {#if loading} +
Loading identity data…
+ {:else} +
+

Tenants ({identity.tenants.length})

+

Adopting legacy data is explicit: it assigns every unassigned pre-tenancy record to one tenant.

+
{#each identity.tenants as tenant}{:else}{/each}
NameUsersCreated
{tenant.name}
{tenant.id}
{usersForTenant(tenant.id).length}{formatDate(tenant.created_at)}
No tenants yet.
+
+ +
+

API keys ({identity.keys.length})

+

Configured keys can be reassigned here; their secret values are never displayed.

+
{#each identity.keys as key}{:else}{/each}
KeyTenantUserDescriptionStatusCreated
{key.id}
{key.source}
void updateKey(key, 'description', event.currentTarget.value)} disabled={busy} />{formatDate(key.created_at)}
No keys available.
+
+ {/if} +
diff --git a/ui/src/components/shared/BooleanStatusBadge.svelte b/ui/src/components/shared/BooleanStatusBadge.svelte new file mode 100644 index 0000000..67ef3a0 --- /dev/null +++ b/ui/src/components/shared/BooleanStatusBadge.svelte @@ -0,0 +1,14 @@ + + +{#if !value} + + {falseLabel} + +{/if} diff --git a/ui/src/components/shell/AdminShell.svelte b/ui/src/components/shell/AdminShell.svelte index df07143..bc5ee94 100644 --- a/ui/src/components/shell/AdminShell.svelte +++ b/ui/src/components/shell/AdminShell.svelte @@ -10,7 +10,10 @@ import ProjectsPage from '../projects/ProjectsPage.svelte'; import SkillsPage from '../skills/SkillsPage.svelte'; import ThoughtsPage from '../thoughts/ThoughtsPage.svelte'; + import IdentityPage from '../identity/IdentityPage.svelte'; import AppSidebar from './AppSidebar.svelte'; + import { fromStore } from 'svelte/store'; + import { selectedTenantID } from '../../tenantScope'; const { currentPage, @@ -29,14 +32,19 @@ onnavigate: (page: ShellPage) => void; onrefresh: () => void; } = $props(); + + const tenantScope = fromStore(selectedTenantID);
+ {#key tenantScope.current} {#if currentPage === 'dashboard'} + {:else if currentPage === 'identity'} + {:else if currentPage === 'projects'} {:else if currentPage === 'thoughts'} @@ -56,5 +64,6 @@ {:else if currentPage === 'maintenance'} {/if} + {/key}
diff --git a/ui/src/components/shell/AppSidebar.svelte b/ui/src/components/shell/AppSidebar.svelte index fd2446f..3f03d3c 100644 --- a/ui/src/components/shell/AppSidebar.svelte +++ b/ui/src/components/shell/AppSidebar.svelte @@ -1,5 +1,12 @@