From 96bbae0d9edd85a4a80e275cbbe467b444e5d4a2 Mon Sep 17 00:00:00 2001 From: Brian Miller Date: Sat, 22 Aug 2026 21:36:36 +0000 Subject: [PATCH] test: guard the entity table against the platform's schema The field table is hand-maintained rather than generated, and the docs have always said it can lag a platform release. What they never said is how you would find out: PocketBase silently discards writes to fields a collection does not have, so a flag for a field the platform removed keeps reporting success and doing nothing. That has already happened here -- `nebula-ca --rotate-keys` existed for a while against a collection with no such field. So the tests now compare the table against a vendored copy of the platform's schema, and fail in both directions: a spec field (or KeyColumn, or LookupKey) the platform does not have, and a platform field the CLI neither exposes nor records a reason for. The second is the one that catches lagging behind a release; the first catches writing into the void. Vendored rather than fetched in CI, deliberately. The drift this guards against is exactly the kind you want to see in a diff and approve, and a stale copy fails loudly in the same way a missing one would -- whereas fetching the platform's default branch would let an unrelated platform commit turn this repo's CI red. `cp ../platform/schema.json cmd/testdata/schema.json` is the refresh, documented in the README and in the test, and schema-source.txt records the version. Two lists carry the judgement, and a third test keeps them from rotting: deliberatelyOmitted, which answers "why is there no flag for this" per field, and libraryOwnedFields, which covers fields pb-nats creates on nats_roles that the platform's schema.json does not declare -- allow_response and its two companions. Worth reading that comment: pb-nats's createRolesCollection returns early when the collection exists, so a database created before those fields were added never acquires them and schema.json will not add them either. Same shape as the system_account_id bug the platform fixed in v0.2.0, flagged upstream. Each check was verified by planting the defect it describes -- a fictional spec field, a new platform field, a stale omission entry, a library-owned field the platform has since declared, a missing collection -- and confirming the failure before reverting. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 15 +- README.md | 23 + cmd/schema_drift_test.go | 289 ++ cmd/testdata/schema-source.txt | 11 + cmd/testdata/schema.json | 4591 ++++++++++++++++++++++++++++++++ 5 files changed, 4928 insertions(+), 1 deletion(-) create mode 100644 cmd/schema_drift_test.go create mode 100644 cmd/testdata/schema-source.txt create mode 100644 cmd/testdata/schema.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b0dad2..d932dd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,20 @@ that period, and this file starts where the versioned releases do. ## [Unreleased] -_Nothing yet._ +### Added + +- **Tests.** The repo had none, so CI's `go test ./...` ran over nothing. The + entity table is now checked for internal consistency — dispatchable field + types, known verbs, a `LookupKey` that names a real field, no flag colliding + with one a command registers itself, no command name or alias claimed twice — + and separately against a **vendored copy of the platform's schema** + (`cmd/testdata/schema.json`), in both directions: a flag for a field the + platform does not have, and a platform field the CLI neither exposes nor + explains. Refresh the copy with + `cp ../platform/schema.json cmd/testdata/schema.json`. + + This closes the drift the docs have always warned about: the field list is + hand-maintained, and until now nothing would tell you it had fallen behind. ## [0.1.0] - 2026-08-22 diff --git a/README.md b/README.md index 78144cf..0ad1cb1 100644 --- a/README.md +++ b/README.md @@ -330,6 +330,29 @@ when you change command shapes that an assistant might rely on. ```sh go build ./... go vet ./... +go test ./... ``` Module path: `github.com/stone-age-io/stone-cli`. Go 1.25+. + +### The entity table is checked against the platform's schema + +The field table in `cmd/entity.go` is hand-maintained, not generated — so the +tests compare it against a **vendored copy** of the platform's collection +schema at `cmd/testdata/schema.json`. They fail in both directions: a flag for a +field the platform does not have (PocketBase discards that write and the command +reports success), and a platform field the CLI neither exposes nor records a +reason for. + +When the platform's schema changes, refresh the copy and run the tests: + +```sh +cp ../platform/schema.json cmd/testdata/schema.json +go test ./... +``` + +A failure then tells you exactly what moved. Add the field to the spec, or add it +to `deliberatelyOmitted` in `cmd/schema_drift_test.go` with the reason — "why is +there no flag for this" is the question that list exists to answer. +`cmd/testdata/schema-source.txt` records which platform version the copy came +from. diff --git a/cmd/schema_drift_test.go b/cmd/schema_drift_test.go new file mode 100644 index 0000000..22f356a --- /dev/null +++ b/cmd/schema_drift_test.go @@ -0,0 +1,289 @@ +package cmd + +import ( + "encoding/json" + "os" + "sort" + "strings" + "testing" +) + +// The CLI's field table is hand-maintained rather than generated from the +// platform's schema.json, and README/docs both say so. What neither said is how +// you would find out it had fallen behind: PocketBase silently discards writes +// to fields a collection does not have, so a flag for a field the platform +// removed keeps reporting success and doing nothing. That has already happened +// once here -- `nebula-ca --rotate-keys` existed for a while against a +// collection with no such field (see the comment on the nebula-ca spec). +// +// So this test compares the table against a vendored copy of the platform's +// schema. Vendored, not fetched: the drift being guarded against is exactly the +// kind you want to see in a diff and approve, and a stale copy fails loudly the +// same way a missing one would. Refresh it with +// +// cp ../platform/schema.json cmd/testdata/schema.json +// +// and read cmd/testdata/schema-source.txt for which platform version it came +// from. + +const ( + vendoredSchema = "testdata/schema.json" + refreshHint = "if the platform changed, refresh with: cp ../platform/schema.json " + vendoredSchema +) + +type schemaCollection struct { + Name string `json:"name"` + Type string `json:"type"` + Fields []struct { + Name string `json:"name"` + Type string `json:"type"` + System bool `json:"system"` + } `json:"fields"` +} + +// neverExposed are fields no entity spec should carry a flag for, whatever the +// collection: PocketBase internals, server-managed timestamps, and the tenancy +// column the CLI injects itself from the active organization. +var neverExposed = map[string]bool{ + "id": true, + "created": true, + "updated": true, + "organization": true, // injected from the active org, never typed by a user + "tokenKey": true, // auth internals + "emailVisibility": true, + "verified": true, +} + +// deliberatelyOmitted records, per collection, the fields the CLI can see and +// chooses not to expose -- with the reason, because "why is there no flag for +// this" is the question this list exists to answer. A platform field that is +// neither exposed nor listed here fails the test, which is the point: that is +// what drift looks like on the day it happens. +var deliberatelyOmitted = map[string][]string{ + // A file upload. The CLI has no multipart path, so floorplans and logos are + // console-only; docs say as much. + "locations": {"floorplan"}, + "organizations": {"logo", "is_system_org", "is_operator_org"}, + + // Secret material and server-derived state. The credential itself is + // readable by the identity that owns it, but nothing types it in. + "nats_users": {"public_key", "private_key", "seed", "jwt", "creds_file", "active"}, + "nebula_hosts": {"certificate", "private_key", "ca_certificate", "config_yaml", "expires_at"}, + "nebula_ca": {"certificate", "private_key"}, + + // Account keys are operator-level, and the two signing-key triggers are a + // route (POST /api/org/nats-account/keys), not fields a tenant writes. + // rotate_keys is listed because pb-nats watches it, not because the CLI + // should offer it. + "nats_accounts": { + "public_key", "private_key", "seed", + "signing_public_key", "signing_private_key", "signing_seed", + "signing_keys", "signing_keys_private", + "add_signing_key", "remove_signing_key", "rotate_keys", + "jwt", "active", "revocations", + }, +} + +// libraryOwnedFields are fields the pb-* libraries create on the collections +// they own, which the platform's schema.json does not declare -- so they are +// absent from the vendored copy while being present on a live database. +// +// pb-nats creates nats_roles with these three (internal/collections/manager.go, +// createRolesCollection) and reads them back when generating a user JWT +// (internal/types/converters.go). The platform's schema.json is a dump that +// predates them, and its import is additive, so it neither declares nor removes +// them. +// +// Worth knowing rather than just working around: createRolesCollection returns +// early when the collection already exists, so a database created before those +// fields were added never acquires them, and schema.json will not add them +// either. This is the same shape as the system_account_id bug the platform fixed +// in v0.2.0. Flagged upstream; the CLI's flags are correct against a current +// database. +var libraryOwnedFields = map[string][]string{ + "nats_roles": {"allow_response", "allow_response_max", "allow_response_ttl"}, +} + +func loadVendoredSchema(t *testing.T) map[string]schemaCollection { + t.Helper() + + raw, err := os.ReadFile(vendoredSchema) + if err != nil { + t.Fatalf("reading %s: %v\n%s", vendoredSchema, err, refreshHint) + } + + var collections []schemaCollection + if err := json.Unmarshal(raw, &collections); err != nil { + t.Fatalf("parsing %s: %v", vendoredSchema, err) + } + + byName := make(map[string]schemaCollection, len(collections)) + for _, c := range collections { + byName[c.Name] = c + } + return byName +} + +func TestSpecFieldsExistInThePlatformSchema(t *testing.T) { + schema := loadVendoredSchema(t) + + for _, spec := range entitySpecs { + t.Run(spec.Name, func(t *testing.T) { + collection, ok := schema[spec.Collection] + if !ok { + t.Fatalf("collection %q is not in the platform schema: every command for this entity would 404.\n%s", + spec.Collection, refreshHint) + } + + known := map[string]bool{} + for _, f := range collection.Fields { + known[f.Name] = true + } + for _, f := range libraryOwnedFields[spec.Collection] { + known[f] = true + } + + for _, f := range spec.Fields { + if !known[f.Name] { + t.Errorf("field %q has a --%s flag but the collection has no such field: PocketBase discards the write and the command reports success.\n%s", + f.Name, f.flagName(), refreshHint) + } + } + + // A column the collection does not have renders empty in every `ls` + // forever, with nothing to indicate why. + for _, col := range spec.KeyColumns { + if !known[col] { + t.Errorf("KeyColumns names %q, which the collection does not have: the column would always render empty.\n%s", col, refreshHint) + } + } + + // resolveRecordID filters on this; a field that is not there makes + // every natural-key lookup an opaque 400. + if spec.LookupKey != "" && !known[spec.LookupKey] { + t.Errorf("LookupKey %q is not a field on the collection.\n%s", spec.LookupKey, refreshHint) + } + }) + } +} + +// The other direction, and the one that actually catches the CLI lagging a +// platform release: a field the platform has that the CLI neither exposes nor +// records a reason for. +func TestPlatformFieldsAreEitherExposedOrExplained(t *testing.T) { + schema := loadVendoredSchema(t) + + for _, spec := range entitySpecs { + t.Run(spec.Name, func(t *testing.T) { + collection, ok := schema[spec.Collection] + if !ok { + t.Skipf("collection %q missing from the vendored schema; covered by the test above", spec.Collection) + } + + // A field counts as accounted for if the CLI can write it, shows it + // as a column (read-only on purpose, e.g. an expiry the server + // computes), or is listed as omitted. + accounted := map[string]bool{} + for _, f := range spec.Fields { + accounted[f.Name] = true + } + for _, c := range spec.KeyColumns { + accounted[c] = true + } + for _, f := range deliberatelyOmitted[spec.Collection] { + accounted[f] = true + } + + var unexplained []string + for _, f := range collection.Fields { + if neverExposed[f.Name] || accounted[f.Name] { + continue + } + unexplained = append(unexplained, f.Name+" ("+f.Type+")") + } + + if len(unexplained) > 0 { + sort.Strings(unexplained) + t.Errorf("the platform has fields this CLI neither exposes nor explains: %s\n"+ + "Either add them to the %s spec, or add them to deliberatelyOmitted[%q] with the reason.", + strings.Join(unexplained, ", "), spec.Name, spec.Collection) + } + }) + } +} + +// Guards the two lists above against rotting in the opposite direction: an +// entry for a field that no longer exists, or one the CLI has since started +// exposing, is stale bookkeeping that hides the next real drift. +func TestOmissionListsHaveNoStaleEntries(t *testing.T) { + schema := loadVendoredSchema(t) + + specByCollection := map[string]EntitySpec{} + for _, s := range entitySpecs { + specByCollection[s.Collection] = s + } + + for collection, omitted := range deliberatelyOmitted { + t.Run(collection, func(t *testing.T) { + c, ok := schema[collection] + if !ok { + t.Fatalf("deliberatelyOmitted names collection %q, which is not in the schema", collection) + } + known := map[string]bool{} + for _, f := range c.Fields { + known[f.Name] = true + } + + spec := specByCollection[collection] + for _, f := range omitted { + if !known[f] { + t.Errorf("listed as omitted but the platform no longer has it: %q -- drop the entry", f) + } + if spec.field(f) != nil { + t.Errorf("listed as omitted but the spec exposes it: %q -- drop the entry", f) + } + } + }) + } + + for collection, owned := range libraryOwnedFields { + t.Run(collection+"/library-owned", func(t *testing.T) { + c, ok := schema[collection] + if !ok { + t.Fatalf("libraryOwnedFields names collection %q, which is not in the schema", collection) + } + for _, f := range owned { + for _, sf := range c.Fields { + if sf.Name == f { + t.Errorf("%q is now declared in the platform's schema.json, so it is no longer library-owned: drop the entry", f) + } + } + } + }) + } +} + +// The vendored file is data the tests above trust completely, so a truncated or +// half-copied schema should fail as itself rather than as twenty confusing +// field errors. +func TestVendoredSchemaLooksComplete(t *testing.T) { + schema := loadVendoredSchema(t) + + // Collections the CLI drives, plus ones it deliberately does not, as a + // shape check on the file rather than a check on the CLI. + for _, want := range []string{"things", "locations", "organizations", "memberships", "nats_users", "nebula_hosts", "leaf_nodes", "audit_logs"} { + if _, ok := schema[want]; !ok { + t.Errorf("collection %q missing: the vendored schema looks partial.\n%s", want, refreshHint) + } + } + + for name, c := range schema { + if len(c.Fields) == 0 { + t.Errorf("collection %q has no fields, which no real collection does", name) + } + } + + if _, err := os.Stat("testdata/schema-source.txt"); err != nil { + t.Errorf("testdata/schema-source.txt is missing: nothing records which platform version the vendored schema came from") + } +} diff --git a/cmd/testdata/schema-source.txt b/cmd/testdata/schema-source.txt new file mode 100644 index 0000000..643e010 --- /dev/null +++ b/cmd/testdata/schema-source.txt @@ -0,0 +1,11 @@ +The platform's collection schema, vendored so the CLI can check its own +field table against it. This file is DATA, not the source of truth -- the source +is the platform repo. + +source: https://github.com/stone-age-io/platform/blob/main/schema.json +platform: v0.2.0 (repo at bc28ab4) +schema.json last changed in platform commit: a4e08b8 +copied: 2026-08-22 + +Refresh with: + cp ../platform/schema.json cmd/testdata/schema.json diff --git a/cmd/testdata/schema.json b/cmd/testdata/schema.json new file mode 100644 index 0000000..77f7c7b --- /dev/null +++ b/cmd/testdata/schema.json @@ -0,0 +1,4591 @@ +[ + { + "id": "pbc_3142635823", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "_superusers", + "type": "auth", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation2078885302", + "maxSelect": 1, + "minSelect": 0, + "name": "current_organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_tokenKey_pbc_3142635823` ON `_superusers` (`tokenKey`)", + "CREATE UNIQUE INDEX `idx_email_pbc_3142635823` ON `_superusers` (`email`) WHERE `email` != ''" + ], + "system": true, + "authRule": "", + "manageRule": null, + "authAlert": { + "enabled": true, + "emailTemplate": { + "subject": "Login from a new location", + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location:

\n

{ALERT_INFO}

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

If this was you, you may disregard this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "oauth2": { + "mappedFields": { + "id": "", + "name": "", + "username": "", + "avatarURL": "" + }, + "enabled": false + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "mfa": { + "enabled": false, + "duration": 1800, + "rule": "" + }, + "otp": { + "enabled": false, + "duration": 180, + "length": 8, + "emailTemplate": { + "subject": "OTP for {APP_NAME}", + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "authToken": { + "duration": 86400 + }, + "passwordResetToken": { + "duration": 1800 + }, + "emailChangeToken": { + "duration": 1800 + }, + "verificationToken": { + "duration": 259200 + }, + "fileToken": { + "duration": 180 + }, + "verificationTemplate": { + "subject": "Verify your {APP_NAME} email", + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "resetPasswordTemplate": { + "subject": "Reset your {APP_NAME} password", + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "confirmEmailChangeTemplate": { + "subject": "Confirm your {APP_NAME} new email address", + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + { + "id": "pbc_4097575383", + "listRule": "// 1. UI USER, owner/admin: every NATS identity in the active organization.\n// Not members -- this collection holds signed identities, and creds_file\n// embeds the seed. Members get exactly one row, via branch 2.\n(@request.auth.collectionName = \"users\" &&\n organization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))) ||\n\n// 2. UI USER, any role: the ONE identity linked to my own membership in the\n// active organization. The browser opens its NATS connection with these\n// credentials (ui/src/stores/nats.ts), so every role including dashboard needs\n// this -- and only ever its own row. Row-correlated: both conditions match\n// the same membership record.\n(@request.auth.collectionName = \"users\" &&\n memberships_via_nats_user.user ?= @request.auth.id &&\n memberships_via_nats_user.organization ?= @request.auth.current_organization) ||\n\n// 3. THING: a Thing sees ONLY the NATS user assigned to its 'nats_user' field\n(@request.auth.collectionName = \"things\" &&\n things_via_nats_user.id ?= @request.auth.id) ||\n\n// 4. SELF: if the NATS user authenticates directly, it sees its own record\n(@request.auth.collectionName = \"nats_users\" &&\n id = @request.auth.id)\n\n// NOTE: there is deliberately no leaf_nodes branch. An edge box gets its creds\n// from GET /api/leaf/bootstrap (hooks/leaf_node_routes.go), which reads this\n// collection with the app's own privileges and returns four named values. That\n// keeps the edge's blast radius fixed regardless of how these rules evolve.", + "viewRule": "// 1. UI USER, owner/admin: every NATS identity in the active organization.\n// Not members -- this collection holds signed identities, and creds_file\n// embeds the seed. Members get exactly one row, via branch 2.\n(@request.auth.collectionName = \"users\" &&\n organization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))) ||\n\n// 2. UI USER, any role: the ONE identity linked to my own membership in the\n// active organization. The browser opens its NATS connection with these\n// credentials (ui/src/stores/nats.ts), so every role including dashboard needs\n// this -- and only ever its own row. Row-correlated: both conditions match\n// the same membership record.\n(@request.auth.collectionName = \"users\" &&\n memberships_via_nats_user.user ?= @request.auth.id &&\n memberships_via_nats_user.organization ?= @request.auth.current_organization) ||\n\n// 3. THING: a Thing sees ONLY the NATS user assigned to its 'nats_user' field\n(@request.auth.collectionName = \"things\" &&\n things_via_nats_user.id ?= @request.auth.id) ||\n\n// 4. SELF: if the NATS user authenticates directly, it sees its own record\n(@request.auth.collectionName = \"nats_users\" &&\n id = @request.auth.id)\n\n// NOTE: there is deliberately no leaf_nodes branch. An edge box gets its creds\n// from GET /api/leaf/bootstrap (hooks/leaf_node_routes.go), which reads this\n// collection with the app's own privileges and returns four named values. That\n// keeps the edge's blast radius fixed regardless of how these rules evolve.", + "createRule": "// Owner/admin only. Minting a NATS identity is a security operation: the\n// permissions on the new record end up inside a JWT signed by the org account.\n@request.auth.collectionName = \"users\" &&\n@request.body.organization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "updateRule": "// Owner/admin only, scoped to the active org, and never re-pointable at another\n// tenant. Members are excluded because publish_permissions here is copied into\n// the signed JWT -- a member could otherwise self-grant publish \">\".\n//\n// Rotating your OWN credentials does not go through this rule: it needs a\n// single-field allowlist (regenerate, and nothing else), which a rule can only\n// approximate with an :isset deny-list that silently opens up whenever a field\n// is added. That is POST /api/me/nats-creds/rotate instead\n// (hooks/credential_routes.go), which writes exactly one field and takes no\n// record id.\n@request.auth.collectionName = \"users\" &&\norganization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")) &&\n@request.body.organization:changed = false", + "deleteRule": "// Owner/admin only (mirrors updateRule). Deleting a NATS identity revokes a\n// device's access, so it is not a member-level action.\n@request.auth.collectionName = \"users\" &&\norganization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "name": "nats_users", + "type": "auth", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": true, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text730134616", + "max": 100, + "min": 0, + "name": "nats_username", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1843675174", + "max": 500, + "min": 0, + "name": "description", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1727648867", + "max": 200, + "min": 0, + "name": "public_key", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text4160324774", + "max": 200, + "min": 0, + "name": "private_key", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text1149756166", + "max": 200, + "min": 0, + "name": "seed", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_4029046258", + "hidden": false, + "id": "relation2607505338", + "maxSelect": 1, + "minSelect": 0, + "name": "account_id", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3780300774", + "hidden": false, + "id": "relation3590529708", + "maxSelect": 1, + "minSelect": 0, + "name": "role_id", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2367147504", + "max": 5000, + "min": 0, + "name": "jwt", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2060661766", + "max": 10000, + "min": 0, + "name": "creds_file", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "bool4185786694", + "name": "bearer_token", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "date1758146132", + "max": "", + "min": "", + "name": "jwt_expires_at", + "presentable": false, + "required": false, + "system": false, + "type": "date" + }, + { + "hidden": false, + "id": "bool2582005226", + "name": "regenerate", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "bool1260321794", + "name": "active", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation3253625724", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": false, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "help": "", + "hidden": false, + "id": "json1222209967", + "maxSize": 5000, + "name": "publish_permissions", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "help": "", + "hidden": false, + "id": "json4032180518", + "maxSize": 5000, + "name": "subscribe_permissions", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "help": "", + "hidden": false, + "id": "json1060489524", + "maxSize": 5000, + "name": "publish_deny_permissions", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "help": "", + "hidden": false, + "id": "json3735174119", + "maxSize": 5000, + "name": "subscribe_deny_permissions", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "help": "", + "hidden": false, + "id": "bool284467634", + "name": "revoke", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_tokenKey_pbc_4097575383` ON `nats_users` (`tokenKey`)", + "CREATE UNIQUE INDEX `idx_email_pbc_4097575383` ON `nats_users` (`email`) WHERE `email` != ''" + ], + "system": false, + "authRule": "", + "manageRule": null, + "authAlert": { + "enabled": false, + "emailTemplate": { + "subject": "Login from a new location", + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location:

\n

{ALERT_INFO}

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

If this was you, you may disregard this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "oauth2": { + "mappedFields": { + "id": "", + "name": "", + "username": "", + "avatarURL": "" + }, + "enabled": false + }, + "passwordAuth": { + "enabled": false, + "identityFields": [ + "email" + ] + }, + "mfa": { + "enabled": false, + "duration": 1800, + "rule": "" + }, + "otp": { + "enabled": false, + "duration": 180, + "length": 8, + "emailTemplate": { + "subject": "OTP for {APP_NAME}", + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "authToken": { + "duration": 604800 + }, + "passwordResetToken": { + "duration": 1800 + }, + "emailChangeToken": { + "duration": 1800 + }, + "verificationToken": { + "duration": 259200 + }, + "fileToken": { + "duration": 180 + }, + "verificationTemplate": { + "subject": "Verify your {APP_NAME} email", + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "resetPasswordTemplate": { + "subject": "Reset your {APP_NAME} password", + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "confirmEmailChangeTemplate": { + "subject": "Confirm your {APP_NAME} new email address", + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + { + "id": "pbc_789448849", + "listRule": "// 1. UI USER, owner/admin: every Nebula host in the active organization.\n// Not members -- config_yaml embeds the host private key in plaintext\n// (Nebula's PKI requires it inline, so it cannot be split out).\n(@request.auth.collectionName = \"users\" &&\n organization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))) ||\n\n// 2. THING: a Thing sees ONLY the Nebula host assigned to its 'nebula_host' field\n(@request.auth.collectionName = \"things\" &&\n things_via_nebula_host.id ?= @request.auth.id) ||\n\n// 3. SELF: if the Nebula host authenticates directly, it sees its own record\n(@request.auth.collectionName = \"nebula_hosts\" &&\n id = @request.auth.id)", + "viewRule": "// 1. UI USER, owner/admin: every Nebula host in the active organization.\n// Not members -- config_yaml embeds the host private key in plaintext\n// (Nebula's PKI requires it inline, so it cannot be split out).\n(@request.auth.collectionName = \"users\" &&\n organization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))) ||\n\n// 2. THING: a Thing sees ONLY the Nebula host assigned to its 'nebula_host' field\n(@request.auth.collectionName = \"things\" &&\n things_via_nebula_host.id ?= @request.auth.id) ||\n\n// 3. SELF: if the Nebula host authenticates directly, it sees its own record\n(@request.auth.collectionName = \"nebula_hosts\" &&\n id = @request.auth.id)", + "createRule": "// Owner/admin only. Creating a host issues a certificate against the org CA --\n// a new identity on the overlay network.\n@request.auth.collectionName = \"users\" &&\n@request.body.organization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "updateRule": "// Owner/admin only, scoped to the active org, never re-pointable at another\n// tenant. Members are excluded because this record carries the host's firewall\n// rules, groups, and overlay IP.\n@request.auth.collectionName = \"users\" &&\norganization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")) &&\n@request.body.organization:changed = false", + "deleteRule": "// Owner/admin only (mirrors updateRule).\n@request.auth.collectionName = \"users\" &&\norganization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "name": "nebula_hosts", + "type": "auth", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": true, + "system": true, + "type": "bool" + }, + { + "hidden": true, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3847340049", + "max": 100, + "min": 0, + "name": "hostname", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3987362972", + "max": 50, + "min": 0, + "name": "overlay_ip", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "json4033689968", + "maxSize": 1000, + "name": "groups", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "bool1453856164", + "name": "is_lighthouse", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2969239855", + "max": 100, + "min": 0, + "name": "public_host_port", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text563927626", + "max": 10000, + "min": 0, + "name": "certificate", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text4160324774", + "max": 10000, + "min": 0, + "name": "private_key", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4137057845", + "max": 10000, + "min": 0, + "name": "ca_certificate", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2056385908", + "max": 50000, + "min": 0, + "name": "config_yaml", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "json1395303521", + "maxSize": 10000, + "name": "firewall_outbound", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json3640940559", + "maxSize": 10000, + "name": "firewall_inbound", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "number4150671348", + "max": 10, + "min": 1, + "name": "validity_years", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "date261981154", + "max": "", + "min": "", + "name": "expires_at", + "presentable": false, + "required": false, + "system": false, + "type": "date" + }, + { + "hidden": false, + "id": "bool1260321794", + "name": "active", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3111208194", + "hidden": false, + "id": "relation873630609", + "maxSelect": 1, + "minSelect": 0, + "name": "network_id", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation3253625724", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": false, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX idx_host_network_ip ON nebula_hosts (network_id, overlay_ip)", + "CREATE UNIQUE INDEX idx_host_network_hostname ON nebula_hosts (network_id, hostname)", + "CREATE UNIQUE INDEX `idx_tokenKey_pbc_789448849` ON `nebula_hosts` (`tokenKey`)", + "CREATE UNIQUE INDEX `idx_email_pbc_789448849` ON `nebula_hosts` (`email`) WHERE `email` != ''" + ], + "system": false, + "authRule": "", + "manageRule": null, + "authAlert": { + "enabled": false, + "emailTemplate": { + "subject": "Login from a new location", + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location:

\n

{ALERT_INFO}

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

If this was you, you may disregard this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "oauth2": { + "mappedFields": { + "id": "", + "name": "", + "username": "", + "avatarURL": "" + }, + "enabled": false + }, + "passwordAuth": { + "enabled": false, + "identityFields": [ + "email" + ] + }, + "mfa": { + "enabled": false, + "duration": 1800, + "rule": "" + }, + "otp": { + "enabled": false, + "duration": 180, + "length": 8, + "emailTemplate": { + "subject": "OTP for {APP_NAME}", + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "authToken": { + "duration": 604800 + }, + "passwordResetToken": { + "duration": 1800 + }, + "emailChangeToken": { + "duration": 1800 + }, + "verificationToken": { + "duration": 259200 + }, + "fileToken": { + "duration": 180 + }, + "verificationTemplate": { + "subject": "Verify your {APP_NAME} email", + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "resetPasswordTemplate": { + "subject": "Reset your {APP_NAME} password", + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "confirmEmailChangeTemplate": { + "subject": "Confirm your {APP_NAME} new email address", + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + { + "id": "pbc_704572500", + "listRule": "// If a Thing is authenticating as itself (e.g. via SDK), it only sees its own record.\n// If a User is authenticating, they see all things belonging to their active organization.\n// A leaf node mirrors all things in its own organization.\n(@request.auth.collectionName = \"things\" && id = @request.auth.id) || \n(@request.auth.collectionName = \"users\" && organization = @request.auth.current_organization) || \n(@request.auth.collectionName = \"leaf_nodes\" && organization = @request.auth.organization)", + "viewRule": "// If a Thing is authenticating as itself (e.g. via SDK), it only sees its own record.\n// If a User is authenticating, they see all things belonging to their active organization.\n// A leaf node mirrors all things in its own organization.\n(@request.auth.collectionName = \"things\" && id = @request.auth.id) || \n(@request.auth.collectionName = \"users\" && organization = @request.auth.current_organization) || \n(@request.auth.collectionName = \"leaf_nodes\" && organization = @request.auth.organization)", + "createRule": "// Only users (not things) can create records, in their own active organization.\n// Members may add inventory; only owner/admin may attach a NATS or Nebula\n// identity to it, or set the active flag.\n//\n// nats_user / nebula_host are owner/admin only: a Thing can read the credential\n// of its own linked identity, so a member able to re-point those relations at a\n// privileged identity and then authenticate as the Thing would have a\n// credential-theft path.\n//\n// active is owner/admin only for the same reason delete is: taking a device off\n// the network revokes its NATS identity (hooks/active_flag.go). Members create\n// and edit inventory; disabling it is a management action.\n//\n// The member branch names the roles it admits. Restricting the FIELDS without\n// naming the ROLE let `dashboard` -- the least privileged role, which has no inventory\n// authority at all -- create and edit Things.\n@request.auth.collectionName = \"users\" &&\n@request.body.organization = @request.auth.current_organization &&\n(\n // Owner/admin: may assign the NATS / Nebula identity links.\n (@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))\n ||\n // Member: inventory fields only (name, description, location, metadata,\n // floorplan_position). Attaching an identity is an admin action.\n ((@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" ||\n @request.auth.memberships_via_user.role ?= \"admin\" ||\n @request.auth.memberships_via_user.role ?= \"member\")) &&\n @request.body.nats_user:changed = false &&\n @request.body.nebula_host:changed = false &&\n @request.body.active:changed = false)\n)", + "updateRule": "// 1. Only users can update things.\n// 2. The thing must belong to the user's current organization.\n// 3. The organization field cannot be modified (no \"teleporting\" between orgs).\n// 4. nats_user / nebula_host are owner/admin only: a Thing can read the\n// credential of its own linked identity, so a member able to re-point those\n// relations at a privileged identity and then authenticate as the Thing would\n// have a credential-theft path.\n// 5. active is owner/admin only for the same reason delete is: flipping it\n// revokes the Thing's NATS identity and kills its outstanding auth tokens\n// (hooks/active_flag.go). A member who could clear it could take any device in\n// the organization off the network.\n// 6. The member branch names the roles it admits. Restricting the FIELDS without\n// naming the ROLE let `dashboard` -- the least privileged role, which has no inventory\n// authority at all -- create and edit Things.\n@request.auth.collectionName = \"users\" &&\norganization = @request.auth.current_organization &&\n@request.body.organization:changed = false &&\n(\n // Owner/admin: may assign the NATS / Nebula identity links.\n (@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))\n ||\n // Member: inventory fields only (name, description, location, metadata,\n // floorplan_position). Attaching an identity is an admin action.\n ((@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" ||\n @request.auth.memberships_via_user.role ?= \"admin\" ||\n @request.auth.memberships_via_user.role ?= \"member\")) &&\n @request.body.nats_user:changed = false &&\n @request.body.nebula_host:changed = false &&\n @request.body.active:changed = false)\n)", + "deleteRule": "// Owner/admin only. `things` was the sole collection left with a member-level\n// delete -- locations, thing_types, location_types, message_schemas,\n// thing_type_operations, leaf_nodes, nats_roles and nebula_networks all gate it.\n//\n// Deleting a Thing is high-impact and not undoable from the UI: it orphans any\n// NATS or Nebula identity attached to it, and leaf-sync propagates the deletion\n// into every edge node local KV mirror. Members create and edit inventory;\n// removing it is a management action.\n@request.auth.collectionName = \"users\" &&\norganization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "name": "things", + "type": "auth", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": true, + "system": true, + "type": "bool" + }, + { + "hidden": true, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation3253625724", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1843675174", + "max": 0, + "min": 0, + "name": "description", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_247972991", + "hidden": false, + "id": "relation2363381545", + "maxSelect": 1, + "minSelect": 0, + "name": "type", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1997877400", + "max": 0, + "min": 0, + "name": "code", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1942858786", + "hidden": false, + "id": "relation1587448267", + "maxSelect": 1, + "minSelect": 0, + "name": "location", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "json1326724116", + "maxSize": 0, + "name": "metadata", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json2891045517", + "maxSize": 0, + "name": "floorplan_position", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_4097575383", + "hidden": false, + "id": "relation3991680165", + "maxSelect": 1, + "minSelect": 0, + "name": "nats_user", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_789448849", + "hidden": false, + "id": "relation324049829", + "maxSelect": 1, + "minSelect": 0, + "name": "nebula_host", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "bool7200000001", + "name": "active", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_tokenKey_pbc_704572500` ON `things` (`tokenKey`)", + "CREATE UNIQUE INDEX `idx_email_pbc_704572500` ON `things` (`email`) WHERE `email` != ''", + "CREATE UNIQUE INDEX `idx_things_org_code` ON `things` (`organization`, `code`) WHERE `code` != ''" + ], + "system": false, + "authRule": "// A deactivated Thing cannot obtain a new auth token.\n//\n// This rule is evaluated at the AUTH ENDPOINT ONLY (apis/record_helpers.go,\n// RecordAuthResponse) -- not on every request carrying an already-issued token.\n// authToken.duration below is 7 days, so on its own this rule would leave a\n// deactivated device connected for up to a week. hooks/active_flag.go closes\n// that window by refreshing tokenKey when active goes true -> false, which\n// invalidates every outstanding token immediately, and by revoking the linked\n// NATS identity -- which is the Thing's real capability, and is not touched by\n// anything PocketBase does.\nactive = true", + "manageRule": "// Org Admins/Owners may manage (e.g. reset the PocketBase password of) their\n// organization's things. Without this, `password` on update requires\n// `oldPassword` (forms/record_upsert.go) which nobody holds for a device, so a\n// Thing's PocketBase credential was mint-once with no recovery short of\n// deleting the record. Mirrors the leaf_nodes manageRule and the updateRule\n// below.\norganization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "authAlert": { + "enabled": true, + "emailTemplate": { + "subject": "Login from a new location", + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "oauth2": { + "mappedFields": { + "id": "", + "name": "", + "username": "", + "avatarURL": "" + }, + "enabled": false + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "mfa": { + "enabled": false, + "duration": 1800, + "rule": "" + }, + "otp": { + "enabled": false, + "duration": 180, + "length": 8, + "emailTemplate": { + "subject": "OTP for {APP_NAME}", + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "authToken": { + "duration": 604800 + }, + "passwordResetToken": { + "duration": 1800 + }, + "emailChangeToken": { + "duration": 1800 + }, + "verificationToken": { + "duration": 259200 + }, + "fileToken": { + "duration": 180 + }, + "verificationTemplate": { + "subject": "Verify your {APP_NAME} email", + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "resetPasswordTemplate": { + "subject": "Reset your {APP_NAME} password", + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "confirmEmailChangeTemplate": { + "subject": "Confirm your {APP_NAME} new email address", + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + { + "id": "_pb_users_auth_", + "listRule": "// 1. Every user can see their own record\nid = @request.auth.id ||\n// 2. Operators can see all users (for owner selection, user management)\n@request.auth.is_operator = true ||\n// 3. Org Admins/Owners can see users in their current org\n(\n @request.auth.current_organization != \"\" &&\n @request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\") &&\n memberships_via_user.organization ?= @request.auth.current_organization\n)", + "viewRule": "// 1. Every user can see their own record\nid = @request.auth.id ||\n// 2. Operators can see all users\n@request.auth.is_operator = true ||\n// 3. Org Admins/Owners can see users in their current org\n(\n @request.auth.current_organization != \"\" &&\n @request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\") &&\n memberships_via_user.organization ?= @request.auth.current_organization\n)", + "createRule": "// Operators can create users (for org onboarding).\n@request.auth.is_operator = true\n||\n// Anonymous self-registration -- but ONLY for an address that already has a\n// pending invite. AcceptInviteView has to create the account unauthenticated\n// (it cannot call /api/tenancy/accept-invite, which requires auth, until the\n// account exists), so this branch must stay open. accept-invite deletes the\n// invite row, so the gate closes behind itself.\n// Exact-match on email, consistent with the accept-invite handler's own check.\n// NOTE: enabling users.oauth2 will route first-time OAuth2 logins through this\n// same rule; uninvited addresses would be rejected here.\n(\n @request.auth.id = ''\n && @request.body.is_operator:isset = false\n && @collection.invites.email ?= @request.body.email\n)", + "updateRule": "// Only your own record.\nid = @request.auth.id\n&&\n// is_operator is NOT grantable through the API. Without this clause any\n// authenticated user can PATCH themselves to platform operator, which reads\n// every tenant's data (including audit_logs). Granting it is superuser\n// (admin panel) or `bootstrap` CLI only -- both bypass API rules.\n@request.body.is_operator:isset = false\n&&\n// You may only switch into an organization you are actually a member of.\n(\n @request.body.current_organization:isset = false\n || @request.auth.memberships_via_user.organization ?= @request.body.current_organization\n)", + "deleteRule": "// Users can only delete their own account.\nid = @request.auth.id", + "name": "users", + "type": "auth", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 255, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file376926767", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/gif", + "image/webp" + ], + "name": "avatar", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": null, + "type": "file" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation1046470055", + "maxSelect": 1, + "minSelect": 0, + "name": "current_organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "bool_is_operator", + "name": "is_operator", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_tokenKey__pb_users_auth_` ON `users` (`tokenKey`)", + "CREATE UNIQUE INDEX `idx_email__pb_users_auth_` ON `users` (`email`) WHERE `email` != ''" + ], + "system": false, + "authRule": "", + "manageRule": null, + "authAlert": { + "enabled": true, + "emailTemplate": { + "subject": "Login from a new location", + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location:

\n

{ALERT_INFO}

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

If this was you, you may disregard this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "oauth2": { + "mappedFields": { + "id": "", + "name": "name", + "username": "", + "avatarURL": "avatar" + }, + "enabled": false + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "mfa": { + "enabled": false, + "duration": 1800, + "rule": "" + }, + "otp": { + "enabled": false, + "duration": 180, + "length": 8, + "emailTemplate": { + "subject": "OTP for {APP_NAME}", + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "authToken": { + "duration": 604800 + }, + "passwordResetToken": { + "duration": 1800 + }, + "emailChangeToken": { + "duration": 1800 + }, + "verificationToken": { + "duration": 259200 + }, + "fileToken": { + "duration": 180 + }, + "verificationTemplate": { + "subject": "Verify your {APP_NAME} email", + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "resetPasswordTemplate": { + "subject": "Reset your {APP_NAME} password", + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "confirmEmailChangeTemplate": { + "subject": "Confirm your {APP_NAME} new email address", + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + { + "id": "pbc_681515208", + "listRule": "// Audit logs have no organization field, so restrict reads to operators.\n// Previously \"@request.auth.id != ''\" exposed every tenant's logs to any authenticated identity.\n@request.auth.is_operator = true", + "viewRule": "// Audit logs have no organization field, so restrict reads to operators.\n// Previously \"@request.auth.id != ''\" exposed every tenant's logs to any authenticated identity.\n@request.auth.is_operator = true", + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "audit_logs", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "select2467634050", + "maxSelect": 1, + "name": "event_type", + "presentable": false, + "required": true, + "system": false, + "type": "select", + "values": [ + "create_request", + "update_request", + "delete_request", + "create", + "update", + "delete", + "auth" + ] + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text341070568", + "max": 255, + "min": 0, + "name": "collection_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1308456204", + "max": 255, + "min": 0, + "name": "record_id", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relation2375276105", + "maxSelect": 1, + "minSelect": 0, + "name": "user", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2130353538", + "max": 100, + "min": 0, + "name": "auth_method", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text352385435", + "max": 20, + "min": 0, + "name": "request_method", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1487170776", + "max": 100, + "min": 0, + "name": "request_ip", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text962331874", + "max": 2000, + "min": 0, + "name": "request_url", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "date2782324286", + "max": "", + "min": "", + "name": "timestamp", + "presentable": false, + "required": true, + "system": false, + "type": "date" + }, + { + "hidden": false, + "id": "json2123753343", + "maxSize": 2000000, + "name": "before_changes", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json2371096462", + "maxSize": 2000000, + "name": "after_changes", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [ + "CREATE INDEX idx_audit_collection_name ON audit_logs (collection_name)", + "CREATE INDEX idx_audit_record_id ON audit_logs (record_id)", + "CREATE INDEX idx_audit_timestamp ON audit_logs (timestamp)", + "CREATE INDEX idx_audit_user ON audit_logs (user)", + "CREATE INDEX idx_audit_collection_timestamp ON audit_logs (collection_name, timestamp)", + "CREATE INDEX idx_audit_user_timestamp ON audit_logs (user, timestamp)" + ], + "system": false + }, + { + "id": "pbc_2452428166", + "listRule": "// 1. The invitation must belong to the user's active organization context.\n// 2. The user must be the Organization Owner OR have an Admin/Owner membership in that org.\norganization = @request.auth.current_organization && \n(\n (organization.owner = @request.auth.id) || \n (organization.memberships_via_organization.user ?= @request.auth.id && \n (organization.memberships_via_organization.role ?= \"owner\" || organization.memberships_via_organization.role ?= \"admin\"))\n)", + "viewRule": "// 1. The invitation must belong to the user's active organization context.\n// 2. The user must be the Organization Owner OR have an Admin/Owner membership in that org.\norganization = @request.auth.current_organization && \n(\n (organization.owner = @request.auth.id) || \n (organization.memberships_via_organization.user ?= @request.auth.id && \n (organization.memberships_via_organization.role ?= \"owner\" || organization.memberships_via_organization.role ?= \"admin\"))\n)", + "createRule": "// Operators can invite to any org, OR admin/owner of the target org\n@request.auth.collectionName = \"users\" && \n(\n @request.auth.is_operator = true ||\n (\n @request.body.organization = @request.auth.current_organization &&\n (\n (@collection.organizations.id ?= @request.body.organization && @collection.organizations.owner ?= @request.auth.id) ||\n (@collection.memberships.user ?= @request.auth.id && \n @collection.memberships.organization ?= @request.body.organization && \n (@collection.memberships.role ?= \"owner\" || @collection.memberships.role ?= \"admin\"))\n )\n )\n)", + "updateRule": "// 1. Must be in the active organization context.\n// 2. Requester must be an Admin/Owner.\n// 3. Moving an invitation to a different organization is forbidden.\norganization = @request.auth.current_organization && \n(\n (organization.owner = @request.auth.id) || \n (organization.memberships_via_organization.user ?= @request.auth.id && \n (organization.memberships_via_organization.role ?= \"owner\" || organization.memberships_via_organization.role ?= \"admin\"))\n) && \n@request.body.organization:changed = false", + "deleteRule": "// 1. Must be in the active organization context.\n// 2. Only Admins/Owners can revoke (delete) a pending invitation.\norganization = @request.auth.current_organization && \n(\n (organization.owner = @request.auth.id) || \n (organization.memberships_via_organization.user ?= @request.auth.id && \n (organization.memberships_via_organization.role ?= \"owner\" || organization.memberships_via_organization.role ?= \"admin\"))\n)", + "name": "invites", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": false, + "type": "email" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1597481275", + "max": 64, + "min": 0, + "name": "token", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "date261981154", + "max": "", + "min": "", + "name": "expires_at", + "presentable": false, + "required": false, + "system": false, + "type": "date" + }, + { + "hidden": false, + "id": "bool1453241294", + "name": "resend_invite", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "cascadeDelete": true, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation3253625724", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "select1466534506", + "maxSelect": 1, + "name": "role", + "presentable": false, + "required": true, + "system": false, + "type": "select", + "values": [ + "admin", + "member", + "viewer", + "dashboard" + ] + }, + { + "cascadeDelete": true, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relation1109389909", + "maxSelect": 1, + "minSelect": 0, + "name": "invited_by", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX idx_token ON invites (token)", + "CREATE INDEX idx_invites_email ON invites (email)", + "CREATE INDEX idx_invites_org ON invites (organization)", + "CREATE INDEX idx_invites_expires ON invites (expires_at)" + ], + "system": false + }, + { + "id": "pbc_143112578", + "listRule": "// Users see their active organization's types; a leaf node mirrors its own organization's types.\n(@request.auth.collectionName = \"users\" && \norganization = @request.auth.current_organization) || \n(@request.auth.collectionName = \"leaf_nodes\" && organization = @request.auth.organization)", + "viewRule": "// Users see their active organization's types; a leaf node mirrors its own organization's types.\n(@request.auth.collectionName = \"users\" && \norganization = @request.auth.current_organization) || \n(@request.auth.collectionName = \"leaf_nodes\" && organization = @request.auth.organization)", + "createRule": "// Only Admins or Owners can create types for their active organization.\n@request.auth.collectionName = \"users\" && \n@request.body.organization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "updateRule": "// 1. Must be Admin/Owner.\n// 2. Must belong to active org.\n// 3. Prevent organization field tampering.\norganization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")) && \n@request.body.organization:changed = false", + "deleteRule": "// Only Admins or Owners can delete types.\norganization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "name": "location_types", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation3253625724", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1843675174", + "max": 0, + "min": 0, + "name": "description", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1997877400", + "max": 0, + "min": 0, + "name": "code", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "json_lt_metadata_schema", + "maxSize": 0, + "name": "metadata_schema", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_location_types_org_code` ON `location_types` (`organization`, `code`) WHERE `code` != ''" + ], + "system": false + }, + { + "id": "pbc_1942858786", + "listRule": "(@request.auth.collectionName = \"users\" && organization = @request.auth.current_organization) || \n(@request.auth.collectionName = \"leaf_nodes\" && organization = @request.auth.organization)", + "viewRule": "(@request.auth.collectionName = \"users\" && organization = @request.auth.current_organization) || \n(@request.auth.collectionName = \"leaf_nodes\" && organization = @request.auth.organization)", + "createRule": "// 1. Must be authenticated as a user holding an inventory role in the active\n// organization. This rule had no role check at all, which let `dashboard` create\n// locations.\n// 2. The organization assigned to the new record must match the active context.\n@request.auth.collectionName = \"users\" &&\n@request.body.organization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" ||\n @request.auth.memberships_via_user.role ?= \"admin\" ||\n @request.auth.memberships_via_user.role ?= \"member\"))", + "updateRule": "// 1. Must be authenticated as a user holding an inventory role in the active\n// organization (see createRule -- this had no role check either).\n// 2. Must belong to the current active organization.\n// 3. Cannot change the organization ID (prevents moving data between tenants).\n@request.auth.collectionName = \"users\" &&\norganization = @request.auth.current_organization &&\n@request.body.organization:changed = false &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" ||\n @request.auth.memberships_via_user.role ?= \"admin\" ||\n @request.auth.memberships_via_user.role ?= \"member\"))", + "deleteRule": "// High-impact actions like deletion should usually be restricted to management roles.\norganization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "name": "locations", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation3253625724", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1843675174", + "max": 0, + "min": 0, + "name": "description", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_143112578", + "hidden": false, + "id": "relation2363381545", + "maxSelect": 1, + "minSelect": 0, + "name": "type", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1997877400", + "max": 0, + "min": 0, + "name": "code", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file3043290937", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "floorplan", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1942858786", + "hidden": false, + "id": "relation1032740943", + "maxSelect": 1, + "minSelect": 0, + "name": "parent", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "geoPoint2551633526", + "name": "coordinates", + "presentable": false, + "required": false, + "system": false, + "type": "geoPoint" + }, + { + "hidden": false, + "id": "json1326724116", + "maxSize": 0, + "name": "metadata", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_locations_org_code` ON `locations` (`organization`, `code`) WHERE `code` != ''" + ], + "system": false + }, + { + "id": "pbc_1990943746", + "listRule": "// 1. Always show my own memberships across all organizations (required for the switcher)\n(user = @request.auth.id) || \n\n// 2. Show other members ONLY IF they are in my current active context\n// AND I have the authority (Owner or Admin) in that specific organization.\n(\n organization = @request.auth.current_organization && \n (\n (organization.owner = @request.auth.id) ||\n (organization.memberships_via_organization.user ?= @request.auth.id && \n (organization.memberships_via_organization.role ?= \"owner\" || organization.memberships_via_organization.role ?= \"admin\"))\n )\n)", + "viewRule": "// 1. Always show my own memberships across all organizations (required for the switcher)\n(user = @request.auth.id) || \n\n// 2. Show other members ONLY IF they are in my current active context\n// AND I have the authority (Owner or Admin) in that specific organization.\n(\n organization = @request.auth.current_organization && \n (\n (organization.owner = @request.auth.id) ||\n (organization.memberships_via_organization.user ?= @request.auth.id && \n (organization.memberships_via_organization.role ?= \"owner\" || organization.memberships_via_organization.role ?= \"admin\"))\n )\n)", + "createRule": "// Only owners or admins of an organization can create new memberships for that organization.\n// Scoped to the requester's active org context, with row-correlated back-relations.\n@request.auth.collectionName = \"users\" && \n@request.body.organization = @request.auth.current_organization && \n(\n @request.body.organization.owner = @request.auth.id ||\n (\n @request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")\n )\n)", + "updateRule": "// A membership row is never re-pointed at another organization -- that would\n// move a member into a different tenant. Mirrors invites.updateRule.\n@request.body.organization:changed = false\n&&\n(\n // 1. My OWN membership -- the operational NATS identity link only.\n // `role` has to stay out of reach here: the bare `user = @request.auth.id`\n // this replaces let any member PATCH their own role to \"owner\".\n (\n user = @request.auth.id\n && @request.body.role:isset = false\n && @request.body.user:isset = false\n && @request.body.invited_by:isset = false\n )\n ||\n // 2. I am the OWNER of the organization (checked directly on the Org record)\n organization.owner = @request.auth.id\n ||\n // 3. I am an Admin/Owner of the organization (checked via back-relation\n // so the user/role conditions correlate to the SAME membership row).\n (\n organization.memberships_via_organization.user ?= @request.auth.id &&\n (\n organization.memberships_via_organization.role ?= \"owner\" ||\n organization.memberships_via_organization.role ?= \"admin\"\n )\n )\n)", + "deleteRule": "// Only owners/admins of the organization can delete/remove a membership.\n// Uses back-relation so user/role conditions correlate to the SAME membership row.\n@request.auth.collectionName = \"users\" && \n(\n organization.owner = @request.auth.id ||\n (\n organization.memberships_via_organization.user ?= @request.auth.id &&\n (organization.memberships_via_organization.role ?= \"owner\" || organization.memberships_via_organization.role ?= \"admin\")\n )\n)", + "name": "memberships", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cascadeDelete": true, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relation2375276105", + "maxSelect": 1, + "minSelect": 0, + "name": "user", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "cascadeDelete": true, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation3253625724", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "select1466534506", + "maxSelect": 1, + "name": "role", + "presentable": false, + "required": true, + "system": false, + "type": "select", + "values": [ + "owner", + "admin", + "member", + "viewer", + "dashboard" + ] + }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relation1109389909", + "maxSelect": 1, + "minSelect": 0, + "name": "invited_by", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_4097575383", + "hidden": false, + "id": "relation3991680165", + "maxSelect": 1, + "minSelect": 0, + "name": "nats_user", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + } + ], + "indexes": [ + "CREATE INDEX idx_memberships_user ON memberships (user)", + "CREATE INDEX idx_memberships_org ON memberships (organization)", + "CREATE UNIQUE INDEX idx_memberships_user_org ON memberships (user, organization)" + ], + "system": false + }, + { + "id": "pbc_4029046258", + "listRule": "// Operators can see all NATS accounts\n@request.auth.is_operator = true\n||\n// Regular users: only records belonging to their active organization context\n(@request.auth.collectionName = \"users\" &&\norganization = @request.auth.current_organization)\n\n// NOTE: no leaf_nodes branch -- see nats_users. The account JWT reaches an edge\n// box through GET /api/leaf/bootstrap.", + "viewRule": "// Operators can see all NATS accounts\n@request.auth.is_operator = true\n||\n// Regular users: only records belonging to their active organization context\n(@request.auth.collectionName = \"users\" &&\norganization = @request.auth.current_organization)\n\n// NOTE: no leaf_nodes branch -- see nats_users. The account JWT reaches an edge\n// box through GET /api/leaf/bootstrap.", + "createRule": null, + "updateRule": "// Platform operators only.\n//\n// This rule used to have an owner/admin branch that froze the six max_* limit\n// fields and organization, commented \"can only change rotate_keys\". It was a\n// deny-list, and it leaked: it still permitted writes to jwt (the signed account\n// JWT), revocations (which user JWTs the account rejects), public_key,\n// signing_public_key, signing_keys, name, description and active -- and any field\n// added later would have been tenant-writable too, silently.\n//\n// A rule cannot express \"this one field and nothing else\", so the three key\n// operations an owner/admin legitimately needs moved to a route that sets exactly\n// one field per call: POST /api/org/nats-account/keys with action = rotate |\n// add_signing | remove_signing (hooks/nats_account_routes.go).\n//\n// The limits stay operator-only on purpose: they are the resource envelope the\n// tenant was sold, so raising them is not a tenant action.\n@request.auth.is_operator = true", + "deleteRule": null, + "name": "nats_accounts", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 100, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1843675174", + "max": 500, + "min": 0, + "name": "description", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1727648867", + "max": 200, + "min": 0, + "name": "public_key", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text4160324774", + "max": 200, + "min": 0, + "name": "private_key", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text1149756166", + "max": 200, + "min": 0, + "name": "seed", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3643984679", + "max": 200, + "min": 0, + "name": "signing_public_key", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text2264386096", + "max": 200, + "min": 0, + "name": "signing_private_key", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text2053025571", + "max": 200, + "min": 0, + "name": "signing_seed", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2367147504", + "max": 50000, + "min": 0, + "name": "jwt", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "bool1260321794", + "name": "active", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "bool867999460", + "name": "rotate_keys", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "number39353658", + "max": null, + "min": -1, + "name": "max_connections", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "number2362752116", + "max": null, + "min": -1, + "name": "max_subscriptions", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "number3532183195", + "max": null, + "min": -1, + "name": "max_data", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "number3604951121", + "max": null, + "min": -1, + "name": "max_payload", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "number3988595182", + "max": null, + "min": -1, + "name": "max_jetstream_disk_storage", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "number2575141467", + "max": null, + "min": -1, + "name": "max_jetstream_memory_storage", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation3253625724", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "help": "", + "hidden": false, + "id": "json2320959689", + "maxSize": 10000, + "name": "signing_keys", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "help": "", + "hidden": true, + "id": "json446014123", + "maxSize": 10000, + "name": "signing_keys_private", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "help": "", + "hidden": false, + "id": "bool1877871817", + "name": "add_signing_key", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "autogeneratePattern": "", + "help": "", + "hidden": false, + "id": "text333673157", + "max": 200, + "min": 0, + "name": "remove_signing_key", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "help": "", + "hidden": false, + "id": "json839254801", + "maxSize": 50000, + "name": "revocations", + "presentable": false, + "required": false, + "system": false, + "type": "json" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2168816934", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "nats_publish_queue", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "help": "", + "hidden": false, + "id": "text2607505338", + "max": 200, + "min": 0, + "name": "account_id", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "select1204587666", + "maxSelect": 1, + "name": "action", + "presentable": false, + "required": true, + "system": false, + "type": "select", + "values": [ + "upsert", + "delete" + ] + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3065852031", + "max": 1000, + "min": 0, + "name": "message", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "number3217549156", + "max": 10, + "min": 0, + "name": "attempts", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "date500274325", + "max": "", + "min": "", + "name": "failed_at", + "presentable": false, + "required": false, + "system": false, + "type": "date" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "help": "", + "hidden": false, + "id": "text2590123719", + "max": 200, + "min": 0, + "name": "account_public_key", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "help": "", + "hidden": false, + "id": "text3534813612", + "max": 200, + "min": 0, + "name": "account_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3780300774", + "listRule": "// Owner/admin of the active organization only. A role is a NATS permission\n// template -- its publish/subscribe sets flow into every JWT signed for a user\n// holding the role, so it carries the same escalation risk as nats_users.\n@request.auth.collectionName = \"users\" &&\norganization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "viewRule": "// Owner/admin of the active organization only. A role is a NATS permission\n// template -- its publish/subscribe sets flow into every JWT signed for a user\n// holding the role, so it carries the same escalation risk as nats_users.\n@request.auth.collectionName = \"users\" &&\norganization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "createRule": "// Owner/admin only. See listRule: a role's permission sets reach the signed JWT.\n@request.auth.collectionName = \"users\" &&\n@request.body.organization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "updateRule": "// Owner/admin only, and never re-pointable at another tenant.\n@request.auth.collectionName = \"users\" &&\norganization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")) &&\n@request.body.organization:changed = false", + "deleteRule": "// High-impact actions like deletion should usually be restricted to management roles.\norganization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "name": "nats_roles", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 100, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1843675174", + "max": 500, + "min": 0, + "name": "description", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "bool4116874775", + "name": "is_default", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "number2362752116", + "max": null, + "min": -1, + "name": "max_subscriptions", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "number3532183195", + "max": null, + "min": -1, + "name": "max_data", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "number3604951121", + "max": null, + "min": -1, + "name": "max_payload", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation3253625724", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "json1222209967", + "maxSize": 0, + "name": "publish_permissions", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json4032180518", + "maxSize": 0, + "name": "subscribe_permissions", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json1060489524", + "maxSize": 0, + "name": "publish_deny_permissions", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json3735174119", + "maxSize": 0, + "name": "subscribe_deny_permissions", + "presentable": false, + "required": false, + "system": false, + "type": "json" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_1386776968", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "nats_system_operator", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 100, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1727648867", + "max": 200, + "min": 0, + "name": "public_key", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text4160324774", + "max": 200, + "min": 0, + "name": "private_key", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text1149756166", + "max": 200, + "min": 0, + "name": "seed", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3643984679", + "max": 200, + "min": 0, + "name": "signing_public_key", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text2264386096", + "max": 200, + "min": 0, + "name": "signing_private_key", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text2053025571", + "max": 200, + "min": 0, + "name": "signing_seed", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2367147504", + "max": 5000, + "min": 0, + "name": "jwt", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "help": "", + "hidden": false, + "id": "json2320959689", + "maxSize": 10000, + "name": "signing_keys", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "help": "", + "hidden": true, + "id": "json446014123", + "maxSize": 10000, + "name": "signing_keys_private", + "presentable": false, + "required": false, + "system": false, + "type": "json" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_93572460", + "listRule": "// Operators can see all Nebula CAs\n@request.auth.is_operator = true\n||\n// Regular users: only records belonging to their active organization context\n(@request.auth.collectionName = \"users\" && \norganization = @request.auth.current_organization)", + "viewRule": "// Operators can see all Nebula CAs\n@request.auth.is_operator = true\n||\n// Regular users: only records belonging to their active organization context\n(@request.auth.collectionName = \"users\" && \norganization = @request.auth.current_organization)", + "createRule": null, + "updateRule": "// Platform operators only.\n//\n// As with nats_accounts.updateRule, the previous owner/admin branch was a\n// deny-list -- it froze validity_years, curve and organization and was commented\n// \"can only change rotate_keys\", a field that DOES NOT EXIST on this collection.\n// What it actually permitted was writing name, certificate and expires_at: an\n// owner or admin could replace the organization's Nebula CA certificate, the trust\n// anchor for its entire overlay network.\n//\n// There is no tenant-triggered CA rotation today because there is no trigger field\n// for one. Rolling a CA is an operator operation. If that changes, add a route\n// (see hooks/nats_account_routes.go) rather than a branch here.\n@request.auth.is_operator = true", + "deleteRule": null, + "name": "nebula_ca", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 100, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text563927626", + "max": 10000, + "min": 0, + "name": "certificate", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text4160324774", + "max": 10000, + "min": 0, + "name": "private_key", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "number4150671348", + "max": 50, + "min": 1, + "name": "validity_years", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "date261981154", + "max": "", + "min": "", + "name": "expires_at", + "presentable": false, + "required": false, + "system": false, + "type": "date" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1469668791", + "max": 50, + "min": 0, + "name": "curve", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation3253625724", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX idx_ca_name ON nebula_ca (name)" + ], + "system": false + }, + { + "id": "pbc_3111208194", + "listRule": "// Owner/admin of the active organization only. A network defines the overlay\n// CIDR and lighthouse topology that every host certificate is issued against.\n@request.auth.collectionName = \"users\" &&\norganization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "viewRule": "// Owner/admin of the active organization only. A network defines the overlay\n// CIDR and lighthouse topology that every host certificate is issued against.\n@request.auth.collectionName = \"users\" &&\norganization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "createRule": "// Owner/admin only. See listRule.\n@request.auth.collectionName = \"users\" &&\n@request.body.organization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "updateRule": "// Owner/admin only, and never re-pointable at another tenant.\n@request.auth.collectionName = \"users\" &&\norganization = @request.auth.current_organization &&\n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization &&\n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")) &&\n@request.body.organization:changed = false", + "deleteRule": "// High-impact actions like deletion should usually be restricted to management roles.\norganization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "name": "nebula_networks", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 100, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1843675174", + "max": 500, + "min": 0, + "name": "description", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2930874575", + "max": 50, + "min": 0, + "name": "cidr_range", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "bool1260321794", + "name": "active", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_93572460", + "hidden": false, + "id": "relation36337348", + "maxSelect": 1, + "minSelect": 0, + "name": "ca_id", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation3253625724", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX idx_network_ca_name ON nebula_networks (ca_id, name)", + "CREATE UNIQUE INDEX idx_network_ca_cidr ON nebula_networks (ca_id, cidr_range)" + ], + "system": false + }, + { + "id": "pbc_2873630990", + "listRule": "@request.auth.id != '' && (@request.auth.is_operator = true || (@collection.memberships.user.id ?= @request.auth.id && @collection.memberships.organization.id ?= id))", + "viewRule": "@request.auth.id != '' && (@request.auth.is_operator = true || (@collection.memberships.user.id ?= @request.auth.id && @collection.memberships.organization.id ?= id))", + "createRule": "@request.auth.id != '' && @request.auth.is_operator = true", + "updateRule": "// Platform operators only. An organization record carries the tenancy flags\n// (managed / is_operator_org / is_system_org) and drives NATS account + Nebula CA\n// provisioning, so it is not tenant-editable: an org owner has no update path.\n//\n// The owner branch that used to live here had no UI behind it anyway -- the only\n// view that writes this collection is admin/OrganizationFormView, whose route is\n// operator-gated.\n@request.auth.is_operator = true", + "deleteRule": "@request.auth.is_operator = true || owner = @request.auth.id", + "name": "organizations", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 100, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1843675174", + "max": 500, + "min": 0, + "name": "description", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "bool1260321794", + "name": "active", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relation3479234172", + "maxSelect": 1, + "minSelect": 0, + "name": "owner", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "file2138917348", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/gif", + "image/webp" + ], + "name": "logo", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [ + "100x100", + "200x200" + ], + "type": "file" + }, + { + "hidden": false, + "id": "bool_is_system_org", + "name": "is_system_org", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "bool_is_operator_org", + "name": "is_operator_org", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "bool_managed_org", + "name": "managed", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": false, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX idx_unique_org_name ON organizations (name)", + "CREATE UNIQUE INDEX idx_one_system_org ON organizations (is_system_org) WHERE is_system_org = true", + "CREATE UNIQUE INDEX idx_one_operator_org ON organizations (is_operator_org) WHERE is_operator_org = true" + ], + "system": false + }, + { + "id": "pbc_247972991", + "listRule": "// Users see their active organization's types; a leaf node mirrors its own organization's types.\n(@request.auth.collectionName = \"users\" && \norganization = @request.auth.current_organization) || \n(@request.auth.collectionName = \"leaf_nodes\" && organization = @request.auth.organization)", + "viewRule": "// Users see their active organization's types; a leaf node mirrors its own organization's types.\n(@request.auth.collectionName = \"users\" && \norganization = @request.auth.current_organization) || \n(@request.auth.collectionName = \"leaf_nodes\" && organization = @request.auth.organization)", + "createRule": "// Only Admins or Owners can create types for their active organization.\n@request.auth.collectionName = \"users\" && \n@request.body.organization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "updateRule": "// 1. Must be Admin/Owner.\n// 2. Must belong to active org.\n// 3. Prevent organization field tampering.\norganization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")) && \n@request.body.organization:changed = false", + "deleteRule": "// Only Admins or Owners can delete types.\norganization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "name": "thing_types", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation3253625724", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1843675174", + "max": 0, + "min": 0, + "name": "description", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1997877400", + "max": 0, + "min": 0, + "name": "code", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "select490417661", + "maxSelect": 4, + "name": "capabilities", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "publish", + "subscribe", + "request", + "reply" + ] + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_subject_prefix", + "max": 0, + "min": 0, + "name": "subject_prefix", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1722283898", + "hidden": false, + "id": "relation_tt_operations", + "maxSelect": 999, + "minSelect": 0, + "name": "operations", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "json_tt_metadata_schema", + "maxSize": 0, + "name": "metadata_schema", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3780300774", + "help": "", + "hidden": false, + "id": "relation_tt_nats_role", + "maxSelect": 1, + "minSelect": 0, + "name": "nats_role", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_thing_types_org_code` ON `thing_types` (`organization`, `code`) WHERE `code` != ''" + ], + "system": false + }, + { + "id": "pbc_3387006851", + "listRule": "@request.auth.collectionName = \"users\" && \norganization = @request.auth.current_organization && \n@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n(@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")", + "viewRule": "@request.auth.collectionName = \"users\" && \norganization = @request.auth.current_organization && \n@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n(@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")", + "createRule": "@request.auth.collectionName = \"users\" && \n@request.body.organization = @request.auth.current_organization && \n@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n(@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")", + "updateRule": "organization = @request.auth.current_organization && \n@request.body.organization:changed = false && \n@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n(@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")", + "deleteRule": "organization = @request.auth.current_organization && \n@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n(@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")", + "name": "nats_account_exports", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cascadeDelete": true, + "collectionId": "pbc_4029046258", + "hidden": false, + "id": "relation_export_account", + "maxSelect": 1, + "minSelect": 0, + "name": "account_id", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_export_name", + "max": 100, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_export_subject", + "max": 500, + "min": 0, + "name": "subject", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "select_export_type", + "maxSelect": 1, + "name": "type", + "presentable": false, + "required": true, + "system": false, + "type": "select", + "values": [ + "stream", + "service" + ] + }, + { + "hidden": false, + "id": "bool_export_token_req", + "name": "token_req", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "select_export_response_type", + "maxSelect": 1, + "name": "response_type", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "Singleton", + "Stream", + "Chunked" + ] + }, + { + "hidden": false, + "id": "number_export_response_threshold", + "max": null, + "min": 0, + "name": "response_threshold", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "number_export_token_position", + "max": null, + "min": 0, + "name": "account_token_position", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "bool_export_advertise", + "name": "advertise", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "bool_export_allow_trace", + "name": "allow_trace", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_export_description", + "max": 500, + "min": 0, + "name": "description", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation_export_org", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate_export_created", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate_export_updated", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_1566205663", + "listRule": "@request.auth.collectionName = \"users\" && \norganization = @request.auth.current_organization && \n@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n(@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")", + "viewRule": "@request.auth.collectionName = \"users\" && \norganization = @request.auth.current_organization && \n@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n(@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")", + "createRule": "@request.auth.collectionName = \"users\" && \n@request.body.organization = @request.auth.current_organization && \n@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n(@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")", + "updateRule": "organization = @request.auth.current_organization && \n@request.body.organization:changed = false && \n@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n(@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")", + "deleteRule": "organization = @request.auth.current_organization && \n@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n(@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")", + "name": "nats_account_imports", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cascadeDelete": true, + "collectionId": "pbc_4029046258", + "hidden": false, + "id": "relation_import_account", + "maxSelect": 1, + "minSelect": 0, + "name": "account_id", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_import_name", + "max": 100, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_import_subject", + "max": 500, + "min": 0, + "name": "subject", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_import_account", + "max": 200, + "min": 0, + "name": "account", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_import_token", + "max": 10000, + "min": 0, + "name": "token", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_import_local_subject", + "max": 500, + "min": 0, + "name": "local_subject", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "select_import_type", + "maxSelect": 1, + "name": "type", + "presentable": false, + "required": true, + "system": false, + "type": "select", + "values": [ + "stream", + "service" + ] + }, + { + "hidden": false, + "id": "bool_import_share", + "name": "share", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "bool_import_allow_trace", + "name": "allow_trace", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_import_description", + "max": 500, + "min": 0, + "name": "description", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation_import_org", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate_import_created", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate_import_updated", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_1722283898", + "listRule": "// Users see their active organization's operations; a leaf node mirrors its own organization's operations.\n(@request.auth.collectionName = \"users\" && \norganization = @request.auth.current_organization) || \n(@request.auth.collectionName = \"leaf_nodes\" && organization = @request.auth.organization)", + "viewRule": "// Users see their active organization's operations; a leaf node mirrors its own organization's operations.\n(@request.auth.collectionName = \"users\" && \norganization = @request.auth.current_organization) || \n(@request.auth.collectionName = \"leaf_nodes\" && organization = @request.auth.organization)", + "createRule": "@request.auth.collectionName = \"users\" && \n@request.body.organization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "updateRule": "organization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")) && \n@request.body.organization:changed = false", + "deleteRule": "organization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "name": "thing_type_operations", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation_tto_org", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_tto_name", + "max": 64, + "min": 1, + "name": "name", + "pattern": "^[a-z0-9_]+$", + "presentable": true, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "select_tto_capability", + "maxSelect": 1, + "name": "capability", + "presentable": false, + "required": true, + "system": false, + "type": "select", + "values": [ + "publish", + "subscribe", + "request", + "reply" + ] + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_tto_subject_suffix", + "max": 0, + "min": 1, + "name": "subject_suffix", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_tto_description", + "max": 0, + "min": 0, + "name": "description", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1612162572", + "hidden": false, + "id": "relation_tto_schema", + "maxSelect": 1, + "minSelect": 0, + "name": "schema", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate_tto_created", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate_tto_updated", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX idx_tto_org_name_cap ON thing_type_operations (organization, name, capability)" + ], + "system": false + }, + { + "id": "pbc_1612162572", + "listRule": "// Users see their active organization's schemas; a leaf node mirrors its own organization's schemas.\n(@request.auth.collectionName = \"users\" && \norganization = @request.auth.current_organization) || \n(@request.auth.collectionName = \"leaf_nodes\" && organization = @request.auth.organization)", + "viewRule": "// Users see their active organization's schemas; a leaf node mirrors its own organization's schemas.\n(@request.auth.collectionName = \"users\" && \norganization = @request.auth.current_organization) || \n(@request.auth.collectionName = \"leaf_nodes\" && organization = @request.auth.organization)", + "createRule": "@request.auth.collectionName = \"users\" && \n@request.body.organization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "updateRule": "organization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")) && \n@request.body.organization:changed = false", + "deleteRule": "organization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "name": "message_schemas", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation_ms_org", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_ms_namespace", + "max": 64, + "min": 1, + "name": "namespace", + "pattern": "^[a-z0-9_]+$", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_ms_name", + "max": 64, + "min": 1, + "name": "name", + "pattern": "^[a-z0-9_]+$", + "presentable": true, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_ms_version", + "max": 32, + "min": 1, + "name": "version", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "select_ms_format", + "maxSelect": 1, + "name": "format", + "presentable": false, + "required": true, + "system": false, + "type": "select", + "values": [ + "json_schema" + ] + }, + { + "hidden": false, + "id": "json_ms_schema", + "maxSize": 0, + "name": "schema", + "presentable": false, + "required": true, + "system": false, + "type": "json" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text_ms_description", + "max": 0, + "min": 0, + "name": "description", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate_ms_created", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate_ms_updated", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX idx_ms_identity ON message_schemas (organization, namespace, name, version)" + ], + "system": false + }, + { + "id": "pbc_3920100277", + "listRule": "// A leaf node authenticating as itself sees only its own record.\n// A user sees all leaf nodes belonging to their active organization.\n(@request.auth.collectionName = \"leaf_nodes\" && id = @request.auth.id) || \n(@request.auth.collectionName = \"users\" && organization = @request.auth.current_organization)", + "viewRule": "// A leaf node authenticating as itself sees only its own record.\n// A user sees all leaf nodes belonging to their active organization.\n(@request.auth.collectionName = \"leaf_nodes\" && id = @request.auth.id) || \n(@request.auth.collectionName = \"users\" && organization = @request.auth.current_organization)", + "createRule": "// Only Admins or Owners can create leaf nodes for their active organization.\n@request.auth.collectionName = \"users\" && \n@request.body.organization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "updateRule": "// 1. Must be Admin/Owner.\n// 2. Must belong to active org.\n// 3. Prevent organization field tampering.\n// 4. Freeze `code`: it is the KV key prefix and the JetStream domain suffix,\n// so changing it silently orphans everything the edge already wrote.\norganization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\")) && \n@request.body.organization:changed = false &&\n@request.body.code:changed = false", + "deleteRule": "// Only Admins or Owners can delete leaf nodes.\norganization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "name": "leaf_nodes", + "type": "auth", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": true, + "system": true, + "type": "bool" + }, + { + "hidden": true, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2873630990", + "hidden": false, + "id": "relation3253625724", + "maxSelect": 1, + "minSelect": 0, + "name": "organization", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1843675174", + "max": 0, + "min": 0, + "name": "description", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1997877400", + "max": 0, + "min": 0, + "name": "code", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text7100000001", + "max": 0, + "min": 0, + "name": "domain", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "json7100000002", + "maxSize": 0, + "name": "synced_collections", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1942858786", + "hidden": false, + "id": "relation1587448267", + "maxSelect": 1, + "minSelect": 0, + "name": "location", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_4097575383", + "hidden": false, + "id": "relation3991680165", + "maxSelect": 1, + "minSelect": 0, + "name": "nats_user", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_789448849", + "hidden": false, + "id": "relation324049829", + "maxSelect": 1, + "minSelect": 0, + "name": "nebula_host", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "json1326724116", + "maxSize": 0, + "name": "metadata", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "bool7200000002", + "name": "active", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_tokenKey_pbc_3920100277` ON `leaf_nodes` (`tokenKey`)", + "CREATE UNIQUE INDEX `idx_email_pbc_3920100277` ON `leaf_nodes` (`email`) WHERE `email` != ''", + "CREATE UNIQUE INDEX `idx_leaf_nodes_org_code` ON `leaf_nodes` (`organization`, `code`) WHERE `code` != ''" + ], + "system": false, + "authRule": "// A deactivated leaf node cannot obtain a new auth token, so leaf-sync stops\n// pulling config and GET /api/leaf/bootstrap stops answering it.\n//\n// Same caveat and same remedy as the things authRule: this is checked at the\n// auth endpoint only, and hooks/active_flag.go refreshes tokenKey and revokes\n// the linked NATS identity on deactivation so the edge box actually goes dark.\n// leaf_nodes has no member-level write path -- create/update are already\n// owner/admin -- so `active` needs no extra field freeze.\nactive = true", + "manageRule": "// Org Admins/Owners may manage (e.g. reset the PocketBase password of) their organization's leaf nodes.\norganization = @request.auth.current_organization && \n(@request.auth.memberships_via_user.organization ?= @request.auth.current_organization && \n (@request.auth.memberships_via_user.role ?= \"owner\" || @request.auth.memberships_via_user.role ?= \"admin\"))", + "authAlert": { + "enabled": true, + "emailTemplate": { + "subject": "Login from a new location", + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "oauth2": { + "mappedFields": { + "id": "", + "name": "", + "username": "", + "avatarURL": "" + }, + "enabled": false + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "mfa": { + "enabled": false, + "duration": 1800, + "rule": "" + }, + "otp": { + "enabled": false, + "duration": 180, + "length": 8, + "emailTemplate": { + "subject": "OTP for {APP_NAME}", + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "authToken": { + "duration": 604800 + }, + "passwordResetToken": { + "duration": 1800 + }, + "emailChangeToken": { + "duration": 1800 + }, + "verificationToken": { + "duration": 259200 + }, + "fileToken": { + "duration": 180 + }, + "verificationTemplate": { + "subject": "Verify your {APP_NAME} email", + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "resetPasswordTemplate": { + "subject": "Reset your {APP_NAME} password", + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "confirmEmailChangeTemplate": { + "subject": "Confirm your {APP_NAME} new email address", + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + } +]