@nebulr-group/bridge-cli 0.6.0-beta.7 → 0.6.0-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -2
- package/dist/auth/api-client.d.ts +2 -0
- package/dist/auth/api-client.d.ts.map +1 -1
- package/dist/auth/api-client.js.map +1 -1
- package/dist/auth/workspace-client.d.ts +70 -0
- package/dist/auth/workspace-client.d.ts.map +1 -0
- package/dist/auth/workspace-client.js +105 -0
- package/dist/auth/workspace-client.js.map +1 -0
- package/dist/bin.js +4 -1
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +5 -14
- package/dist/cli.js.map +1 -1
- package/dist/commands/app.command.d.ts.map +1 -1
- package/dist/commands/app.command.js +117 -1
- package/dist/commands/app.command.js.map +1 -1
- package/dist/commands/auth/login.command.d.ts +13 -1
- package/dist/commands/auth/login.command.d.ts.map +1 -1
- package/dist/commands/auth/login.command.js +17 -3
- package/dist/commands/auth/login.command.js.map +1 -1
- package/dist/commands/auth/status.command.js +14 -0
- package/dist/commands/auth/status.command.js.map +1 -1
- package/dist/commands/check.command.d.ts +10 -0
- package/dist/commands/check.command.d.ts.map +1 -0
- package/dist/commands/check.command.js +95 -0
- package/dist/commands/check.command.js.map +1 -0
- package/dist/commands/plan.command.d.ts +19 -5
- package/dist/commands/plan.command.d.ts.map +1 -1
- package/dist/commands/plan.command.js +40 -26
- package/dist/commands/plan.command.js.map +1 -1
- package/dist/config.d.ts +46 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +190 -47
- package/dist/config.js.map +1 -1
- package/dist/credentials.d.ts +21 -0
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +21 -0
- package/dist/credentials.js.map +1 -1
- package/dist/gate-rules.d.ts +51 -0
- package/dist/gate-rules.d.ts.map +1 -0
- package/dist/gate-rules.js +171 -0
- package/dist/gate-rules.js.map +1 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +7 -0
- package/dist/output.js.map +1 -1
- package/dist/program.d.ts +17 -0
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +76 -0
- package/dist/program.js.map +1 -1
- package/dist/prompts/auth-master-integration-prompt.md +5 -2
- package/dist/prompts/billing/master.md +5 -2
- package/dist/prompts/decisions/feature-control.md +6 -1
- package/dist/prompts/decisions/going-live.md +1 -1
- package/dist/prompts/decisions/payments.md +9 -4
- package/dist/prompts/decisions/roles.md +2 -2
- package/dist/prompts/decisions/teams.md +4 -3
- package/dist/prompts/fit-together.md +22 -4
- package/dist/prompts/flags/master.md +3 -0
- package/dist/prompts/mcp-tools.json +7 -2
- package/dist/prompts/mechanisms.md +9 -5
- package/package.json +1 -1
|
@@ -2,7 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Read this before any decision guide that touches who gets what. Every other guide builds on it. MCP: resource `bridge://guides/fit-together`. CLI: `bridge guide fit-together`.
|
|
4
4
|
|
|
5
|
-
## The rule
|
|
5
|
+
## The one rule for app code
|
|
6
|
+
|
|
7
|
+
**Every gate in app code is a flag.** A link, a page, a button, an endpoint: if some people get it and others do not, the code asks a flag, and the flag's rule says why: a privilege (`privileges contains "USER_WRITE"`), a plan feature (`bridge:billing.entitlement.analytics eq true`) or a rollout. App code never reads a role, a privilege list, the plan or a plan feature to decide what someone may see or do.
|
|
8
|
+
|
|
9
|
+
Two things are not gates and are not flags: **numbers** are plan limits (`<QuotaGate>` in the browser, `@RequireQuota` on the backend), and **permission on one specific record** stays in app code ("only the author edits their post").
|
|
10
|
+
|
|
11
|
+
Before calling the work done, run `npx @nebulr-group/bridge-cli check gates` in the project. It lists every direct role, privilege, plan or plan-feature check with the flag to use instead. Fix each one.
|
|
12
|
+
|
|
13
|
+
- **SvelteKit:** a piece of UI or a nav link is `<FeatureFlag key="…">`; a page is a route rule `{ match: '/admin/*', featureFlag: 'admin-area', redirectTo: '/' }`.
|
|
14
|
+
- **NestJS:** an endpoint is `@RequireFeatureFlag('…')` on the handler.
|
|
15
|
+
|
|
16
|
+
## Why a flag
|
|
6
17
|
|
|
7
18
|
Features should be controlled with feature flags. A flag can switch a route, an API endpoint, a feature or a single piece of code; its rules change without a release and update live when someone's role or plan changes. If a plan sells the feature, include it on the plan as well. This is valuable because the plan is where the customer sees what they are buying: the pricing table, the upgrade dialog and the "not on your plan" reason all read the plan's list, and the flag's rule points at that same list, so changing what Pro includes is one edit in one place.
|
|
8
19
|
|
|
@@ -13,6 +24,7 @@ Features should be controlled with feature flags. A flag can switch a route, an
|
|
|
13
24
|
| Is this route, API endpoint, feature or piece of code on for them? | a **flag** | The code asks for the flag by key; the flag's rule decides |
|
|
14
25
|
| Does their plan sell it? | the plan's **features list**, read by the flag's rule | `bridge plan feature add pro analytics`, then the rule `bridge:billing.entitlement.analytics eq true` |
|
|
15
26
|
| How many do they get (projects, exports, seats)? | a **plan limit** | `bridge plan quota set pro --metric exports --limit 100 --policy hard` |
|
|
27
|
+
| How many people fit in a workspace? | a **plan limit** counted from membership (seats) | `bridge plan quota set pro --metric seats --limit 10 --policy hard --kind gauge --source membership` |
|
|
16
28
|
| Who is this person, and may they do this kind of thing? | a flag rule on a **privilege** (preferred) or a **role** | `privileges contains "REPORTS_VIEW"`, or `user.role eq "ADMIN"` |
|
|
17
29
|
| May this person change this one record? | the app's own code | "Only the author edits their post" |
|
|
18
30
|
|
|
@@ -21,8 +33,8 @@ The flag's rule never names plans. It points at `bridge:billing.entitlement.<fea
|
|
|
21
33
|
## Where a flag is not the tool
|
|
22
34
|
|
|
23
35
|
- **Numbers are plan limits.** "100 exports a month" or "10 projects" is a quota on the plan, not a flag. Bridge refuses at the limit and the upgrade dialog explains it.
|
|
36
|
+
- **Seats are a plan limit too, counted from membership.** Bridge counts the workspace's active members; the check sits where invites happen (the team page's seat-limit setting, or `@RequireQuota('seats')` on the app's own invite handler), never in a flag or an entitlement.
|
|
24
37
|
- **Permission on one specific record stays in app code.** Whether this person may edit *this* invoice depends on data only the app has (who wrote it, which team owns it). The flag decides whether invoice editing exists for them at all; the app decides the record.
|
|
25
|
-
- **Checking a plan feature without a flag is the exception.** `@RequireEntitlement('analytics')` on the backend and `<Entitled to="analytics">` or `$entitlements.can('analytics')` in the UI read the plan's list directly. They work, and they are for the rare case where the developer asks for no flag. Use a flag by default: it can also carry a rollout, a trial group or a one-customer exception later, without a code change.
|
|
26
38
|
|
|
27
39
|
## Access by who someone is
|
|
28
40
|
|
|
@@ -30,7 +42,8 @@ Control it with a flag rule, never with a role check written into the app's code
|
|
|
30
42
|
|
|
31
43
|
- **Prefer a privilege rule** (`privileges contains "REPORTS_VIEW"`) over a role rule (`user.role eq "ADMIN"`). A privilege rule keeps working when roles are renamed or reshuffled. `contains` on `privileges` means the person holds exactly that key: `REPORTS_VIEW` does not match `REPORTS_VIEW_ALL`.
|
|
32
44
|
- **A role rule is fine when the developer means the role itself.** Use the role key exactly as `list_roles` shows it; `ADMIN` and `admin` are different.
|
|
33
|
-
- **An admin-only page is a flag route rule, not a check on the page.** In SvelteKit: `{ match: '/admin/*', featureFlag: 'admin-area', redirectTo: '/' }`, with the flag `admin-area` ruled on a privilege. On the NestJS backend: `@
|
|
45
|
+
- **An admin-only page is a flag route rule, not a check on the page.** In SvelteKit: `{ match: '/admin/*', featureFlag: 'admin-area', redirectTo: '/' }`, with the flag `admin-area` ruled on a privilege. On the NestJS backend: `@RequireFeatureFlag('admin-area')` on the handler. Both read the same rule.
|
|
46
|
+
- **The team page is the same.** The team link and the team route are gated by a flag ruled on the privilege that manages people (for example `privileges contains "USER_WRITE"`, after `bridge role list`), never by a list of role names.
|
|
34
47
|
- **Roles are the app's own.** What a role can do is only ever "in the default setup". Read the app's real roles and privileges with `list_roles` / `bridge role list` before writing any rule, and never assume what a role grants from its name. In the default setup a new app has Owner, Admin and Member, and Member is the default for everyone after the first person in a workspace.
|
|
35
48
|
|
|
36
49
|
## The backend sees the same rule
|
|
@@ -42,7 +55,7 @@ A flag rule on a role, a privilege, the plan or a plan feature gives the same an
|
|
|
42
55
|
Ask the developer first: **"Does this action call your server?"** Then:
|
|
43
56
|
|
|
44
57
|
- **Yes, it calls the backend:** the backend counts it. One decorator on the handler (`@RequireQuota('exports')` in NestJS) refuses at the limit with `402` and records the use after a successful request. The frontend only shows the count and the upgrade dialog; it does not report the same metric.
|
|
45
|
-
- **No, it happens in the browser** (local-first, data on the device, no server of the developer's own): the browser counts it. `bridge.usage.report('exports')` for something that happened, `bridge.usage.set('projects', n)` for how many exist now, and `<QuotaGate metric="exports">` around the button so it stops at the limit. This is a first-class way to run limits.
|
|
58
|
+
- **No, it happens in the browser** (local-first, data on the device, no server of the developer's own): the browser counts it. `bridge.usage.report('exports')` for something that happened, `bridge.usage.set('projects', n)` for how many exist now, and `<QuotaGate metric="exports">` around the button so it stops at the limit. This is a complete, first-class way to run limits.
|
|
46
59
|
|
|
47
60
|
Never count one metric in both places: it is counted twice. Never describe browser counting as a lesser or temporary option; ask the question above and follow the answer.
|
|
48
61
|
|
|
@@ -67,6 +80,11 @@ No upgrade dialog opens by itself. It opens when someone opens a gated route, cl
|
|
|
67
80
|
1. `bridge plan feature add pro analytics --name "Analytics"`, and a flag `analytics` with the rule `bridge:billing.entitlement.analytics eq true`.
|
|
68
81
|
2. `bridge role list` first. Then a flag `workspace-settings` with a privilege rule, e.g. `privileges contains "TENANT_WRITE"` if that is the privilege the app's roles use for it, and a route rule on `/settings/*`.
|
|
69
82
|
3. Ask whether exporting calls the developer's server. If it does: `bridge plan quota set pro --metric exports --limit 100 --policy hard` and `@RequireQuota('exports')` on the export handler. If it does not: the same quota, `bridge.usage.report('exports')` in the browser and `<QuotaGate metric="exports">` around the button.
|
|
83
|
+
4. `npx @nebulr-group/bridge-cli check gates`: nothing left that reads a role, a privilege, the plan or a plan feature directly.
|
|
84
|
+
|
|
85
|
+
## Exceptions
|
|
86
|
+
|
|
87
|
+
**Checking a plan feature without a flag is the exception.** `@RequireEntitlement('analytics')` on the backend and `<Entitled to="analytics">` or `$entitlements.can('analytics')` in the UI read the plan's list directly. They are for the rare case where the developer explicitly asks for no flag, and they print a one-time note in development. Mark each such line `// bridge-gate-exception: <reason>` so `check gates` leaves it. Use a flag by default: it can also carry a rollout, a trial group or a one-customer exception later, without a code change.
|
|
70
88
|
|
|
71
89
|
## Where each piece is decided
|
|
72
90
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
You are integrating **Bridge Feature Flags** into a user's application — the flags-only path. No auth scaffolding, no billing components. If the user also wants auth or billing, run `bridge guide` (auth master) or `bridge guide billing` afterwards.
|
|
4
4
|
|
|
5
|
+
> **The one rule for app code: every gate is a flag.** A link, a page, a button, an endpoint: if some people get it and others do not, the code asks a flag, and the flag's rule says why (a privilege, a plan feature, a rollout). App code never reads a role, a privilege list, the plan or a plan feature to decide what someone may see or do. Numbers are plan limits, and permission on one specific record stays in app code. `bridge guide fit-together` has the whole rule; the last verification step runs `npx @nebulr-group/bridge-cli check gates`.
|
|
6
|
+
|
|
5
7
|
Flags are the standard way to control a route, an API endpoint, a feature or a piece of code. A feature a plan sells is listed on the plan and the flag's rule points at `bridge:billing.entitlement.<feature>`, never at plan names; access by who someone is is a rule on a privilege (preferred) or a role, never a role check in code. `bridge guide fit-together` has the whole rule; `bridge guide decision feature-control` has the questions to ask.
|
|
6
8
|
|
|
7
9
|
This master prompt orchestrates discovery, confirmation, and verification. The **actual install commands, provider shapes, and runnable code snippets live in the per-framework prompts** that ship in each plugin repo's `mcp/` folder. This master does the planning; the per-framework prompts do the wiring.
|
|
@@ -195,6 +197,7 @@ For each integrated project:
|
|
|
195
197
|
1. **Build check** — run the project's build command (from `package.json` scripts)
|
|
196
198
|
2. **Eval check** — read one flag (existing or freshly created) and confirm it returns the expected value
|
|
197
199
|
3. **Context check** — confirm `identity` is set when the project uses any rolled-out rule
|
|
200
|
+
4. **Gate check** — `npx @nebulr-group/bridge-cli check gates` in the project: every direct role, privilege, plan or plan-feature check it lists becomes a flag. Run it again until it is clean
|
|
198
201
|
|
|
199
202
|
If anything fails, diagnose and fix before moving on.
|
|
200
203
|
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"The names of the Bridge MCP server's tools, and nothing else. The bundled guides are tested against this list: a guide that names a tool not in it fails src/__tests__/guide-decisions.test.ts.",
|
|
4
4
|
"Source: the `bridgeTools` registry in bridge-api, microservices/account/nebulr-api/mcp/tools/register.ts.",
|
|
5
5
|
"Generated, never edited by hand: in bridge-api run `bun scripts/generate-mcp-tools-json.ts > <bridge-cli>/bridge-cli/prompts/mcp-tools.json` (in the bridge-api container).",
|
|
6
|
-
"The count must equal the number of entries in `bridgeTools` (
|
|
6
|
+
"The count must equal the number of entries in `bridgeTools` (76 at the source below). bridge-api's guide-currency.spec.ts fails when this file and the registry disagree."
|
|
7
7
|
],
|
|
8
|
-
"source": "bridge-api feature/mcp-journey
|
|
8
|
+
"source": "bridge-api feature/mcp-journey 75c0a1f",
|
|
9
9
|
"tools": [
|
|
10
10
|
"add_plan_feature",
|
|
11
11
|
"add_redirect_uri",
|
|
@@ -16,11 +16,14 @@
|
|
|
16
16
|
"create_app",
|
|
17
17
|
"create_feature_flag",
|
|
18
18
|
"create_plan",
|
|
19
|
+
"create_privilege",
|
|
19
20
|
"create_role",
|
|
20
21
|
"create_tenant",
|
|
21
22
|
"create_test_user",
|
|
22
23
|
"debug_auth_issue",
|
|
23
24
|
"delete_feature_flag",
|
|
25
|
+
"delete_privilege",
|
|
26
|
+
"delete_role",
|
|
24
27
|
"delete_tenant",
|
|
25
28
|
"diagnose_integration",
|
|
26
29
|
"evaluate_feature_flag",
|
|
@@ -47,6 +50,7 @@
|
|
|
47
50
|
"list_feature_flags",
|
|
48
51
|
"list_plan_quotas",
|
|
49
52
|
"list_plans",
|
|
53
|
+
"list_privileges",
|
|
50
54
|
"list_redirect_uris",
|
|
51
55
|
"list_roles",
|
|
52
56
|
"list_tenants",
|
|
@@ -73,6 +77,7 @@
|
|
|
73
77
|
"update_feature_flag",
|
|
74
78
|
"update_password_policy",
|
|
75
79
|
"update_plan",
|
|
80
|
+
"update_privilege",
|
|
76
81
|
"update_role",
|
|
77
82
|
"update_tenant",
|
|
78
83
|
"update_user",
|
|
@@ -104,7 +104,7 @@ export class ExportsController {
|
|
|
104
104
|
|
|
105
105
|
Ask the developer first: **does this action call your server?**
|
|
106
106
|
|
|
107
|
-
- **It calls your backend:** the
|
|
107
|
+
- **It calls your backend:** the backend handler counts it and refuses at the limit, because that is where the action happens. The frontend shows the count and the upgrade dialog, and does not count the same metric again.
|
|
108
108
|
- **It happens in the browser** and never reaches a server of yours (local-first, data on the device): the browser counts it and gates the button (section 5). That is a first-class setup.
|
|
109
109
|
|
|
110
110
|
Never both for one metric: it would be counted twice.
|
|
@@ -149,13 +149,13 @@ remove() { /* … */ }
|
|
|
149
149
|
|
|
150
150
|
`current` returns your own count, and that count is what the limit is compared against, so Bridge's copy heals itself if it ever missed an update. There is no decrement and no reservation: every create and delete sends the whole count.
|
|
151
151
|
|
|
152
|
-
**Seats** (`
|
|
152
|
+
**Seats** are a gauge the app names (e.g. `seats`) and marks as counted from membership: `bridge plan quota set <plan> --metric seats --limit 10 --policy hard --kind gauge --source membership`. Bridge answers it with the workspace's active members, pending invites included, read fresh each time; the app never reports it. Check it where invites happen: on the built-in team page with `<TeamManagementPanel seatsMetric="seats" />`, or with `@RequireQuota('seats')` on your own invite handler (it checks and writes nothing). Bridge's invite API does not refuse at the limit by itself. Never gate seats with a flag or an entitlement.
|
|
153
153
|
|
|
154
154
|
`bridge plan quota list` shows every metric the app's plans limit, with its kind.
|
|
155
155
|
|
|
156
156
|
**Every hard quota is also an entitlement** with the same name, true while there is room. So never pair `@RequireEntitlement('exports')` with `@RequireQuota('exports')`: at the cap the entitlement answers `403` before the quota can answer the `402` the frontend knows how to upsell. `@RequireQuota` is for a *limit*; `@RequireEntitlement` checks a plan *feature* (`analytics`, `sso`) without a flag, which is the exception (next paragraph).
|
|
157
157
|
|
|
158
|
-
**A plan feature goes in the plan's features list.** `bridge plan feature add pro analytics --name "Analytics"` makes `analytics` true on `pro`, and false on every other plan. The pricing table and the upgrade dialog name it from the same list. Control the feature with a flag whose rule is `bridge:billing.entitlement.analytics eq true`, so changing what Pro sells is one edit on the plan and no flag rule has to follow. Checking it directly, without a flag, is the exception
|
|
158
|
+
**A plan feature goes in the plan's features list.** `bridge plan feature add pro analytics --name "Analytics"` makes `analytics` true on `pro`, and false on every other plan. The pricing table and the upgrade dialog name it from the same list. Control the feature with a flag whose rule is `bridge:billing.entitlement.analytics eq true`, so changing what Pro sells is one edit on the plan and no flag rule has to follow. Checking it directly, without a flag, is the exception (section Exceptions, at the end). A hard quota with limit 1 that nothing counts still works the same way; it is the older way to do this. `app_active` is always there: true while the workspace's subscription is active, trialing, past due or cancelling at period end.
|
|
159
159
|
|
|
160
160
|
## 4. Three ways to handle a limit in the UI
|
|
161
161
|
|
|
@@ -165,7 +165,7 @@ Pick the lowest level that does the job. Each is optional; level 0 is on without
|
|
|
165
165
|
|---|---|---|
|
|
166
166
|
| **0 — nothing** | a plain button calling your API | Your backend refuses at the cap (`402`), and `<BridgeBootstrap>` opens an **upgrade dialog** naming the metric, linking to the subscription page. A workspace member who cannot manage billing is told to ask the owner instead |
|
|
167
167
|
| **1 — one component** | `<QuotaGate metric="tickets">…</QuotaGate>` around the button; `<FeatureFlag key="analytics" upgrade>…</FeatureFlag>` around a paid feature | The button is disabled at a known hard cap with an upgrade line beside it; the paid feature shows on a plan that includes it, and elsewhere an "Upgrade to use this" button that opens the dialog when clicked |
|
|
168
|
-
| **2 — your own UI** | `useQuota('tickets')
|
|
168
|
+
| **2 — your own UI** | `useQuota('tickets')`, and `<FeatureFlag key="analytics">` around what the plan sells | Whatever you build from the live numbers |
|
|
169
169
|
|
|
170
170
|
```svelte
|
|
171
171
|
<script lang="ts">
|
|
@@ -211,7 +211,7 @@ await bridge.usage.set('projects', projects.length); // a gauge: how many exis
|
|
|
211
211
|
|
|
212
212
|
Put `<QuotaGate metric="exports">` around the button (section 4, level 1) so it stops at the limit, and the upgrade line sells the next plan.
|
|
213
213
|
|
|
214
|
-
This is a first-class way to run limits.
|
|
214
|
+
This is a complete, first-class way to run limits. When the action does call your backend, count it there instead (section 2), never in both places.
|
|
215
215
|
|
|
216
216
|
## 6. Four levels of customising Bridge's pages
|
|
217
217
|
|
|
@@ -292,3 +292,7 @@ All default rules use `:where()`, so any selector of yours wins without `!import
|
|
|
292
292
|
| Backend sign-in and route protection | `bridge guide nestjs` |
|
|
293
293
|
| Feature flags | `bridge guide flags` |
|
|
294
294
|
| Proving sign-in works | `bridge test-user create`, then `bridge test-user verify` |
|
|
295
|
+
|
|
296
|
+
## Exceptions
|
|
297
|
+
|
|
298
|
+
Every gate in app code is a flag (`bridge guide fit-together`). Checking a plan feature without a flag (`@RequireEntitlement` on the backend, `<Entitled>` or `$entitlements.can` in the UI) exists for the rare case where the developer explicitly asks for no flag. It prints a one-time note in development; mark the line `// bridge-gate-exception: <reason>` so `check gates` leaves it.
|