EXTENSION for TreeOS
user-tiers
The access control layer between users and features. Every user has a tier stored in metadata.tiers.plan (defaults to 'basic' if unset). Extensions declare which tiers unlock their features by calling registerFeature(featureName, allowedTiers) during init. The hasAccess(userId, feature) export checks the user's current tier against the registered tier list for that feature. Unknown features return true (permissive default: if nobody registered the feature, nobody restricted it). Built-in feature gates: auto-place requires standard or premium, file-upload requires standard or premium. Other extensions add their own gates at boot. The billing extension calls setUserTier when a subscription changes. Admin users can set any user's tier via PUT /user/:userId/tier. The CLI exposes 'tier' to check your current plan. No models of its own. Tier data lives in user metadata. The exports (getUserTier, hasAccess, setUserTier, registerFeature) are the contract other extensions depend on. Energy reads the tier for daily limits. Billing writes the tier on payment. Everything else checks access through hasAccess.
v1.0.3 by TreeOS Site 0 downloads 4 files 168 lines 5.6 KB published 38d ago
treeos ext install user-tiers
View changelog

Manifest

Provides

  • routes
  • 1 CLI commands

Requires

  • services: protocol
  • models: User

Optional

  • extensions: treeos-base
SHA256: a20edf5a324d0592da8a72309330dab073a587ed80c02538e0a1b1c971800d12

Dependents

1 package depend on this

PackageTypeRelationship
treeos v1.0.1osstandalone

CLI Commands

CommandMethodDescription
tierGETShow your current tier

Source Code

1import { getUserMeta, setUserMeta } from "../../seed/tree/userMetadata.js";
2
3let User;
4
5export function setModels(models) {
6  User = models.User;
7}
8
9// Feature access map. Extensions can register their own features via registerFeature().
10const FEATURE_ACCESS = {
11  "auto-place": ["standard", "premium"],
12  "file-upload": ["standard", "premium"],
13};
14
15/**
16 * Register a feature with its required tiers.
17 * Called by extensions during init to declare what tiers unlock their features.
18 */
19export function registerFeature(feature, tiers) {
20  FEATURE_ACCESS[feature] = tiers;
21}
22
23/**
24 * Get a user's current tier. Returns "basic" if not set.
25 */
26export async function getUserTier(userId) {
27  const user = await User.findById(userId).select("metadata").lean();
28  if (!user) return "basic";
29  const tiers = getUserMeta(user, "tiers");
30  return tiers.plan || "basic";
31}
32
33/**
34 * Check if a user's tier grants access to a feature.
35 * Returns true if the feature isn't registered (permissive default).
36 */
37export async function hasAccess(userId, feature) {
38  const allowedTiers = FEATURE_ACCESS[feature];
39  if (!allowedTiers) return true; // unknown feature = no restriction
40
41  const tier = await getUserTier(userId);
42  return allowedTiers.includes(tier);
43}
44
45/**
46 * Set a user's tier. Called by billing or admin.
47 */
48export async function setUserTier(userId, tier) {
49  const { batchSetUserMeta } = await import("../../seed/tree/userMetadata.js");
50  await batchSetUserMeta(userId, "tiers", { plan: tier });
51  return tier;
52}
53
1import log from "../../seed/log.js";
2import { setModels, getUserTier, hasAccess, setUserTier, registerFeature } from "./core.js";
3import { getExtension } from "../loader.js";
4import { getUserMeta } from "../../seed/tree/userMetadata.js";
5
6export async function init(core) {
7  setModels(core.models);
8
9  log.info("UserTiers", "Tier management loaded");
10
11  const router = (await import("./routes.js")).default(core);
12
13  // Register plan badge on user profile (replaces the default "User/Admin" badge)
14  try {
15    const treeos = getExtension("treeos-base");
16    treeos?.exports?.registerSlot?.("user-profile-badge", "user-tiers", ({ userId, queryString, user }) => {
17      const plan = (getUserMeta(user, "tiers").plan || "basic").toLowerCase();
18      const label = plan.charAt(0).toUpperCase() + plan.slice(1);
19      return `<a href="/api/v1/user/${userId}/energy${queryString}">
20        <span class="plan-badge plan-${plan}">${label} Plan</span>
21      </a>`;
22    }, { priority: 10 });
23  } catch {}
24
25  return {
26    router,
27    exports: { getUserTier, hasAccess, setUserTier, registerFeature },
28  };
29}
30
1export default {
2  name: "user-tiers",
3  version: "1.0.3",
4  builtFor: "TreeOS",
5  description:
6    "The access control layer between users and features. Every user has a tier stored " +
7    "in metadata.tiers.plan (defaults to 'basic' if unset). Extensions declare which " +
8    "tiers unlock their features by calling registerFeature(featureName, allowedTiers) " +
9    "during init. The hasAccess(userId, feature) export checks the user's current tier " +
10    "against the registered tier list for that feature. Unknown features return true " +
11    "(permissive default: if nobody registered the feature, nobody restricted it).\n\n" +
12    "Built-in feature gates: auto-place requires standard or premium, file-upload " +
13    "requires standard or premium. Other extensions add their own gates at boot. The " +
14    "billing extension calls setUserTier when a subscription changes. Admin users can " +
15    "set any user's tier via PUT /user/:userId/tier. The CLI exposes 'tier' to check " +
16    "your current plan. No models of its own. Tier data lives in user metadata. The " +
17    "exports (getUserTier, hasAccess, setUserTier, registerFeature) are the contract " +
18    "other extensions depend on. Energy reads the tier for daily limits. Billing " +
19    "writes the tier on payment. Everything else checks access through hasAccess.",
20
21  needs: {
22    models: ["User"],
23    services: ["protocol"],
24  },
25
26  optional: {
27    extensions: ["treeos-base"],
28  },
29
30  provides: {
31    routes: "./routes.js",
32    tools: false,
33    modes: false,
34    jobs: false,
35    orchestrator: false,
36    energyActions: {},
37    sessionTypes: {},
38    cli: [
39      { command: "tier", scope: ["home"], description: "Show your current tier", method: "GET", endpoint: "/user/:userId/tier" },
40    ],
41  },
42};
43
1import express from "express";
2import authenticate from "../../seed/middleware/authenticate.js";
3import { getUserTier, setUserTier } from "./core.js";
4
5export default function (core) {
6  const router = express.Router();
7  const { sendOk, sendError, ERR } = core.protocol;
8  const User = core.models.User;
9
10  // GET /user/:userId/tier
11  router.get("/user/:userId/tier", authenticate, async (req, res) => {
12    try {
13      const tier = await getUserTier(req.params.userId);
14      sendOk(res, { userId: req.params.userId, tier });
15    } catch (err) {
16      sendError(res, 500, ERR.INTERNAL, err.message);
17    }
18  });
19
20  // PUT /user/:userId/tier - admin only
21  router.put("/user/:userId/tier", authenticate, async (req, res) => {
22    try {
23      const admin = await User.findById(req.userId).select("isAdmin").lean();
24      if (!admin?.isAdmin) {
25        return sendError(res, 403, ERR.FORBIDDEN, "Admin access required");
26      }
27
28      const { tier } = req.body;
29      if (!tier || typeof tier !== "string") {
30        return sendError(res, 400, ERR.INVALID_INPUT, "Tier is required");
31      }
32
33      const result = await setUserTier(req.params.userId, tier);
34      sendOk(res, { userId: req.params.userId, tier: result });
35    } catch (err) {
36      sendError(res, 500, ERR.INTERNAL, err.message);
37    }
38  });
39
40  return router;
41}
42

Versions

Version Published Downloads
1.0.3 38d ago 0
1.0.2 47d ago 0
1.0.0 48d ago 0
0 stars
0 flags
React from the CLI: treeos ext star user-tiers

Comments

Loading comments...

Post comments from the CLI: treeos ext comment user-tiers "your comment"
Max 3 comments per extension. One star and one flag per user.