@nebulr-group/bridge-cli 0.6.0-beta.6 → 0.6.0-beta.8

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.
Files changed (46) hide show
  1. package/README.md +6 -0
  2. package/dist/commands/check.command.d.ts +10 -0
  3. package/dist/commands/check.command.d.ts.map +1 -0
  4. package/dist/commands/check.command.js +95 -0
  5. package/dist/commands/check.command.js.map +1 -0
  6. package/dist/commands/guide.command.d.ts +5 -0
  7. package/dist/commands/guide.command.d.ts.map +1 -1
  8. package/dist/commands/guide.command.js +28 -4
  9. package/dist/commands/guide.command.js.map +1 -1
  10. package/dist/commands/journeys.d.ts.map +1 -1
  11. package/dist/commands/journeys.js +4 -2
  12. package/dist/commands/journeys.js.map +1 -1
  13. package/dist/commands/plan.command.d.ts +47 -5
  14. package/dist/commands/plan.command.d.ts.map +1 -1
  15. package/dist/commands/plan.command.js +197 -34
  16. package/dist/commands/plan.command.js.map +1 -1
  17. package/dist/commands/role.command.d.ts.map +1 -1
  18. package/dist/commands/role.command.js +19 -1
  19. package/dist/commands/role.command.js.map +1 -1
  20. package/dist/commands/user.command.d.ts.map +1 -1
  21. package/dist/commands/user.command.js +2 -1
  22. package/dist/commands/user.command.js.map +1 -1
  23. package/dist/gate-rules.d.ts +51 -0
  24. package/dist/gate-rules.d.ts.map +1 -0
  25. package/dist/gate-rules.js +171 -0
  26. package/dist/gate-rules.js.map +1 -0
  27. package/dist/program.d.ts.map +1 -1
  28. package/dist/program.js +3 -0
  29. package/dist/program.js.map +1 -1
  30. package/dist/prompts/auth-master-integration-prompt.md +4 -1
  31. package/dist/prompts/billing/master.md +16 -6
  32. package/dist/prompts/decisions/feature-control.md +46 -20
  33. package/dist/prompts/decisions/going-live.md +1 -1
  34. package/dist/prompts/decisions/login.md +1 -1
  35. package/dist/prompts/decisions/look-and-feel.md +1 -1
  36. package/dist/prompts/decisions/payments.md +20 -12
  37. package/dist/prompts/decisions/roles.md +22 -16
  38. package/dist/prompts/decisions/teams.md +7 -6
  39. package/dist/prompts/fit-together.md +97 -0
  40. package/dist/prompts/flags/master.md +6 -1
  41. package/dist/prompts/guide-coverage.json +6 -1
  42. package/dist/prompts/mcp-tools.json +6 -2
  43. package/dist/prompts/mechanisms.md +28 -14
  44. package/dist/prompts/orientation.md +5 -3
  45. package/dist/prompts/product-decisions.json +2 -2
  46. package/package.json +1 -1
@@ -0,0 +1,51 @@
1
+ /**
2
+ * TBP-705 — the owner's rule for app code (2026-09-29): every gate in app code
3
+ * is a flag. Its rule says why (a privilege, a plan feature, a rollout); app
4
+ * code never reads a role, a privilege list, the plan or a plan feature to
5
+ * decide what someone may see or do. Numbers (plan limits) and permission on
6
+ * one specific record are not gates.
7
+ *
8
+ * Two checks live here, from one list of what a direct check looks like:
9
+ * - `codeGateFindings`: `bridge check gates`, run by the agent as the last
10
+ * step of every integration, over the developer's own sources;
11
+ * - `docGateViolations`: the currency test over the guides we serve, so no
12
+ * guide teaches the thing the command then flags.
13
+ *
14
+ * bridge-svelte and bridge-nestjs carry a copy of the doc pattern list in
15
+ * their own currency tests. This file is the source; keep them the same.
16
+ */
17
+ export type GateKind = 'role' | 'privilege' | 'plan-feature' | 'plan-name' | 'removed-api';
18
+ export type GateFinding = {
19
+ file: string;
20
+ line: number;
21
+ kind: GateKind;
22
+ text: string;
23
+ /** What to use instead, in words an agent can act on. */
24
+ instead: string;
25
+ };
26
+ /** Written on the line, or the line above, to mark a direct check the developer asked for. */
27
+ export declare const GATE_EXCEPTION_MARKER = "bridge-gate-exception";
28
+ type CodeCheck = {
29
+ kind: GateKind;
30
+ pattern: RegExp;
31
+ instead: (match: RegExpExecArray) => string;
32
+ };
33
+ export declare const CODE_CHECKS: CodeCheck[];
34
+ /** Every direct gate check in one source file. */
35
+ export declare function codeGateFindings(source: string, file: string): GateFinding[];
36
+ export declare const SOURCE_EXTENSIONS: string[];
37
+ export declare const SKIPPED_DIRS: Set<string>;
38
+ export declare function isScannedSource(name: string): boolean;
39
+ export declare const DOC_FORBIDDEN: Array<[string, RegExp]>;
40
+ export type DocViolation = {
41
+ line: number;
42
+ rule: string;
43
+ text: string;
44
+ };
45
+ /**
46
+ * Every sentence in a guide that teaches a direct check, outside the one
47
+ * section a guide may have whose heading says "Exceptions".
48
+ */
49
+ export declare function docGateViolations(markdown: string): DocViolation[];
50
+ export {};
51
+ //# sourceMappingURL=gate-rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-rules.d.ts","sourceRoot":"","sources":["../src/gate-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,cAAc,GAAG,WAAW,GAAG,aAAa,CAAC;AAE3F,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,8FAA8F;AAC9F,eAAO,MAAM,qBAAqB,0BAA0B,CAAC;AA2B7D,KAAK,SAAS,GAAG;IACf,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,MAAM,CAAC;CAC7C,CAAC;AAKF,eAAO,MAAM,WAAW,EAAE,SAAS,EA+BlC,CAAC;AAIF,kDAAkD;AAClD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAgB5E;AAED,eAAO,MAAM,iBAAiB,UAAoE,CAAC;AACnG,eAAO,MAAM,YAAY,aAGvB,CAAC;AAEH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAIrD;AAID,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CA8BjD,CAAC;AAOF,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAExE;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,EAAE,CAsBlE"}
@@ -0,0 +1,171 @@
1
+ /**
2
+ * TBP-705 — the owner's rule for app code (2026-09-29): every gate in app code
3
+ * is a flag. Its rule says why (a privilege, a plan feature, a rollout); app
4
+ * code never reads a role, a privilege list, the plan or a plan feature to
5
+ * decide what someone may see or do. Numbers (plan limits) and permission on
6
+ * one specific record are not gates.
7
+ *
8
+ * Two checks live here, from one list of what a direct check looks like:
9
+ * - `codeGateFindings`: `bridge check gates`, run by the agent as the last
10
+ * step of every integration, over the developer's own sources;
11
+ * - `docGateViolations`: the currency test over the guides we serve, so no
12
+ * guide teaches the thing the command then flags.
13
+ *
14
+ * bridge-svelte and bridge-nestjs carry a copy of the doc pattern list in
15
+ * their own currency tests. This file is the source; keep them the same.
16
+ */
17
+ /** Written on the line, or the line above, to mark a direct check the developer asked for. */
18
+ export const GATE_EXCEPTION_MARKER = 'bridge-gate-exception';
19
+ const ROLE_INSTEAD = 'A flag ruled on the privilege this role stands for (`bridge role list` shows which privileges each role holds), ' +
20
+ 'e.g. `privileges contains "USER_WRITE"`. Then `<FeatureFlag key="…">` or a route rule with `featureFlag` in SvelteKit, ' +
21
+ '`@RequireFeatureFlag(\'…\')` in NestJS. A role rule (`user.role eq "ADMIN"`) only when the developer means the role itself.';
22
+ const PLAN_NAME_INSTEAD = 'List the feature on the plans that sell it (`bridge plan feature add <plan> <feature>`) and gate with a flag ruled ' +
23
+ '`bridge:billing.entitlement.<feature> eq true`. Never compare plan names in code.';
24
+ function planFeatureInstead(key) {
25
+ const k = key ?? '<feature>';
26
+ return (`A flag \`${k}\` ruled \`bridge:billing.entitlement.${k} eq true\` ` +
27
+ '(SvelteKit `<FeatureFlag key>` / route rule `featureFlag`, NestJS `@RequireFeatureFlag`). ' +
28
+ `A direct plan-feature check is the documented exception: keep it only if the developer asked for no flag, and mark the line \`// ${GATE_EXCEPTION_MARKER}: <reason>\`.`);
29
+ }
30
+ function privilegeInstead(key) {
31
+ return (`A flag ruled \`privileges contains "${key ?? '<PRIVILEGE>'}"\`, read with \`<FeatureFlag key>\` / a route rule ` +
32
+ '`featureFlag` (SvelteKit) or `@RequireFeatureFlag` (NestJS).');
33
+ }
34
+ const firstLiteral = (s) => /['"`]([\w.:-]+)['"`]/.exec(s ?? '')?.[1];
35
+ const leadingLiteral = (s) => /^\s*\{?\s*['"`]([\w.:-]+)['"`]/.exec(s ?? '')?.[1];
36
+ export const CODE_CHECKS = [
37
+ // Roles
38
+ { kind: 'role', pattern: /\.role\s*(?:===|!==|==|!=)|(?:===|!==|==|!=)\s*[\w$.?]*\.role\b/, instead: () => ROLE_INSTEAD },
39
+ { kind: 'role', pattern: /\brole\s*(?:===|!==|==|!=)\s*['"`]/i, instead: () => ROLE_INSTEAD },
40
+ { kind: 'role', pattern: /\.includes\(\s*[\w$.?]*\brole\b/i, instead: () => ROLE_INSTEAD },
41
+ { kind: 'role', pattern: /\bhasRole\s*\(/, instead: () => ROLE_INSTEAD },
42
+ { kind: 'role', pattern: /\bcase\s+['"`](?:OWNER|ADMIN|MEMBER)['"`]\s*:/, instead: () => ROLE_INSTEAD },
43
+ {
44
+ kind: 'removed-api',
45
+ pattern: /@RequireRole\s*\(/,
46
+ instead: () => `\`@RequireRole\` is gone. ${ROLE_INSTEAD}`,
47
+ },
48
+ // Privileges read in app code (`@RequirePrivilege` is an API-token scope, not a gate on a person)
49
+ {
50
+ kind: 'privilege',
51
+ pattern: /\bprivileges\s*\??\.\s*(?:includes|some|indexOf|has|find)\s*\(([^)]*)/,
52
+ instead: (m) => privilegeInstead(firstLiteral(m[1])),
53
+ },
54
+ // Plan features read directly. The key is named only when it is written as a literal.
55
+ { kind: 'plan-feature', pattern: /<Entitled\b[^>]*?\bto=(.*)/, instead: (m) => planFeatureInstead(leadingLiteral(m[1])) },
56
+ { kind: 'plan-feature', pattern: /\bentitlements\s*\??\.\s*can\s*\((.*)/, instead: (m) => planFeatureInstead(leadingLiteral(m[1])) },
57
+ { kind: 'plan-feature', pattern: /@RequireEntitlement\s*\((.*)/, instead: (m) => planFeatureInstead(leadingLiteral(m[1])) },
58
+ // Plan names compared
59
+ { kind: 'plan-name', pattern: /\bplan(?:Key|Name|Slug|Id)?\s*(?:===|!==|==|!=)\s*['"`]/i, instead: () => PLAN_NAME_INSTEAD },
60
+ { kind: 'plan-name', pattern: /\.plan(?:\??\.(?:key|slug|name|id))?\s*(?:===|!==|==|!=)\s*['"`]/, instead: () => PLAN_NAME_INSTEAD },
61
+ { kind: 'plan-name', pattern: /\bis(?:Pro|Free|Enterprise|Team|Business|Premium)(?:Plan|User)?\b\s*[(=]/, instead: () => PLAN_NAME_INSTEAD },
62
+ {
63
+ kind: 'removed-api',
64
+ pattern: /\bplans\s*:\s*\[/,
65
+ instead: () => `Route rules no longer take \`plans\`. ${PLAN_NAME_INSTEAD}`,
66
+ },
67
+ ];
68
+ const COMMENT_LINE = /^\s*(?:\/\/|\*|\/\*|<!--)/;
69
+ /** Every direct gate check in one source file. */
70
+ export function codeGateFindings(source, file) {
71
+ const lines = source.split('\n');
72
+ const out = [];
73
+ lines.forEach((line, i) => {
74
+ if (COMMENT_LINE.test(line))
75
+ return;
76
+ if (line.includes(GATE_EXCEPTION_MARKER) || (i > 0 && lines[i - 1].includes(GATE_EXCEPTION_MARKER)))
77
+ return;
78
+ const seen = new Set();
79
+ for (const check of CODE_CHECKS) {
80
+ if (seen.has(check.kind))
81
+ continue;
82
+ const m = check.pattern.exec(line);
83
+ if (!m)
84
+ continue;
85
+ seen.add(check.kind);
86
+ out.push({ file, line: i + 1, kind: check.kind, text: line.trim(), instead: check.instead(m) });
87
+ }
88
+ });
89
+ return out;
90
+ }
91
+ export const SOURCE_EXTENSIONS = ['.svelte', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.vue'];
92
+ export const SKIPPED_DIRS = new Set([
93
+ 'node_modules', '.git', 'dist', 'build', '.svelte-kit', '.next', '.nuxt', '.output', 'coverage', '.turbo', '.vercel',
94
+ '__tests__', 'e2e', 'tests',
95
+ ]);
96
+ export function isScannedSource(name) {
97
+ if (name.endsWith('.d.ts'))
98
+ return false;
99
+ if (/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(name))
100
+ return false;
101
+ return SOURCE_EXTENSIONS.some((ext) => name.endsWith(ext));
102
+ }
103
+ // ── Docs: what no guide may teach ───────────────────────────────────────────
104
+ export const DOC_FORBIDDEN = [
105
+ ['a role compared in code', /\b(?:user|session|claims|token|me|currentUser|profile|locals\.user|req\.user|\$user|\$profile)\??\.role\s*(?:===|!==|==|!=)/],
106
+ ['a role compared in code', /\brole\s*(?:===|!==|==|!=)\s*['"`]/i],
107
+ ['a role list checked in code', /\[\s*['"][A-Z_]+['"](?:\s*,\s*['"][A-Z_]+['"])*\s*\]\s*\.includes\(/],
108
+ ['a role checked in code', /\bhasRole\s*\(/],
109
+ ['a role decorator on a handler', /@RequireRole\s*\(/],
110
+ ['a role check on the page', /\brole check\b[^.\n]{0,30}\b(?:in|on) (?:the |your )?(?:page|component|code|handler|layout|controller)/i],
111
+ ['a role check on the page', /\bcheck\w*\b[^.\n]{0,30}\brole\b[^.\n]{0,30}\b(?:in|on) (?:the |your )?(?:page|component|code|layout)\b/i],
112
+ ['"target a role instead"', /target a role instead/i],
113
+ ['privileges read in code', /\bprivileges\s*\??\.\s*(?:includes|some|indexOf|has)\s*\(/],
114
+ ['a direct plan-feature check', /<Entitled\b/],
115
+ ['a direct plan-feature check', /\bentitlements\s*\??\.\s*can\s*\(/],
116
+ ['a direct plan-feature check', /@RequireEntitlement\s*\(/],
117
+ ['a plan name compared in code', /\.plan(?:Key|Name|Slug)?\s*(?:===|!==|==|!=)\s*['"`]/],
118
+ ['a plan name compared in code', /\bplan(?:Key|Name|Slug)?\s*(?:===|!==|==|!=)\s*['"`]/i],
119
+ ['a plan-name helper', /\bis(?:Pro|Free|Enterprise|Team|Business)(?:Plan|User)?\b\s*[(=]/],
120
+ ['a route rule on plans', /\bplans\s*:\s*\[/],
121
+ ['a route rule on a role', /\{[^}\n]*\b(?:match|path)\s*:[^}\n]*\brole\s*:\s*['"`]/],
122
+ ['a route rule on a privilege', /\bprivilege\s*:\s*['"`](?!ANONYMOUS|AUTHENTICATED)[A-Z_]+['"`]/],
123
+ ['a flag rule that names plans', /"attribute"\s*:\s*"(?:tenant\.plan|bridge:billing\.plan)"/],
124
+ ['browser counting called demo-grade', /demo[- ]grade/i],
125
+ ['browser counting called display only', /display,? not enforcement/i],
126
+ ['browser counting said unable to refuse', /only (?:a|the|your) backend can (?:refuse|enforce|stop)/i],
127
+ [
128
+ 'browser counting said not to work',
129
+ /\b(?:browser|frontend|client)(?:[- ](?:only|side))?\b[^.\n]{0,40}\b(?:won't|will not|does not|doesn't|cannot|can't|can not) (?:work|enforce|be trusted)/i,
130
+ ],
131
+ ['browser counting said not production-ready', /\b(?:browser|frontend|client)\b[^.\n]{0,60}\bnot (?:production|prod)[- ](?:ready|grade)/i],
132
+ // TBP-757: `contains` on privileges is exact membership; advice written around the old substring match is stale.
133
+ ['privileges said to match as text', /\b(?:matched|matches|match) (?:privilege keys )?as text\b|\bno other (?:privilege )?key contains\b/i],
134
+ ];
135
+ /** A sentence that tells the reader NOT to do the forbidden thing. */
136
+ const NEGATED = /\b(?:never|instead of|rather than|do not|don't|not a|is gone|no longer)\b/i;
137
+ const HEADING = /^(#{1,6})\s+(.*)$/;
138
+ /**
139
+ * Every sentence in a guide that teaches a direct check, outside the one
140
+ * section a guide may have whose heading says "Exceptions".
141
+ */
142
+ export function docGateViolations(markdown) {
143
+ const out = [];
144
+ let exceptionLevel = 0; // heading depth of the Exceptions section we are in, 0 = none
145
+ let inFence = false;
146
+ markdown.split('\n').forEach((line, i) => {
147
+ if (/^\s*(?:```|~~~)/.test(line))
148
+ inFence = !inFence;
149
+ const heading = inFence ? null : HEADING.exec(line);
150
+ if (heading) {
151
+ const level = heading[1].length;
152
+ if (exceptionLevel && level <= exceptionLevel)
153
+ exceptionLevel = 0;
154
+ if (/exception/i.test(heading[2]))
155
+ exceptionLevel = level;
156
+ return;
157
+ }
158
+ if (exceptionLevel)
159
+ return;
160
+ for (const sentence of line.split(/(?<=[.!?])\s+/)) {
161
+ if (NEGATED.test(sentence))
162
+ continue;
163
+ for (const [rule, pattern] of DOC_FORBIDDEN) {
164
+ if (pattern.test(sentence))
165
+ out.push({ line: i + 1, rule, text: sentence.trim() });
166
+ }
167
+ }
168
+ });
169
+ return out;
170
+ }
171
+ //# sourceMappingURL=gate-rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-rules.js","sourceRoot":"","sources":["../src/gate-rules.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBzC;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmBvD"}
1
+ {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAoBzC;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAqBvD"}
package/dist/program.js CHANGED
@@ -15,6 +15,7 @@ import { registerGuideCommands } from './commands/guide.command.js';
15
15
  import { registerOpsCommands } from './commands/ops.command.js';
16
16
  import { registerStripeCommands } from './commands/stripe.command.js';
17
17
  import { registerIntegrationCommands } from './commands/integration.command.js';
18
+ import { registerCheckCommands } from './commands/check.command.js';
18
19
  /**
19
20
  * Every command group the CLI has, registered on `program`.
20
21
  *
@@ -41,5 +42,7 @@ export function registerCommands(program) {
41
42
  registerStripeCommands(program);
42
43
  // After setup + event: adds `setup status` and `event auth-attempts` to those groups (TBP-541).
43
44
  registerIntegrationCommands(program);
45
+ // TBP-705 — `bridge check gates`, the last verification step of every guide.
46
+ registerCheckCommands(program);
44
47
  }
45
48
  //# sourceMappingURL=program.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"program.js","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,yBAAyB,EAAE,MAAM,iCAAiC,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,EAAE,2BAA2B,EAAE,MAAM,mCAAmC,CAAC;AAEhF;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAC/C,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC7B,sBAAsB,CAAC,OAAO,CAAC,CAAC;IAChC,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9B,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9B,yBAAyB,CAAC,OAAO,CAAC,CAAC;IACnC,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9B,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9B,wBAAwB,CAAC,OAAO,CAAC,CAAC;IAClC,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9B,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/B,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/B,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/B,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9B,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/B,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC7B,sBAAsB,CAAC,OAAO,CAAC,CAAC;IAChC,gGAAgG;IAChG,2BAA2B,CAAC,OAAO,CAAC,CAAC;AACvC,CAAC"}
1
+ {"version":3,"file":"program.js","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,yBAAyB,EAAE,MAAM,iCAAiC,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,EAAE,2BAA2B,EAAE,MAAM,mCAAmC,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAEpE;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAC/C,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC7B,sBAAsB,CAAC,OAAO,CAAC,CAAC;IAChC,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9B,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9B,yBAAyB,CAAC,OAAO,CAAC,CAAC;IACnC,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9B,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9B,wBAAwB,CAAC,OAAO,CAAC,CAAC;IAClC,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9B,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/B,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/B,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/B,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9B,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/B,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC7B,sBAAsB,CAAC,OAAO,CAAC,CAAC;IAChC,gGAAgG;IAChG,2BAA2B,CAAC,OAAO,CAAC,CAAC;IACrC,6EAA6E;IAC7E,qBAAqB,CAAC,OAAO,CAAC,CAAC;AACjC,CAAC"}
@@ -2,6 +2,8 @@
2
2
 
3
3
  You are integrating **Bridge Authentication** (auth, tenant management, RBAC) into a user's application. Your job is to detect the project structure, identify the technologies, and apply the right per-framework integration for each.
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
  This prompt is framework-agnostic. It orchestrates discovery, confirmation, and verification — the actual install commands, file shapes, and code snippets live in the per-framework guides fetched in Step 4.
6
8
 
7
9
  > **Related master prompts** — if the user asked for flags or billing specifically (not auth), stop here and route them:
@@ -238,8 +240,9 @@ For each integrated project:
238
240
  1. **Build check** — run the project's build command (from `package.json` scripts)
239
241
  2. **Config check** — confirm the `appId` environment variable is set
240
242
  3. **Route check** — confirm protected routes have guards and public routes are accessible
243
+ 4. **Gate check** — run `npx @nebulr-group/bridge-cli check gates` in each project. Every gate in app code is a flag: it lists each direct role, privilege, plan or plan-feature check with the flag to use instead. Fix every one and run it again until it is clean. Only a check the developer explicitly asked to keep stays, marked `// bridge-gate-exception: <reason>`
241
244
 
242
- If anything fails, diagnose and fix before moving on. Once the checks pass, move on to the summary in Step 6b — do NOT just dump a list of changed files at the user.
245
+ If anything fails, including the gate check, diagnose and fix before moving on. Once the checks pass, move on to the summary in Step 6b — do NOT just dump a list of changed files at the user.
243
246
 
244
247
  ## Step 6b — Tell the developer what they just got
245
248
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  You are integrating **Bridge Billing** into a user's application — plan selection, Stripe Checkout, subscription state, lifecycle notices (trial / dunning / cancel), and usage quota counters.
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
  This prompt is framework-agnostic. It orchestrates discovery, pricing model setup, and verification — the actual install commands and code snippets live in the per-framework guides fetched in Step 4.
6
8
 
7
9
  > **Related master prompts** — if the user hasn't set up auth yet, stop here and route them:
@@ -117,7 +119,7 @@ From their answer, map everything to confirmation tables before creating anythin
117
119
 
118
120
  Use `hard` when overage should be blocked. Use `metered` when overage should bill via Stripe.
119
121
 
120
- **Counter or gauge?** If deleting it frees room, it's a gauge and your app counts it; if it happened, it's a counter and Bridge counts it. AI completions, exports and API calls are counters (reset each period); projects, tickets and seats (`users`) are gauges (never reset). The developer decides the metric list; you decide the kind from that sentence and say why in one line.
122
+ **Counter or gauge?** If deleting it frees room, it's a gauge and your app counts it; if it happened, it's a counter and Bridge counts it. AI completions, exports and API calls are counters (reset each period); projects, tickets and seats are gauges (never reset). Seats are a gauge the app names (e.g. `seats`) with source `membership`: Bridge counts the workspace's active members itself, pending invites included, and the app never reports them. The developer decides the metric list; you decide the kind from that sentence and say why in one line.
121
123
 
122
124
  **Entitlements** (features on for some plans, off for others):
123
125
 
@@ -126,7 +128,7 @@ Use `hard` when overage should be blocked. Use `metered` when overage should bil
126
128
  | `free` | `advanced_analytics` | off |
127
129
  | `pro` | `advanced_analytics` | on |
128
130
 
129
- An entitlement is not configured on its own: every `hard` quota with a limit above 0 is also an entitlement of the same name. A feature that is on for `pro` is therefore a hard quota on `pro` (e.g. `--metric advanced_analytics --limit 1 --policy hard`) that nothing counts, and absent from `free`; the backend gates it with `@RequireEntitlement('advanced_analytics')`, never with `@RequireQuota`.
131
+ A feature that is on for `pro` goes in `pro`'s features list (`bridge plan feature add pro advanced_analytics --name "Advanced analytics"`) and not in `free`'s. It reaches the app as `bridge:billing.entitlement.advanced_analytics`: the flag that controls the feature uses the rule `bridge:billing.entitlement.advanced_analytics eq true`, so no rule names a plan. A direct check without a flag, `@RequireEntitlement('advanced_analytics')` (never `@RequireQuota`), is the exception. A hard quota with limit 1 that nothing counts still works as an entitlement too; it is the older way.
130
132
 
131
133
  If there are no per-plan limits or feature differences, the quotas and entitlements tables are empty — skip those commands below.
132
134
 
@@ -188,11 +190,18 @@ bridge plan quota set <plan> --metric <key> --limit <n> --policy hard --kind gau
188
190
  bridge plan quota set <plan> --metric <key> --limit <n> --policy metered --price-amount <perUnit> [--price-currency <cur>]
189
191
  ```
190
192
 
191
- Run one command per row. (There is no `plan entitlement set` command —
192
- entitlements are derived from `hard` quotas automatically; do not invent one.)
193
+ Run one command per row. Then add each on/off feature from the entitlements
194
+ table to the plans that include it:
195
+
196
+ ```bash
197
+ bridge plan feature add <plan> <feature_key> --name "<Display name>"
198
+ ```
199
+
200
+ (There is no `plan entitlement set` command — do not invent one. Features live
201
+ on the plan; every `hard` quota is also an entitlement of the same name.)
193
202
  Check the result with `bridge plan quota list`, which lists every metric with its kind.
194
203
 
195
- **Where the limit is enforced.** A quota is only a number until the backend enforces it: the plan-limit decorator on the handler that creates the thing (NestJS `@RequireQuota`) refuses at the cap and records the use after a successful request — a POST increments the limit, nothing else to wire. The frontend only shows that decision; a frontend-only app cannot enforce a limit. `bridge guide mechanisms` has the whole model.
204
+ **Where the limit is counted.** Once, where the action happens. Ask the developer whether the action calls their server. If it does, the plan-limit decorator on the handler that creates the thing (NestJS `@RequireQuota`) refuses at the cap and records the use after a successful request — a POST increments the limit, nothing else to wire — and the frontend only shows that decision. If it happens in the browser only, the frontend reports it (`bridge.usage.report` / `bridge.usage.set`) and `<QuotaGate>` stops the button at the cap: a complete, first-class setup. Never both for one metric. `bridge guide mechanisms` has the whole model; `bridge guide fit-together` shows where limits sit next to plans, roles and flags.
196
205
 
197
206
  ## Step 4 — Fetch and apply the per-framework guide
198
207
 
@@ -252,6 +261,7 @@ For each integrated project, the agent verifies — do not hand this to the deve
252
261
  6. Cancel a payment — redirected back to the subscription page
253
262
  7. Paywall (unless opted out): sign in as a new tenant with no plan — you land on the paywall page (or `/welcome` if the developer chose one) and can't reach the app until a plan is chosen
254
263
  8. Plan limit (when a backend was wired): at the cap, the decorated request answers `402 QUOTA_EXCEEDED` — send it with curl, not only through the UI — and in the app the upgrade dialog opens naming the metric
264
+ 9. **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
255
265
 
256
266
  If anything fails, diagnose and fix before moving on.
257
267
 
@@ -315,4 +325,4 @@ Do not run these automatically — the developer decides when they want each.
315
325
  | `hard` | Entitlement flips off at cap; no overage | Seat counts, included features |
316
326
  | `metered` | Overage bills as a Stripe metered price; usage continues | API calls, storage |
317
327
 
318
- Bridge's usage ingest always accepts events — it never refuses on the app's behalf. The refusal at a hard cap comes from **your** backend: the plan-limit decorator (`@RequireQuota`) reads the quota and answers `402 QUOTA_EXCEEDED` before the handler runs. `bridge guide mechanisms` explains the model.
328
+ Bridge's usage ingest always accepts events — it never refuses on the app's behalf. With a backend, the refusal at a hard cap comes from **your** backend: the plan-limit decorator (`@RequireQuota`) reads the quota and answers `402 QUOTA_EXCEEDED` before the handler runs. Without one, `<QuotaGate>` stops the button at the cap. `bridge guide mechanisms` explains the model.
@@ -1,16 +1,20 @@
1
1
  # Feature control — decision guide
2
2
 
3
- Turning features on and off for some people and not others, without a deploy. The tool is a **flag**: code asks for it by key, and the flag's rule decides the answer.
3
+ Controlling who gets a route, an API endpoint, a feature or a single piece of code, without a deploy. The tool is a **flag**: code asks for it by key, and the flag's rule decides the answer.
4
4
 
5
- > Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). How the pieces work underneath: `bridge guide mechanisms`.
5
+ > Read first: `bridge guide fit-together` (MCP: resource `bridge://guides/fit-together`), how roles, plans, limits and flags fit together. Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). How the pieces work underneath: `bridge guide mechanisms`.
6
6
 
7
7
  ## The rule that decides everything else
8
8
 
9
- - **Is a feature on for them? A flag.** Always, and for good. The flag is the stable place in code; the *reasons* live in its rule and change as the business does: 10% of users today, "plan is Pro" tomorrow, one named customer as an exception next month. Each of those is a rule edit, not new code.
10
- - **How much do they get? The plan.** Prices, trials and limits live on the plan: see the **payments** guide.
11
- - **Who inside a workspace may do it? A role.** See the **roles** guide.
9
+ 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, and point the flag's rule at the plan's list.
12
10
 
13
- A plan can also grant a feature on its own (a *plan feature*, checked with `@RequireEntitlement` on the backend and `<Entitled>` in the UI; the payments guide explains it). Use that only when the feature *is* what the plan sells and nothing else will ever decide it: the backend then refuses it without extra wiring. Anything that could ever need a rollout, a trial cohort or a one-customer exception is a flag.
11
+ - **Is it on for them? A flag.** The flag is the stable place in code; the *reasons* live in its rule and change as the business does: 10% of users today, "the plan includes it" tomorrow, one named customer as an exception next month. Each of those is a rule edit, not new code.
12
+ - **Does a plan sell it? The plan's features list, and the flag's rule reads it:** `bridge:billing.entitlement.<feature> eq true`. The rule never names plans, so changing what Pro includes is one edit on the plan.
13
+ - **Who is this person? A flag rule on a privilege** (preferred) **or a role.** Never a role check written into the app's code.
14
+ - **How much do they get? A plan limit.** Numbers are not flags: see the **payments** guide.
15
+ - **May they change this one record? The app's own code.** Only the app knows who owns what.
16
+
17
+ **Every gate in app code is a flag.** App code never reads a role, a privilege list, the plan or a plan feature to decide what someone may see or do; before calling the work done, `npx @nebulr-group/bridge-cli check gates` lists any that are left.
14
18
 
15
19
  ## Start from what is there
16
20
 
@@ -18,22 +22,28 @@ A plan can also grant a feature on its own (a *plan feature*, checked with `@Req
18
22
  |---|---|---|
19
23
  | Every flag with its rule, and warnings on flags that cannot mean what they look like | `list_feature_flags` | `bridge flag list` |
20
24
  | One flag | `get_flag` | `bridge flag get <key>` |
21
- | Plan keys and role keys, for rules | `list_plans`, `list_roles` | `bridge plan list`, `bridge role list` |
25
+ | What each plan sells, for plan-feature rules | `list_plans` | `bridge plan list`, `bridge plan feature list <plan>` |
26
+ | The app's real roles and privileges, for who-someone-is rules | `list_roles` | `bridge role list` |
27
+
28
+ Read the roles before writing a rule on them: the developer may have renamed or reshaped any of them, so never assume what a role grants from its name.
22
29
 
23
30
  ## Ask the developer
24
31
 
25
32
  Product questions. Ask, wait, never guess.
26
33
 
27
- 1. **Which feature, in their words, and who should get it?** Everyone, nobody yet, a percentage of users, one plan, some roles, named customers, or a mix.
28
- 2. **What do people without it see?** Nothing at all, a teaser with an upgrade link, or the old version.
29
- 3. **Should it switch on or off by itself at a set time?**
30
- 4. **If it is tied to a plan: is it simply part of that plan for ever,** or might it later go to a trial group or a single customer too? (This picks flag or plan feature, above.)
34
+ 1. **Which feature, in their words, and who should get it?** Everyone, nobody yet, a percentage of users, the plans that sell it, people allowed to do something, named customers, or a mix.
35
+ 2. **If a plan sells it: which plans?** The answer goes on the plans' features lists, not into the rule.
36
+ 3. **If it depends on who someone is: what may they do that others may not?** Ask in the app's verbs ("manage billing", "see reports"). That names a privilege; the roles guide covers creating one.
37
+ 4. **What do people without it see?** Nothing at all, the old version, or, when the plan is the reason, an upgrade prompt they can click.
38
+ 5. **Should it switch on or off by itself at a set time?**
31
39
 
32
40
  ## Decide yourself
33
41
 
34
42
  - **Flag keys in kebab-case, named after the feature: `new-editor`, `analytics`.** Reason: the key is what code reads; it should still make sense after the rollout is over.
35
43
  - **Boolean unless the developer needs variants.** Reason: on/off covers almost every case and reads plainly in code.
36
- - **Target a plan by its key (`pro`), from `list_plans`, never its display name,** whatever an example elsewhere shows. Reason: the app sends the key.
44
+ - **A feature a plan sells: add it to the plan (`add_plan_feature`), then rule on `bridge:billing.entitlement.<feature> eq true`.** Reason: the pricing table, the upgrade dialog and the flag all read one list, and no rule names a plan.
45
+ - **Who someone is: rule on a privilege (`privileges contains "REPORTS_VIEW"`) before a role (`user.role eq "ADMIN"`).** Reason: a privilege rule survives renamed or reshuffled roles. Use the full privilege key: `contains` on the privileges list matches a whole key, never part of one.
46
+ - **An admin-only page is a flag route rule, not a check on the page.** Reason: the rule is changed in one place and the backend reads the same one.
37
47
  - **Always give the rule an `otherwiseValue`, and make it the safe answer (usually `false`).** Reason: anyone the rule does not match gets it.
38
48
  - **Test the rule with the exact attributes the app sends before relying on it** (`evaluate_feature_flag` / `bridge flag eval`). Reason: a rule on an attribute the app never sends silently answers `otherwiseValue` for everyone.
39
49
  - **Read flags through the SDK the per-framework guide names, never with a hand-written request.** Reason: a wrong address fails into "off", which looks exactly like "not bought".
@@ -52,7 +62,7 @@ These are the exact names and values. A rule on any other name matches nothing u
52
62
  | `user.role` | the role **key** as created, e.g. `OWNER`, `ADMIN` | exactly: `admin` does not match `ADMIN` |
53
63
  | `tenant.id` | the workspace id | exactly |
54
64
  | `tenant.plan` | the plan **key**, e.g. `pro` (not the name) | ignoring case: `Pro` matches `pro` |
55
- | `privileges` | a list of privilege keys | as a list; target a role instead |
65
+ | `privileges` | the person's privilege keys, as a list | `contains`: the key appears in the list. The preferred way to target who someone is |
56
66
 
57
67
  **Also sent by the browser SDKs, from the workspace's subscription:**
58
68
 
@@ -63,9 +73,11 @@ These are the exact names and values. A rule on any other name matches nothing u
63
73
  | `bridge:billing.trial` | `true` or `false` |
64
74
  | `bridge:billing.quota.<metric>.used` / `.limit` / `.remaining` | numbers |
65
75
  | `bridge:billing.quota.<metric>.percent_used` | a fraction: `0.8` is 80% |
66
- | `bridge:billing.entitlement.<name>` | `true` or `false`, including `app_active` (the subscription is active, trialing, past due or cancelling) |
76
+ | `bridge:billing.entitlement.<name>` | `true` or `false`: a feature in the plan's features list is `true` when the workspace's plan includes it, so "the plan includes analytics" is the rule `bridge:billing.entitlement.analytics eq true`. Also `app_active` (the subscription is active, trialing, past due or cancelling) |
77
+
78
+ **On the NestJS backend** the same attributes are filled in with no wiring: `user.role`, `privileges`, `user.id`, `user.email`, `tenant.id` and `tenant.plan` from the verified sign-in, and `bridge:billing.plan`, `.subscription.status`, `.trial` and `.entitlement.<name>` from Bridge's own billing records. A rule gives the same answer there as in the browser. Only the plan-limit numbers (`bridge:billing.quota.*`) are browser-only. Values the client sends are never used for these.
67
79
 
68
- **On the NestJS backend** only the signed-in person's id is filled in (as the identity). A rule on `tenant.plan` or `user.role` sees nothing there until the app passes those values from the verified user on each call; the NestJS flags guide shows how. Never pass values the client sent.
80
+ **Why it is off.** A flag that is off says why: `plan` (the plan does not include it, so an upgrade would turn it on), `permission` (role or privileges), or `off` (switched off). No upgrade dialog opens by itself: it opens when someone opens a gated route or clicks something gated (SvelteKit: `<FeatureFlag key="analytics" upgrade>`). A flag-gated backend endpoint answers `402 FEATURE_NOT_IN_PLAN`, `403 FEATURE_NOT_PERMITTED` or `403 FEATURE_OFF`.
69
81
 
70
82
  **Percentage rollouts** bucket on the identity: the person's id when signed in, a stored anonymous id in the browser otherwise. A backend call with no signed-in person gets the default.
71
83
 
@@ -80,15 +92,23 @@ A condition's values are always a list, even for a single value.
80
92
  | Create a flag with its rule | `create_feature_flag` | `bridge flag create --key analytics --state on-with-rule --rule '<json>'` |
81
93
  | Change a rule or values | `update_feature_flag` | `bridge flag update --key analytics --rule '<json>'` |
82
94
  | Switch it on or off for everyone now (the rule is kept, unused while on) | `toggle_feature_flag` | `bridge flag toggle --key analytics --enabled true` |
83
- | Check who gets what | `evaluate_feature_flag` | `bridge flag eval analytics --identity u1 --attribute tenant.plan=pro` |
95
+ | Add the feature to the plans that sell it | `add_plan_feature` | `bridge plan feature add pro analytics --name "Analytics"` |
96
+ | Check who gets what | `evaluate_feature_flag` | `bridge flag eval analytics --identity u1 --attribute bridge:billing.entitlement.analytics=true` |
84
97
  | Switch at a set time | `set_flag_schedule` | `bridge flag schedule set analytics --at … --state on` |
85
98
  | Copy flags between apps | `export_feature_flags`, `import_feature_flags` | `bridge flag export`, `bridge flag import <file>` |
86
99
  | Code for the framework | `get_integration_guide` (topic `feature-flags`) | `bridge guide flags --framework svelte` |
87
100
 
88
- A rule for "Pro only":
101
+ A rule for "the plans that sell analytics":
102
+
103
+ ```json
104
+ { "branches": [{ "conditions": [{ "attribute": "bridge:billing.entitlement.analytics", "operator": "eq", "values": [true] }], "returnValue": true }],
105
+ "otherwiseValue": false, "rolloutPct": 100 }
106
+ ```
107
+
108
+ A rule for "people who may see reports":
89
109
 
90
110
  ```json
91
- { "branches": [{ "conditions": [{ "attribute": "tenant.plan", "operator": "eq", "values": ["pro"] }], "returnValue": true }],
111
+ { "branches": [{ "conditions": [{ "attribute": "privileges", "operator": "contains", "values": ["REPORTS_VIEW"] }], "returnValue": true }],
92
112
  "otherwiseValue": false, "rolloutPct": 100 }
93
113
  ```
94
114
 
@@ -97,9 +117,15 @@ A rule for "Pro only":
97
117
  1. `evaluate_feature_flag` / `bridge flag eval` with a context that should get the feature, then one that should not: the answers differ as the developer intended.
98
118
  2. `list_feature_flags` / `bridge flag list` shows no warnings on the flag.
99
119
  3. In the app, as a test user on each side of the rule.
120
+ 4. `npx @nebulr-group/bridge-cli check gates` in the project: no direct role, privilege, plan or plan-feature check left.
121
+
122
+ ## Exceptions
123
+
124
+ 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.
100
125
 
101
126
  ## Where this connects
102
127
 
103
- - **Payments:** plan keys, and plan features versus flags.
104
- - **Roles:** role keys for `user.role`, and why a flag is not a permission.
128
+ - **Fit together:** the rule above, with roles, plans, limits and flags side by side (`bridge guide fit-together`).
129
+ - **Payments:** the plan's features list the rule points at, and limits for anything counted.
130
+ - **Roles:** the privileges and roles a who-someone-is rule targets; read them with `list_roles` first.
105
131
  - **Going live:** flags are per app; a production app needs its own (`export_feature_flags`, then `import_feature_flags`).
@@ -2,7 +2,7 @@
2
2
 
3
3
  Everything between "it works on my machine" and real users signing up and paying. Bridge keeps a readiness checklist for each app; this guide is how to work through it with the developer.
4
4
 
5
- > Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). How the pieces work underneath: `bridge guide mechanisms`.
5
+ > How roles, plans, limits and flags fit together: `bridge guide fit-together` (MCP: resource `bridge://guides/fit-together`). Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). How the pieces work underneath: `bridge guide mechanisms`.
6
6
 
7
7
  ## Start from the checklist
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  How people sign up and sign in to the developer's app. This guide says what to ask, what to decide yourself, and which tool or command does each step. The code itself is in the per-framework guide.
4
4
 
5
- > Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). How the pieces work underneath: `bridge guide mechanisms`.
5
+ > How roles, plans, limits and flags fit together: `bridge guide fit-together` (MCP: resource `bridge://guides/fit-together`). Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). How the pieces work underneath: `bridge guide mechanisms`.
6
6
 
7
7
  ## Start from what is there
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  How Bridge's pages and emails look to the developer's users: the sign-in pages, the subscription pages, and the emails Bridge sends in the app's name.
4
4
 
5
- > Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). The customisation levels in full, with every design token: `bridge guide mechanisms`.
5
+ > How roles, plans, limits and flags fit together: `bridge guide fit-together` (MCP: resource `bridge://guides/fit-together`). Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). The customisation levels in full, with every design token: `bridge guide mechanisms`.
6
6
 
7
7
  ## Two places pages can live
8
8