mirror of
https://github.com/hanzoai/bot-hub.git
synced 2026-10-30 18:46:42 +00:00
Every route mounts under /v1/skills, the address api.hanzo.ai routes to this API; auth, personas, users, tokens, upload, integrations, search and whoami are sub-resources and RESERVED keeps them from being slugs. The web, the OG renderers, the CLI's default registry, /.well-known/bothub.json and openapi.json name api.hanzo.ai. Sign-in calls back to api.hanzo.ai/v1/skills/auth/callback and hands the token over in the fragment. The store starts again. docker-entrypoint.sh read Base's port from BASE_PORT, which Kubernetes sets to tcp://<ip>:<port> in a namespace holding a Service named base; Base now listens on 127.0.0.1:8090, fixed. The collections migration is rewritten for the Base the Dockerfile pins (1.5.91): flat field options, declared created/updated, and the users collection every Base already has, extended rather than created twice. Reads are anonymous against public rules, since Base takes IAM credentials only and no longer serves password sign-in; writes answer 503 saying so. The catalogue migration, generated by scripts/catalog.mjs, publishes the 83 skills in hanzoai/bot@8be8f1dc7b. /health reports the store, so the probes restart a pod whose Base is gone. cache.ts decides caching once: an anonymous 2xx GET is public for a minute (five at the edge) with a weak ETag and 304s; a credential, a write or an error is private, no-store. Filter values go through lit(). Embeddings go through api.hanzo.ai and are skipped without a key. k8s/ was a second declaration of the deployment; universe is the one.
826 lines
38 KiB
JavaScript
826 lines
38 KiB
JavaScript
/// <reference path="../base_/data/types.d.ts" />
|
|
|
|
// Bot Hub collections: the hub's tables, on the users collection every Base has.
|
|
// A collection declares its created and updated stamps; Base adds neither.
|
|
// Run: ./base serve --migrationsDir ./hz_migrations
|
|
|
|
migrate((app) => {
|
|
// ─── Users ─────────────────────────────────────────────────────────────────
|
|
// Every Base carries an auth collection named users; the hub adds its
|
|
// profile fields to it. Sign-in is Hanzo IAM's, so no password or OAuth2
|
|
// setting is touched here.
|
|
const users = app.findCollectionByNameOrId("users")
|
|
for (const field of [
|
|
{ name: "handle", type: "text", max: 64 },
|
|
{ name: "displayName", type: "text", max: 256 },
|
|
{ name: "bio", type: "text", max: 2000 },
|
|
{ name: "role", type: "select", values: ["user","moderator","admin"], maxSelect: 1 },
|
|
{ name: "image", type: "url" },
|
|
{ name: "githubId", type: "text", max: 64 },
|
|
{ name: "githubCreatedAt", type: "date" },
|
|
{ name: "githubFetchedAt", type: "date" },
|
|
{ name: "githubProfileSyncedAt", type: "date" },
|
|
{ name: "trustedPublisher", type: "bool" },
|
|
{ name: "deactivatedAt", type: "date" },
|
|
{ name: "purgedAt", type: "date" },
|
|
{ name: "deletedAt", type: "date" },
|
|
{ name: "banReason", type: "text" },
|
|
{ name: "phone", type: "text", max: 32 },
|
|
]) {
|
|
users.fields.add(new Field(field))
|
|
}
|
|
users.indexes = users.indexes.concat([
|
|
"CREATE UNIQUE INDEX idx_users_handle ON users (handle) WHERE handle != ''",
|
|
"CREATE UNIQUE INDEX idx_users_github ON users (githubId) WHERE githubId != ''",
|
|
])
|
|
users.listRule = ""
|
|
users.viewRule = ""
|
|
users.updateRule = 'id = @request.auth.id || @request.auth.role = "admin"'
|
|
users.deleteRule = '@request.auth.role = "admin"'
|
|
app.save(users)
|
|
|
|
// ─── Skills ────────────────────────────────────────────────────────────────
|
|
const skills = new Collection({
|
|
id: "hub_skills",
|
|
name: "skills",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "slug", type: "text", max: 128 },
|
|
{ name: "displayName", type: "text", max: 256 },
|
|
{ name: "summary", type: "text", max: 4000 },
|
|
{ name: "resourceId", type: "text" },
|
|
{ name: "ownerUserId", type: "relation", collectionId: users.id, maxSelect: 1 },
|
|
{ name: "canonicalSkillId", type: "text" },
|
|
{ name: "forkOf", type: "json" },
|
|
{ name: "latestVersionId", type: "text" },
|
|
{ name: "tags", type: "json" },
|
|
{ name: "softDeletedAt", type: "date" },
|
|
{ name: "badges", type: "json" },
|
|
{ name: "moderationStatus", type: "select", values: ["active","hidden","removed"], maxSelect: 1 },
|
|
{ name: "moderationNotes", type: "text" },
|
|
{ name: "moderationReason", type: "text" },
|
|
{ name: "quality", type: "json" },
|
|
{ name: "moderationFlags", type: "json" },
|
|
{ name: "lastReviewedAt", type: "date" },
|
|
{ name: "scanLastCheckedAt", type: "date" },
|
|
{ name: "scanCheckCount", type: "number", min: 0 },
|
|
{ name: "hiddenAt", type: "date" },
|
|
{ name: "hiddenBy", type: "text" },
|
|
{ name: "reportCount", type: "number", min: 0 },
|
|
{ name: "lastReportedAt", type: "date" },
|
|
{ name: "batch", type: "text" },
|
|
{ name: "statsDownloads", type: "number", min: 0 },
|
|
{ name: "statsStars", type: "number", min: 0 },
|
|
{ name: "statsInstallsCurrent", type: "number", min: 0 },
|
|
{ name: "statsInstallsAllTime", type: "number", min: 0 },
|
|
{ name: "statsVersions", type: "number", min: 0 },
|
|
{ name: "statsComments", type: "number", min: 0 },
|
|
],
|
|
indexes: [
|
|
"CREATE UNIQUE INDEX idx_skills_slug ON skills (slug)",
|
|
"CREATE INDEX idx_skills_owner ON skills (ownerUserId)",
|
|
"CREATE INDEX idx_skills_updated ON skills (updated)",
|
|
"CREATE INDEX idx_skills_downloads ON skills (statsDownloads, updated)",
|
|
"CREATE INDEX idx_skills_stars ON skills (statsStars, updated)",
|
|
"CREATE INDEX idx_skills_batch ON skills (batch)",
|
|
],
|
|
listRule: "",
|
|
viewRule: "",
|
|
createRule: '@request.auth.id != ""',
|
|
updateRule: 'ownerUserId = @request.auth.id || @request.auth.role = "admin"',
|
|
deleteRule: 'ownerUserId = @request.auth.id || @request.auth.role = "admin"',
|
|
})
|
|
app.save(skills)
|
|
|
|
// ─── Skill Versions ────────────────────────────────────────────────────────
|
|
const skillVersions = new Collection({
|
|
id: "hub_skill_versions",
|
|
name: "skill_versions",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "skillId", type: "relation", collectionId: "hub_skills", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "version", type: "text", max: 64 },
|
|
{ name: "fingerprint", type: "text" },
|
|
{ name: "changelog", type: "text" },
|
|
{ name: "changelogSource", type: "select", values: ["auto","user"], maxSelect: 1 },
|
|
{ name: "files", type: "json" },
|
|
{ name: "parsed", type: "json" },
|
|
{ name: "createdBy", type: "relation", collectionId: users.id, maxSelect: 1 },
|
|
{ name: "sha256hash", type: "text" },
|
|
{ name: "vtAnalysis", type: "json" },
|
|
{ name: "llmAnalysis", type: "json" },
|
|
{ name: "softDeletedAt", type: "date" },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_sv_skill ON skill_versions (skillId)",
|
|
"CREATE UNIQUE INDEX idx_sv_skill_version ON skill_versions (skillId, version)",
|
|
"CREATE INDEX idx_sv_sha256 ON skill_versions (sha256hash)",
|
|
],
|
|
listRule: "",
|
|
viewRule: "",
|
|
createRule: '@request.auth.id != ""',
|
|
updateRule: '@request.auth.role = "admin"',
|
|
deleteRule: '@request.auth.role = "admin"',
|
|
})
|
|
app.save(skillVersions)
|
|
|
|
// ─── Personas ──────────────────────────────────────────────────────────────
|
|
const personas = new Collection({
|
|
id: "hub_personas",
|
|
name: "personas",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "slug", type: "text", max: 128 },
|
|
{ name: "displayName", type: "text", max: 256 },
|
|
{ name: "summary", type: "text", max: 4000 },
|
|
{ name: "ownerUserId", type: "relation", collectionId: users.id, maxSelect: 1 },
|
|
{ name: "latestVersionId", type: "text" },
|
|
{ name: "tags", type: "json" },
|
|
{ name: "softDeletedAt", type: "date" },
|
|
{ name: "statsDownloads", type: "number", min: 0 },
|
|
{ name: "statsStars", type: "number", min: 0 },
|
|
{ name: "statsVersions", type: "number", min: 0 },
|
|
{ name: "statsComments", type: "number", min: 0 },
|
|
],
|
|
indexes: [
|
|
"CREATE UNIQUE INDEX idx_personas_slug ON personas (slug)",
|
|
"CREATE INDEX idx_personas_owner ON personas (ownerUserId)",
|
|
"CREATE INDEX idx_personas_updated ON personas (updated)",
|
|
],
|
|
listRule: "",
|
|
viewRule: "",
|
|
createRule: '@request.auth.id != ""',
|
|
updateRule: 'ownerUserId = @request.auth.id || @request.auth.role = "admin"',
|
|
deleteRule: 'ownerUserId = @request.auth.id || @request.auth.role = "admin"',
|
|
})
|
|
app.save(personas)
|
|
|
|
// ─── Persona Versions ──────────────────────────────────────────────────────
|
|
const personaVersions = new Collection({
|
|
id: "hub_persona_versions",
|
|
name: "persona_versions",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "personaId", type: "relation", collectionId: "hub_personas", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "version", type: "text", max: 64 },
|
|
{ name: "fingerprint", type: "text" },
|
|
{ name: "changelog", type: "text" },
|
|
{ name: "changelogSource", type: "select", values: ["auto","user"], maxSelect: 1 },
|
|
{ name: "files", type: "json" },
|
|
{ name: "parsed", type: "json" },
|
|
{ name: "createdBy", type: "relation", collectionId: users.id, maxSelect: 1 },
|
|
{ name: "softDeletedAt", type: "date" },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_pv_persona ON persona_versions (personaId)",
|
|
"CREATE UNIQUE INDEX idx_pv_persona_version ON persona_versions (personaId, version)",
|
|
],
|
|
listRule: "",
|
|
viewRule: "",
|
|
createRule: '@request.auth.id != ""',
|
|
updateRule: '@request.auth.role = "admin"',
|
|
deleteRule: '@request.auth.role = "admin"',
|
|
})
|
|
app.save(personaVersions)
|
|
|
|
// ─── Comments ──────────────────────────────────────────────────────────────
|
|
const comments = new Collection({
|
|
id: "hub_comments",
|
|
name: "comments",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "skillId", type: "relation", collectionId: "hub_skills", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "userId", type: "relation", collectionId: users.id, maxSelect: 1 },
|
|
{ name: "body", type: "text" },
|
|
{ name: "softDeletedAt", type: "date" },
|
|
{ name: "deletedBy", type: "text" },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_comments_skill ON comments (skillId)",
|
|
"CREATE INDEX idx_comments_user ON comments (userId)",
|
|
],
|
|
listRule: "",
|
|
viewRule: "",
|
|
createRule: '@request.auth.id != ""',
|
|
updateRule: 'userId = @request.auth.id || @request.auth.role = "admin"',
|
|
deleteRule: 'userId = @request.auth.id || @request.auth.role = "admin"',
|
|
})
|
|
app.save(comments)
|
|
|
|
// ─── Persona Comments ─────────────────────────────────────────────────────
|
|
const personaComments = new Collection({
|
|
id: "hub_persona_comments",
|
|
name: "persona_comments",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "personaId", type: "relation", collectionId: "hub_personas", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "userId", type: "relation", collectionId: users.id, maxSelect: 1 },
|
|
{ name: "body", type: "text" },
|
|
{ name: "softDeletedAt", type: "date" },
|
|
{ name: "deletedBy", type: "text" },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_pc_persona ON persona_comments (personaId)",
|
|
"CREATE INDEX idx_pc_user ON persona_comments (userId)",
|
|
],
|
|
listRule: "",
|
|
viewRule: "",
|
|
createRule: '@request.auth.id != ""',
|
|
updateRule: 'userId = @request.auth.id || @request.auth.role = "admin"',
|
|
deleteRule: 'userId = @request.auth.id || @request.auth.role = "admin"',
|
|
})
|
|
app.save(personaComments)
|
|
|
|
// ─── Stars ─────────────────────────────────────────────────────────────────
|
|
const stars = new Collection({
|
|
id: "hub_stars",
|
|
name: "stars",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "skillId", type: "relation", collectionId: "hub_skills", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "userId", type: "relation", collectionId: users.id, maxSelect: 1 },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_stars_skill ON stars (skillId)",
|
|
"CREATE INDEX idx_stars_user ON stars (userId)",
|
|
"CREATE UNIQUE INDEX idx_stars_skill_user ON stars (skillId, userId)",
|
|
],
|
|
listRule: "",
|
|
viewRule: "",
|
|
createRule: '@request.auth.id != ""',
|
|
updateRule: null,
|
|
deleteRule: 'userId = @request.auth.id',
|
|
})
|
|
app.save(stars)
|
|
|
|
// ─── Persona Stars ────────────────────────────────────────────────────────
|
|
const personaStars = new Collection({
|
|
id: "hub_persona_stars",
|
|
name: "persona_stars",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "personaId", type: "relation", collectionId: "hub_personas", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "userId", type: "relation", collectionId: users.id, maxSelect: 1 },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_ps_persona ON persona_stars (personaId)",
|
|
"CREATE INDEX idx_ps_user ON persona_stars (userId)",
|
|
"CREATE UNIQUE INDEX idx_ps_persona_user ON persona_stars (personaId, userId)",
|
|
],
|
|
listRule: "",
|
|
viewRule: "",
|
|
createRule: '@request.auth.id != ""',
|
|
updateRule: null,
|
|
deleteRule: 'userId = @request.auth.id',
|
|
})
|
|
app.save(personaStars)
|
|
|
|
// ─── Skill Reports ────────────────────────────────────────────────────────
|
|
const skillReports = new Collection({
|
|
id: "hub_skill_reports",
|
|
name: "skill_reports",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "skillId", type: "relation", collectionId: "hub_skills", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "userId", type: "relation", collectionId: users.id, maxSelect: 1 },
|
|
{ name: "reason", type: "text" },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_sr_skill ON skill_reports (skillId)",
|
|
"CREATE UNIQUE INDEX idx_sr_skill_user ON skill_reports (skillId, userId)",
|
|
],
|
|
listRule: '@request.auth.role = "admin"',
|
|
viewRule: '@request.auth.role = "admin"',
|
|
createRule: '@request.auth.id != ""',
|
|
updateRule: null,
|
|
deleteRule: '@request.auth.role = "admin"',
|
|
})
|
|
app.save(skillReports)
|
|
|
|
// ─── Skill Embeddings ─────────────────────────────────────────────────────
|
|
const skillEmbeddings = new Collection({
|
|
id: "hub_skill_embeddings",
|
|
name: "skill_embeddings",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "skillId", type: "relation", collectionId: "hub_skills", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "versionId", type: "relation", collectionId: "hub_skill_versions", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "ownerId", type: "relation", collectionId: users.id, maxSelect: 1 },
|
|
{ name: "embedding", type: "json" },
|
|
{ name: "isLatest", type: "bool" },
|
|
{ name: "isApproved", type: "bool" },
|
|
{ name: "visibility", type: "select", values: ["latest","latest-approved","all"], maxSelect: 1 },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_se_skill ON skill_embeddings (skillId)",
|
|
"CREATE INDEX idx_se_version ON skill_embeddings (versionId)",
|
|
],
|
|
listRule: null,
|
|
viewRule: null,
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
})
|
|
app.save(skillEmbeddings)
|
|
|
|
// ─── Persona Embeddings ───────────────────────────────────────────────────
|
|
const personaEmbeddings = new Collection({
|
|
id: "hub_persona_embeddings",
|
|
name: "persona_embeddings",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "personaId", type: "relation", collectionId: "hub_personas", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "versionId", type: "relation", collectionId: "hub_persona_versions", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "ownerId", type: "relation", collectionId: users.id, maxSelect: 1 },
|
|
{ name: "embedding", type: "json" },
|
|
{ name: "isLatest", type: "bool" },
|
|
{ name: "isApproved", type: "bool" },
|
|
{ name: "visibility", type: "select", values: ["latest","latest-approved","all"], maxSelect: 1 },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_pe_persona ON persona_embeddings (personaId)",
|
|
"CREATE INDEX idx_pe_version ON persona_embeddings (versionId)",
|
|
],
|
|
listRule: null,
|
|
viewRule: null,
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
})
|
|
app.save(personaEmbeddings)
|
|
|
|
// ─── Skill Badges ─────────────────────────────────────────────────────────
|
|
const skillBadges = new Collection({
|
|
id: "hub_skill_badges",
|
|
name: "skill_badges",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "skillId", type: "relation", collectionId: "hub_skills", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "kind", type: "select", values: ["highlighted","official","deprecated","redactionApproved"], maxSelect: 1 },
|
|
{ name: "byUserId", type: "relation", collectionId: users.id, maxSelect: 1 },
|
|
{ name: "at", type: "date" },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_sb_skill ON skill_badges (skillId)",
|
|
"CREATE INDEX idx_sb_skill_kind ON skill_badges (skillId, kind)",
|
|
],
|
|
listRule: "",
|
|
viewRule: "",
|
|
createRule: '@request.auth.role = "admin"',
|
|
updateRule: '@request.auth.role = "admin"',
|
|
deleteRule: '@request.auth.role = "admin"',
|
|
})
|
|
app.save(skillBadges)
|
|
|
|
// ─── API Tokens ───────────────────────────────────────────────────────────
|
|
const apiTokens = new Collection({
|
|
id: "hub_api_tokens",
|
|
name: "api_tokens",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "userId", type: "relation", collectionId: users.id, maxSelect: 1, cascadeDelete: true },
|
|
{ name: "label", type: "text" },
|
|
{ name: "prefix", type: "text", max: 16 },
|
|
{ name: "tokenHash", type: "text" },
|
|
{ name: "lastUsedAt", type: "date" },
|
|
{ name: "revokedAt", type: "date" },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_at_user ON api_tokens (userId)",
|
|
"CREATE UNIQUE INDEX idx_at_hash ON api_tokens (tokenHash)",
|
|
],
|
|
listRule: 'userId = @request.auth.id',
|
|
viewRule: 'userId = @request.auth.id',
|
|
createRule: '@request.auth.id != ""',
|
|
updateRule: 'userId = @request.auth.id',
|
|
deleteRule: 'userId = @request.auth.id',
|
|
})
|
|
app.save(apiTokens)
|
|
|
|
// ─── Skill Daily Stats ────────────────────────────────────────────────────
|
|
const skillDailyStats = new Collection({
|
|
id: "hub_skill_daily_stats",
|
|
name: "skill_daily_stats",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "skillId", type: "relation", collectionId: "hub_skills", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "day", type: "number" },
|
|
{ name: "downloads", type: "number", min: 0 },
|
|
{ name: "installs", type: "number", min: 0 },
|
|
],
|
|
indexes: [
|
|
"CREATE UNIQUE INDEX idx_sds_skill_day ON skill_daily_stats (skillId, day)",
|
|
"CREATE INDEX idx_sds_day ON skill_daily_stats (day)",
|
|
],
|
|
listRule: "",
|
|
viewRule: "",
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
})
|
|
app.save(skillDailyStats)
|
|
|
|
// ─── Skill Leaderboards ───────────────────────────────────────────────────
|
|
const skillLeaderboards = new Collection({
|
|
id: "hub_skill_leaderboards",
|
|
name: "skill_leaderboards",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "kind", type: "text", max: 32 },
|
|
{ name: "generatedAt", type: "date" },
|
|
{ name: "rangeStartDay", type: "number" },
|
|
{ name: "rangeEndDay", type: "number" },
|
|
{ name: "items", type: "json" },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_sl_kind ON skill_leaderboards (kind, generatedAt)",
|
|
],
|
|
listRule: "",
|
|
viewRule: "",
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
})
|
|
app.save(skillLeaderboards)
|
|
|
|
// ─── Skill Stat Events ────────────────────────────────────────────────────
|
|
const skillStatEvents = new Collection({
|
|
id: "hub_skill_stat_events",
|
|
name: "skill_stat_events",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "skillId", type: "relation", collectionId: "hub_skills", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "kind", type: "text", max: 32 },
|
|
{ name: "delta", type: "json" },
|
|
{ name: "occurredAt", type: "date" },
|
|
{ name: "processedAt", type: "date" },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_sse_unprocessed ON skill_stat_events (processedAt)",
|
|
"CREATE INDEX idx_sse_skill ON skill_stat_events (skillId)",
|
|
],
|
|
listRule: null,
|
|
viewRule: null,
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
})
|
|
app.save(skillStatEvents)
|
|
|
|
// ─── Audit Logs ───────────────────────────────────────────────────────────
|
|
const auditLogs = new Collection({
|
|
id: "hub_audit_logs",
|
|
name: "audit_logs",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "actorUserId", type: "relation", collectionId: users.id, maxSelect: 1 },
|
|
{ name: "action", type: "text", max: 64 },
|
|
{ name: "targetType", type: "text", max: 32 },
|
|
{ name: "targetId", type: "text" },
|
|
{ name: "metadata", type: "json" },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_al_actor ON audit_logs (actorUserId)",
|
|
"CREATE INDEX idx_al_target ON audit_logs (targetType, targetId)",
|
|
],
|
|
listRule: '@request.auth.role = "admin"',
|
|
viewRule: '@request.auth.role = "admin"',
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
})
|
|
app.save(auditLogs)
|
|
|
|
// ─── VT Scan Logs ────────────────────────────────────────────────────────
|
|
const vtScanLogs = new Collection({
|
|
id: "hub_vt_scan_logs",
|
|
name: "vt_scan_logs",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "scanType", type: "select", values: ["daily_rescan","backfill","pending_poll"], maxSelect: 1 },
|
|
{ name: "total", type: "number" },
|
|
{ name: "scanUpdated", type: "number" },
|
|
{ name: "unchanged", type: "number" },
|
|
{ name: "errors", type: "number" },
|
|
{ name: "flaggedSkills", type: "json" },
|
|
{ name: "durationMs", type: "number" },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_vsl_type ON vt_scan_logs (scanType, created)",
|
|
],
|
|
listRule: '@request.auth.role = "admin"',
|
|
viewRule: '@request.auth.role = "admin"',
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
})
|
|
app.save(vtScanLogs)
|
|
|
|
// ─── Download Dedupes ─────────────────────────────────────────────────────
|
|
const downloadDedupes = new Collection({
|
|
id: "hub_download_dedupes",
|
|
name: "download_dedupes",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "skillId", type: "relation", collectionId: "hub_skills", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "identityHash", type: "text" },
|
|
{ name: "hourStart", type: "date" },
|
|
],
|
|
indexes: [
|
|
"CREATE UNIQUE INDEX idx_dd_skill_hash_hour ON download_dedupes (skillId, identityHash, hourStart)",
|
|
"CREATE INDEX idx_dd_hour ON download_dedupes (hourStart)",
|
|
],
|
|
listRule: null,
|
|
viewRule: null,
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
})
|
|
app.save(downloadDedupes)
|
|
|
|
// ─── Reserved Slugs ───────────────────────────────────────────────────────
|
|
const reservedSlugs = new Collection({
|
|
id: "hub_reserved_slugs",
|
|
name: "reserved_slugs",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "slug", type: "text", max: 128 },
|
|
{ name: "originalOwnerUserId", type: "relation", collectionId: users.id, maxSelect: 1 },
|
|
{ name: "deletedAt", type: "date" },
|
|
{ name: "expiresAt", type: "date" },
|
|
{ name: "reason", type: "text" },
|
|
{ name: "releasedAt", type: "date" },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_rs_slug ON reserved_slugs (slug)",
|
|
"CREATE INDEX idx_rs_expiry ON reserved_slugs (expiresAt)",
|
|
],
|
|
listRule: '@request.auth.role = "admin"',
|
|
viewRule: '@request.auth.role = "admin"',
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
})
|
|
app.save(reservedSlugs)
|
|
|
|
// ─── Skill Version Fingerprints ───────────────────────────────────────────
|
|
const skillVersionFingerprints = new Collection({
|
|
id: "hub_svf",
|
|
name: "skill_version_fingerprints",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "skillId", type: "relation", collectionId: "hub_skills", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "versionId", type: "relation", collectionId: "hub_skill_versions", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "fingerprint", type: "text" },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_svf_version ON skill_version_fingerprints (versionId)",
|
|
"CREATE INDEX idx_svf_fp ON skill_version_fingerprints (fingerprint)",
|
|
"CREATE INDEX idx_svf_skill_fp ON skill_version_fingerprints (skillId, fingerprint)",
|
|
],
|
|
listRule: null,
|
|
viewRule: null,
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
})
|
|
app.save(skillVersionFingerprints)
|
|
|
|
// ─── Persona Version Fingerprints ─────────────────────────────────────────
|
|
const personaVersionFingerprints = new Collection({
|
|
id: "hub_pvf",
|
|
name: "persona_version_fingerprints",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "personaId", type: "relation", collectionId: "hub_personas", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "versionId", type: "relation", collectionId: "hub_persona_versions", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "fingerprint", type: "text" },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_pvf_version ON persona_version_fingerprints (versionId)",
|
|
"CREATE INDEX idx_pvf_fp ON persona_version_fingerprints (fingerprint)",
|
|
"CREATE INDEX idx_pvf_persona_fp ON persona_version_fingerprints (personaId, fingerprint)",
|
|
],
|
|
listRule: null,
|
|
viewRule: null,
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
})
|
|
app.save(personaVersionFingerprints)
|
|
|
|
// ─── User Sync Roots ──────────────────────────────────────────────────────
|
|
const userSyncRoots = new Collection({
|
|
id: "hub_user_sync_roots",
|
|
name: "user_sync_roots",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "userId", type: "relation", collectionId: users.id, maxSelect: 1, cascadeDelete: true },
|
|
{ name: "rootId", type: "text" },
|
|
{ name: "label", type: "text" },
|
|
{ name: "firstSeenAt", type: "date" },
|
|
{ name: "lastSeenAt", type: "date" },
|
|
{ name: "expiredAt", type: "date" },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_usr_user ON user_sync_roots (userId)",
|
|
"CREATE UNIQUE INDEX idx_usr_user_root ON user_sync_roots (userId, rootId)",
|
|
],
|
|
listRule: 'userId = @request.auth.id',
|
|
viewRule: 'userId = @request.auth.id',
|
|
createRule: '@request.auth.id != ""',
|
|
updateRule: 'userId = @request.auth.id',
|
|
deleteRule: 'userId = @request.auth.id',
|
|
})
|
|
app.save(userSyncRoots)
|
|
|
|
// ─── User Skill Installs ──────────────────────────────────────────────────
|
|
const userSkillInstalls = new Collection({
|
|
id: "hub_user_skill_installs",
|
|
name: "user_skill_installs",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "userId", type: "relation", collectionId: users.id, maxSelect: 1, cascadeDelete: true },
|
|
{ name: "skillId", type: "relation", collectionId: "hub_skills", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "firstSeenAt", type: "date" },
|
|
{ name: "lastSeenAt", type: "date" },
|
|
{ name: "activeRoots", type: "number", min: 0 },
|
|
{ name: "lastVersion", type: "text", max: 64 },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_usi_user ON user_skill_installs (userId)",
|
|
"CREATE UNIQUE INDEX idx_usi_user_skill ON user_skill_installs (userId, skillId)",
|
|
"CREATE INDEX idx_usi_skill ON user_skill_installs (skillId)",
|
|
],
|
|
listRule: 'userId = @request.auth.id',
|
|
viewRule: 'userId = @request.auth.id',
|
|
createRule: '@request.auth.id != ""',
|
|
updateRule: 'userId = @request.auth.id',
|
|
deleteRule: 'userId = @request.auth.id',
|
|
})
|
|
app.save(userSkillInstalls)
|
|
|
|
// ─── User Skill Root Installs ─────────────────────────────────────────────
|
|
const userSkillRootInstalls = new Collection({
|
|
id: "hub_usri",
|
|
name: "user_skill_root_installs",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "userId", type: "relation", collectionId: users.id, maxSelect: 1, cascadeDelete: true },
|
|
{ name: "rootId", type: "text" },
|
|
{ name: "skillId", type: "relation", collectionId: "hub_skills", maxSelect: 1, cascadeDelete: true },
|
|
{ name: "firstSeenAt", type: "date" },
|
|
{ name: "lastSeenAt", type: "date" },
|
|
{ name: "lastVersion", type: "text", max: 64 },
|
|
{ name: "removedAt", type: "date" },
|
|
],
|
|
indexes: [
|
|
"CREATE INDEX idx_usri_user ON user_skill_root_installs (userId)",
|
|
"CREATE UNIQUE INDEX idx_usri_user_root_skill ON user_skill_root_installs (userId, rootId, skillId)",
|
|
"CREATE INDEX idx_usri_skill ON user_skill_root_installs (skillId)",
|
|
],
|
|
listRule: 'userId = @request.auth.id',
|
|
viewRule: 'userId = @request.auth.id',
|
|
createRule: '@request.auth.id != ""',
|
|
updateRule: 'userId = @request.auth.id',
|
|
deleteRule: 'userId = @request.auth.id',
|
|
})
|
|
app.save(userSkillRootInstalls)
|
|
|
|
// ─── GitHub Backup Sync State ─────────────────────────────────────────────
|
|
const githubBackupSyncState = new Collection({
|
|
id: "hub_github_sync",
|
|
name: "github_backup_sync_state",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "key", type: "text", max: 64 },
|
|
{ name: "cursor", type: "text" },
|
|
],
|
|
indexes: [
|
|
"CREATE UNIQUE INDEX idx_gbss_key ON github_backup_sync_state (key)",
|
|
],
|
|
listRule: null,
|
|
viewRule: null,
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
})
|
|
app.save(githubBackupSyncState)
|
|
|
|
// ─── Skill Stat Cursors ───────────────────────────────────────────────────
|
|
const skillStatCursors = new Collection({
|
|
id: "hub_stat_cursors",
|
|
name: "skill_stat_update_cursors",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "key", type: "text", max: 64 },
|
|
{ name: "cursorCreationTime", type: "date" },
|
|
],
|
|
indexes: [
|
|
"CREATE UNIQUE INDEX idx_ssuc_key ON skill_stat_update_cursors (key)",
|
|
],
|
|
listRule: null,
|
|
viewRule: null,
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
})
|
|
app.save(skillStatCursors)
|
|
|
|
// ─── Skill Stat Backfill State ────────────────────────────────────────────
|
|
const skillStatBackfill = new Collection({
|
|
id: "hub_stat_backfill",
|
|
name: "skill_stat_backfill_state",
|
|
type: "base",
|
|
fields: [
|
|
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
|
{ name: "key", type: "text", max: 64 },
|
|
{ name: "cursor", type: "text" },
|
|
{ name: "doneAt", type: "date" },
|
|
],
|
|
indexes: [
|
|
"CREATE UNIQUE INDEX idx_ssbs_key ON skill_stat_backfill_state (key)",
|
|
],
|
|
listRule: null,
|
|
viewRule: null,
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
})
|
|
app.save(skillStatBackfill)
|
|
|
|
}, (app) => {
|
|
// DOWN migration — delete all hub collections in reverse order
|
|
const names = [
|
|
"skill_stat_backfill_state", "skill_stat_update_cursors",
|
|
"github_backup_sync_state", "user_skill_root_installs",
|
|
"user_skill_installs", "user_sync_roots",
|
|
"persona_version_fingerprints", "skill_version_fingerprints",
|
|
"reserved_slugs", "download_dedupes", "vt_scan_logs",
|
|
"audit_logs", "skill_stat_events", "skill_leaderboards",
|
|
"skill_daily_stats", "api_tokens", "skill_badges",
|
|
"persona_embeddings", "skill_embeddings",
|
|
"persona_stars", "stars", "persona_comments", "comments",
|
|
"skill_reports", "persona_versions", "personas",
|
|
"skill_versions", "skills",
|
|
]
|
|
for (const name of names) {
|
|
try { app.delete(app.findCollectionByNameOrId(name)) } catch {}
|
|
}
|
|
})
|