@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.
Files changed (61) hide show
  1. package/README.md +31 -2
  2. package/dist/auth/api-client.d.ts +2 -0
  3. package/dist/auth/api-client.d.ts.map +1 -1
  4. package/dist/auth/api-client.js.map +1 -1
  5. package/dist/auth/workspace-client.d.ts +70 -0
  6. package/dist/auth/workspace-client.d.ts.map +1 -0
  7. package/dist/auth/workspace-client.js +105 -0
  8. package/dist/auth/workspace-client.js.map +1 -0
  9. package/dist/bin.js +4 -1
  10. package/dist/bin.js.map +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +5 -14
  13. package/dist/cli.js.map +1 -1
  14. package/dist/commands/app.command.d.ts.map +1 -1
  15. package/dist/commands/app.command.js +117 -1
  16. package/dist/commands/app.command.js.map +1 -1
  17. package/dist/commands/auth/login.command.d.ts +13 -1
  18. package/dist/commands/auth/login.command.d.ts.map +1 -1
  19. package/dist/commands/auth/login.command.js +17 -3
  20. package/dist/commands/auth/login.command.js.map +1 -1
  21. package/dist/commands/auth/status.command.js +14 -0
  22. package/dist/commands/auth/status.command.js.map +1 -1
  23. package/dist/commands/check.command.d.ts +10 -0
  24. package/dist/commands/check.command.d.ts.map +1 -0
  25. package/dist/commands/check.command.js +95 -0
  26. package/dist/commands/check.command.js.map +1 -0
  27. package/dist/commands/plan.command.d.ts +19 -5
  28. package/dist/commands/plan.command.d.ts.map +1 -1
  29. package/dist/commands/plan.command.js +40 -26
  30. package/dist/commands/plan.command.js.map +1 -1
  31. package/dist/config.d.ts +46 -0
  32. package/dist/config.d.ts.map +1 -1
  33. package/dist/config.js +190 -47
  34. package/dist/config.js.map +1 -1
  35. package/dist/credentials.d.ts +21 -0
  36. package/dist/credentials.d.ts.map +1 -1
  37. package/dist/credentials.js +21 -0
  38. package/dist/credentials.js.map +1 -1
  39. package/dist/gate-rules.d.ts +51 -0
  40. package/dist/gate-rules.d.ts.map +1 -0
  41. package/dist/gate-rules.js +171 -0
  42. package/dist/gate-rules.js.map +1 -0
  43. package/dist/output.d.ts.map +1 -1
  44. package/dist/output.js +7 -0
  45. package/dist/output.js.map +1 -1
  46. package/dist/program.d.ts +17 -0
  47. package/dist/program.d.ts.map +1 -1
  48. package/dist/program.js +76 -0
  49. package/dist/program.js.map +1 -1
  50. package/dist/prompts/auth-master-integration-prompt.md +5 -2
  51. package/dist/prompts/billing/master.md +5 -2
  52. package/dist/prompts/decisions/feature-control.md +6 -1
  53. package/dist/prompts/decisions/going-live.md +1 -1
  54. package/dist/prompts/decisions/payments.md +9 -4
  55. package/dist/prompts/decisions/roles.md +2 -2
  56. package/dist/prompts/decisions/teams.md +4 -3
  57. package/dist/prompts/fit-together.md +22 -4
  58. package/dist/prompts/flags/master.md +3 -0
  59. package/dist/prompts/mcp-tools.json +7 -2
  60. package/dist/prompts/mechanisms.md +9 -5
  61. package/package.json +1 -1
@@ -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":"AAAA;;;;;;;;;;;;;;;GAeG;AAeH,8FAA8F;AAC9F,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAC;AAE7D,MAAM,YAAY,GAChB,kHAAkH;IAClH,yHAAyH;IACzH,6HAA6H,CAAC;AAEhI,MAAM,iBAAiB,GACrB,qHAAqH;IACrH,mFAAmF,CAAC;AAEtF,SAAS,kBAAkB,CAAC,GAAuB;IACjD,MAAM,CAAC,GAAG,GAAG,IAAI,WAAW,CAAC;IAC7B,OAAO,CACL,YAAY,CAAC,yCAAyC,CAAC,aAAa;QACpE,4FAA4F;QAC5F,oIAAoI,qBAAqB,eAAe,CACzK,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAuB;IAC/C,OAAO,CACL,uCAAuC,GAAG,IAAI,aAAa,sDAAsD;QACjH,8DAA8D,CAC/D,CAAC;AACJ,CAAC;AAQD,MAAM,YAAY,GAAG,CAAC,CAAqB,EAAE,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAC1F,MAAM,cAAc,GAAG,CAAC,CAAqB,EAAE,EAAE,CAAC,gCAAgC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAEtG,MAAM,CAAC,MAAM,WAAW,GAAgB;IACtC,QAAQ;IACR,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,iEAAiE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,EAAE;IACzH,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,qCAAqC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,EAAE;IAC7F,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,kCAAkC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,EAAE;IAC1F,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,EAAE;IACxE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,+CAA+C,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,EAAE;IACvG;QACE,IAAI,EAAE,aAAa;QACnB,OAAO,EAAE,mBAAmB;QAC5B,OAAO,EAAE,GAAG,EAAE,CAAC,6BAA6B,YAAY,EAAE;KAC3D;IACD,kGAAkG;IAClG;QACE,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,uEAAuE;QAChF,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KACrD;IACD,sFAAsF;IACtF,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,4BAA4B,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE;IACzH,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,uCAAuC,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE;IACpI,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,8BAA8B,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE;IAC3H,sBAAsB;IACtB,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,0DAA0D,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,iBAAiB,EAAE;IAC5H,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,kEAAkE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,iBAAiB,EAAE;IACpI,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,0EAA0E,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,iBAAiB,EAAE;IAC5I;QACE,IAAI,EAAE,aAAa;QACnB,OAAO,EAAE,kBAAkB;QAC3B,OAAO,EAAE,GAAG,EAAE,CAAC,yCAAyC,iBAAiB,EAAE;KAC5E;CACF,CAAC;AAEF,MAAM,YAAY,GAAG,2BAA2B,CAAC;AAEjD,kDAAkD;AAClD,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,IAAY;IAC3D,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,GAAG,GAAkB,EAAE,CAAC;IAC9B,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;QACxB,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO;QACpC,IAAI,IAAI,CAAC,QAAQ,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,qBAAqB,CAAC,CAAC;YAAE,OAAO;QAC5G,MAAM,IAAI,GAAG,IAAI,GAAG,EAAY,CAAC;QACjC,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;YAChC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,SAAS;YACnC,MAAM,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnC,IAAI,CAAC,CAAC;gBAAE,SAAS;YACjB,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACrB,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAClG,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AACnG,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC;IAClC,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS;IACpH,WAAW,EAAE,KAAK,EAAE,OAAO;CAC5B,CAAC,CAAC;AAEH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC;IACzC,IAAI,gCAAgC,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9D,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;AAC7D,CAAC;AAED,+EAA+E;AAE/E,MAAM,CAAC,MAAM,aAAa,GAA4B;IACpD,CAAC,yBAAyB,EAAE,6HAA6H,CAAC;IAC1J,CAAC,yBAAyB,EAAE,qCAAqC,CAAC;IAClE,CAAC,6BAA6B,EAAE,qEAAqE,CAAC;IACtG,CAAC,wBAAwB,EAAE,gBAAgB,CAAC;IAC5C,CAAC,+BAA+B,EAAE,mBAAmB,CAAC;IACtD,CAAC,0BAA0B,EAAE,yGAAyG,CAAC;IACvI,CAAC,0BAA0B,EAAE,0GAA0G,CAAC;IACxI,CAAC,yBAAyB,EAAE,wBAAwB,CAAC;IACrD,CAAC,yBAAyB,EAAE,2DAA2D,CAAC;IACxF,CAAC,6BAA6B,EAAE,aAAa,CAAC;IAC9C,CAAC,6BAA6B,EAAE,mCAAmC,CAAC;IACpE,CAAC,6BAA6B,EAAE,0BAA0B,CAAC;IAC3D,CAAC,8BAA8B,EAAE,sDAAsD,CAAC;IACxF,CAAC,8BAA8B,EAAE,uDAAuD,CAAC;IACzF,CAAC,oBAAoB,EAAE,kEAAkE,CAAC;IAC1F,CAAC,uBAAuB,EAAE,kBAAkB,CAAC;IAC7C,CAAC,wBAAwB,EAAE,wDAAwD,CAAC;IACpF,CAAC,6BAA6B,EAAE,gEAAgE,CAAC;IACjG,CAAC,8BAA8B,EAAE,2DAA2D,CAAC;IAC7F,CAAC,oCAAoC,EAAE,gBAAgB,CAAC;IACxD,CAAC,sCAAsC,EAAE,4BAA4B,CAAC;IACtE,CAAC,wCAAwC,EAAE,0DAA0D,CAAC;IACtG;QACE,mCAAmC;QACnC,0JAA0J;KAC3J;IACD,CAAC,4CAA4C,EAAE,0FAA0F,CAAC;IAC1I,iHAAiH;IACjH,CAAC,kCAAkC,EAAE,qGAAqG,CAAC;CAC5I,CAAC;AAEF,sEAAsE;AACtE,MAAM,OAAO,GAAG,4EAA4E,CAAC;AAE7F,MAAM,OAAO,GAAG,mBAAmB,CAAC;AAIpC;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,MAAM,GAAG,GAAmB,EAAE,CAAC;IAC/B,IAAI,cAAc,GAAG,CAAC,CAAC,CAAC,8DAA8D;IACtF,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;QACvC,IAAI,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,GAAG,CAAC,OAAO,CAAC;QACrD,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpD,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YAChC,IAAI,cAAc,IAAI,KAAK,IAAI,cAAc;gBAAE,cAAc,GAAG,CAAC,CAAC;YAClE,IAAI,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;gBAAE,cAAc,GAAG,KAAK,CAAC;YAC1D,OAAO;QACT,CAAC;QACD,IAAI,cAAc;YAAE,OAAO;QAC3B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,EAAE,CAAC;YACnD,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACrC,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,aAAa,EAAE,CAAC;gBAC5C,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC;oBAAE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACrF,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAIA,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAGjD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAElD;AAGD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUjE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAmEhD"}
1
+ {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAIA,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAGjD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAElD;AAGD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUjE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAwEhD"}
package/dist/output.js CHANGED
@@ -41,6 +41,13 @@ export function outputError(error) {
41
41
  code = 'CONFIG_ERROR';
42
42
  message = error.message;
43
43
  exitCode = 3;
44
+ // TBP-769 — a config error may name its own code and next step.
45
+ const carriedCode = error.code;
46
+ if (typeof carriedCode === 'string' && carriedCode.length > 0)
47
+ code = carriedCode;
48
+ const carriedHint = error.hint;
49
+ if (typeof carriedHint === 'string' && carriedHint.length > 0)
50
+ hint = carriedHint;
44
51
  }
45
52
  else if (error instanceof HttpError) {
46
53
  code = `HTTP_${error.status}`;
@@ -1 +1 @@
1
- {"version":3,"file":"output.js","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"output.js","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"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"}
package/dist/program.d.ts CHANGED
@@ -7,4 +7,21 @@ import type { Command } from 'commander';
7
7
  * checks every `bridge …` command the guides name against it (TBP-706).
8
8
  */
9
9
  export declare function registerCommands(program: Command): void;
10
+ /** Raised after the error was already printed, to stop the action running. */
11
+ export declare class AppContextAbort extends Error {
12
+ constructor();
13
+ }
14
+ /**
15
+ * Global options and the hook that applies them before every action.
16
+ *
17
+ * `--profile` (TBP-628) picks WHICH stored login; `--app` (TBP-769) picks the
18
+ * app WITHIN that login, like the MCP tools' `app` argument.
19
+ *
20
+ * The `--app` name collision: `bridge auth login` has its own `--app` (pin the
21
+ * home app on the consent screen). commander lets the program-level option
22
+ * consume the flag wherever it appears, so login would never see it. The hook
23
+ * therefore hands the value back to any command that declares its own `--app`
24
+ * and, for that command, does not treat it as an app selection.
25
+ */
26
+ export declare function registerGlobalOptions(program: Command): void;
10
27
  //# sourceMappingURL=program.d.ts.map
@@ -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;AAsBzC;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAqBvD;AA2BD,8EAA8E;AAC9E,qBAAa,eAAgB,SAAQ,KAAK;;CAKzC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoC5D"}
package/dist/program.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { prepareAppContext, setAppOverride, setProfileOverride } from './config.js';
2
+ import { outputError } from './output.js';
1
3
  import { registerAppCommands } from './commands/app.command.js';
2
4
  import { registerTenantCommands } from './commands/tenant.command.js';
3
5
  import { registerUserCommands } from './commands/user.command.js';
@@ -15,6 +17,7 @@ import { registerGuideCommands } from './commands/guide.command.js';
15
17
  import { registerOpsCommands } from './commands/ops.command.js';
16
18
  import { registerStripeCommands } from './commands/stripe.command.js';
17
19
  import { registerIntegrationCommands } from './commands/integration.command.js';
20
+ import { registerCheckCommands } from './commands/check.command.js';
18
21
  /**
19
22
  * Every command group the CLI has, registered on `program`.
20
23
  *
@@ -41,5 +44,78 @@ export function registerCommands(program) {
41
44
  registerStripeCommands(program);
42
45
  // After setup + event: adds `setup status` and `event auth-attempts` to those groups (TBP-541).
43
46
  registerIntegrationCommands(program);
47
+ // TBP-705 — `bridge check gates`, the last verification step of every guide.
48
+ registerCheckCommands(program);
49
+ }
50
+ /**
51
+ * Commands that never act on an app through the management API, so the
52
+ * `--app` / `currentApp` context is not resolved for them (no network call,
53
+ * and `bridge guide` keeps working offline). Matched on the command path.
54
+ */
55
+ const NO_APP_CONTEXT = [
56
+ 'auth login',
57
+ 'auth logout',
58
+ 'auth status',
59
+ 'auth use',
60
+ 'app list',
61
+ 'app use',
62
+ 'app create',
63
+ 'guide',
64
+ 'ops',
65
+ 'check',
66
+ ];
67
+ function commandPath(cmd) {
68
+ const names = [];
69
+ for (let c = cmd; c && c.parent; c = c.parent)
70
+ names.unshift(c.name());
71
+ return names.join(' ');
72
+ }
73
+ /** Raised after the error was already printed, to stop the action running. */
74
+ export class AppContextAbort extends Error {
75
+ constructor() {
76
+ super('app context could not be resolved');
77
+ this.name = 'AppContextAbort';
78
+ }
79
+ }
80
+ /**
81
+ * Global options and the hook that applies them before every action.
82
+ *
83
+ * `--profile` (TBP-628) picks WHICH stored login; `--app` (TBP-769) picks the
84
+ * app WITHIN that login, like the MCP tools' `app` argument.
85
+ *
86
+ * The `--app` name collision: `bridge auth login` has its own `--app` (pin the
87
+ * home app on the consent screen). commander lets the program-level option
88
+ * consume the flag wherever it appears, so login would never see it. The hook
89
+ * therefore hands the value back to any command that declares its own `--app`
90
+ * and, for that command, does not treat it as an app selection.
91
+ */
92
+ export function registerGlobalOptions(program) {
93
+ program
94
+ .option('--profile <label|app-id|app-name>', 'Use a specific stored credential for this command (env: BRIDGE_PROFILE)')
95
+ .option('--app <id|name>', 'Act on another app of your workspace login for this command (default: the app chosen with `bridge app use`, else the home app). On `auth login` it pins the home app instead');
96
+ program.hook('preAction', async (thisCommand, actionCommand) => {
97
+ // Runs before any subcommand action, so `getManagementClient()` sees the
98
+ // overrides wherever in the tree it is eventually called from.
99
+ setProfileOverride(thisCommand.opts().profile);
100
+ const app = thisCommand.opts().app;
101
+ const ownsAppOption = actionCommand !== thisCommand && actionCommand.options.some((o) => o.long === '--app');
102
+ if (ownsAppOption) {
103
+ if (app !== undefined)
104
+ actionCommand.setOptionValue('app', app);
105
+ setAppOverride(null);
106
+ return;
107
+ }
108
+ setAppOverride(app);
109
+ const path = commandPath(actionCommand);
110
+ if (NO_APP_CONTEXT.some((p) => path === p || path.startsWith(`${p} `)))
111
+ return;
112
+ try {
113
+ await prepareAppContext();
114
+ }
115
+ catch (err) {
116
+ outputError(err);
117
+ throw new AppContextAbort();
118
+ }
119
+ });
44
120
  }
45
121
  //# 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":"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"}
@@ -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,12 +240,13 @@ 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
 
246
- Run `bridge guide integration-success` to fetch the success message template, then output it personalised for this project.
249
+ Fetch the success message template (MCP: `get_integration_guide` topic `integration-success`; CLI: `bridge guide integration-success`), then output it personalised for this project.
247
250
 
248
251
  **Substitutions (always apply):**
249
252
 
@@ -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
 
@@ -199,7 +201,7 @@ bridge plan feature add <plan> <feature_key> --name "<Display name>"
199
201
  on the plan; every `hard` quota is also an entitlement of the same name.)
200
202
  Check the result with `bridge plan quota list`, which lists every metric with its kind.
201
203
 
202
- **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 first-class setup that trusts the browser. 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.
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.
203
205
 
204
206
  ## Step 4 — Fetch and apply the per-framework guide
205
207
 
@@ -259,6 +261,7 @@ For each integrated project, the agent verifies — do not hand this to the deve
259
261
  6. Cancel a payment — redirected back to the subscription page
260
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
261
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
262
265
 
263
266
  If anything fails, diagnose and fix before moving on.
264
267
 
@@ -14,7 +14,7 @@ Features should be controlled with feature flags. A flag can switch a route, an
14
14
  - **How much do they get? A plan limit.** Numbers are not flags: see the **payments** guide.
15
15
  - **May they change this one record? The app's own code.** Only the app knows who owns what.
16
16
 
17
- Checking a plan feature without a flag (`@RequireEntitlement` on the backend, `<Entitled>` or `$entitlements.can` in the UI) is available, and it is the exception: use it only when the developer asks for no flag.
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.
18
18
 
19
19
  ## Start from what is there
20
20
 
@@ -117,6 +117,11 @@ A rule for "people who may see reports":
117
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.
118
118
  2. `list_feature_flags` / `bridge flag list` shows no warnings on the flag.
119
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.
120
125
 
121
126
  ## Where this connects
122
127
 
@@ -38,7 +38,7 @@ Product questions. Ask, wait, never guess.
38
38
  |---|---|---|
39
39
  | Readiness checklist | `get_setup_status` | `bridge setup status` |
40
40
  | Check a project's env files | `diagnose_integration` | `bridge diagnose` |
41
- | Create a production app | `create_app`, then `use_app` | — (create it in the dashboard, then `bridge auth login --app <id>`) |
41
+ | Create a production app | `create_app`, then `use_app` | `bridge app create --name <name> --use` |
42
42
  | Production URL, default callback, allowed origins | `update_app` | `bridge app update --ui-url … --default-callback-uri … --allowed-origins …` |
43
43
  | Production callback URL | `add_redirect_uri` | `bridge app redirect-uris add <url>` |
44
44
  | Live Stripe keys | `setup_payments` | `bridge setup payments --stripe-key sk_live_… --stripe-public-key pk_live_…` |
@@ -23,6 +23,7 @@ Product questions. Ask, wait, never guess. Then show the answer back as tables (
23
23
  4. **What does each plan limit, and how much?** In their words: "2 tickets on Free, 100 on Pro", "10 seats", "1,000 exports a month".
24
24
  5. **For each limit: at the limit, block, or keep going and charge per extra unit?** If they charge, how much per unit?
25
25
  6. **For each limit: does the action call your server?** ("Does clicking Export send a request to your backend?") The answer decides where it is counted; see below.
26
+ For a seat limit, ask instead: **do invites go through Bridge's built-in team page, or through your own invite handler?** The answer decides where the seat limit is checked; see below.
26
27
  7. **Which features does each plan sell?** ("Analytics on Pro only.") They go on the plans' features lists.
27
28
  8. **Do they want a welcome page for new customers** where they choose a plan? Offer it; create it only if they say yes.
28
29
 
@@ -36,7 +37,7 @@ Product questions. Ask, wait, never guess. Then show the answer back as tables (
36
37
  - **Monthly and yearly prices of one tier are one plan with two prices,** never `pro-monthly` and `pro-yearly`. Reason: the subscription page then shows one plan with a monthly/yearly switch.
37
38
  - **A free plan gets a price of 0.** Reason: a plan with no price cannot be assigned to a workspace.
38
39
  - **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. Exports, AI completions and emails are counters (reset each billing period). Tickets, projects and stored files are gauges (never reset). Decide from that sentence and say which you chose.
39
- - **Seats are the built-in `users` metric, a gauge.** Reason: Bridge counts workspace members itself.
40
+ - **Seats are a plan limit the app names, counted from membership:** metric `seats`, kind `gauge`, source `membership` (`set_plan_quota` / `bridge plan quota set pro --metric seats --limit 10 --policy hard --kind gauge --source membership`). Reason: Bridge then counts the workspace's active members itself, pending invites included, so the app never reports a seat. No metric name is built in.
40
41
  - **Reuse metric names the app already has** (`list_plan_quotas`) before inventing new ones. Reason: two names for one thing split the count.
41
42
  - **Connect Stripe with test keys first; live keys only when going live.** Reason: nobody is charged while you build. Use `setup_payments` / `bridge setup payments`: it stores the keys and switches payments on in one step.
42
43
  - **Connect Stripe before adding paid prices.** Reason: each price is created in Stripe as it is saved.
@@ -50,11 +51,11 @@ Tell the developer this plainly; it is where integrations go wrong.
50
51
  1. **The plan holds the number.** A limit is a quota on the plan: metric, limit, `hard`, and `counter` or `gauge`.
51
52
  2. **It is counted once, where the action happens.** Ask whether the action calls the developer's server, then follow the answer. When it does, the backend counts and enforces it: in NestJS one decorator on the handler that creates the thing, `@RequireQuota('exports')`. Before the handler runs, a workspace at its limit gets `402` with `{ code: 'QUOTA_EXCEEDED', metric, used, limit, fix }` and the handler never runs. After a `2xx` answer, Bridge records one use. Nothing else to wire: no usage call, no counter table. If Bridge cannot answer, the request is refused with `503`, never let through.
52
53
  3. **Gauges send the app's own count.** `@RequireQuota('tickets', { current })` on create and `@SyncQuota('tickets', { current })` on delete, where `current` returns the app's count from its own database. There is no decrement.
53
- 4. **Seats need no count at all.** `@RequireQuota('users')` on the invite handler checks the seat limit.
54
+ 4. **Seats need no count at all; they are checked where invites happen.** Bridge counts the workspace's active members, pending invites included, and the count drops when someone is removed or suspended. Invites through the built-in team page: give it the seat limit's name, `<TeamManagementPanel seatsMetric="seats" />`, and Invite stops at the limit with the upgrade dialog. Invites through the app's own handler: `@RequireQuota('seats')` on that handler. Bridge's invite API does not refuse at the limit by itself, so one of the two is always needed. Never gate seats with a flag or an entitlement.
54
55
  5. **With a backend, the frontend only shows the count and explains the refusal.** It does not report the same metric. With no code at all, the upgrade dialog opens on a `402`, names the limit, and links to the subscription page. A member who cannot manage billing is told to ask the owner. Disabling a button before the click (`<QuotaGate>`) or showing "8 of 10" (`useQuota`) is optional.
55
- 6. **When the action happens in the browser, the browser counts it.** For an app whose action never reaches a server of its own (local-first, data on the device), `bridge.usage.report('exports')` records something that happened and `bridge.usage.set('projects', n)` how many exist, and `<QuotaGate metric="exports">` around the button stops it at the limit. This is a first-class way to run limits; it trusts the browser. Never count one metric in both places.
56
+ 6. **When the action happens in the browser, the browser counts it.** For an app whose action never reaches a server of its own (local-first, data on the device), `bridge.usage.report('exports')` records something that happened and `bridge.usage.set('projects', n)` how many exist, and `<QuotaGate metric="exports">` around the button stops it at the limit. This is a complete, first-class way to run limits. Never count one metric in both places.
56
57
  7. **`metered` never blocks.** Past the included amount it bills per unit through Stripe.
57
- 8. **A feature a plan sells goes in the plan's features list, and a flag controls it.** `analytics` on `pro` makes `bridge:billing.entitlement.analytics` true on Pro and false elsewhere; the flag that controls the feature has the rule `bridge:billing.entitlement.analytics eq true`, so the rule never names a plan. Checking the feature directly without a flag (`@RequireEntitlement`, `<Entitled>`) is the exception. No upgrade dialog opens by itself: it opens when someone opens a gated route, clicks something gated, or the backend answers `402`. A hard quota with limit 1 still works the same way; it is the older way. Never put `@RequireEntitlement` and `@RequireQuota` on the same name: at the cap the entitlement answers `403` first and the upgrade dialog never opens.
58
+ 8. **A feature a plan sells goes in the plan's features list, and a flag controls it.** `analytics` on `pro` makes `bridge:billing.entitlement.analytics` true on Pro and false elsewhere; the flag that controls the feature has the rule `bridge:billing.entitlement.analytics eq true`, so the rule never names a plan; checking the plan feature directly is the exception at the end of this guide. No upgrade dialog opens by itself: it opens when someone opens a gated route, clicks something gated, or the backend answers `402`. A hard quota with limit 1 still works the same way; it is the older way. Never put `@RequireEntitlement` and `@RequireQuota` on the same name: at the cap the entitlement answers `403` first and the upgrade dialog never opens.
58
59
  9. **The plan-choice page** (`/subscription/plan`) appears only when the app has plans and "customers must pick a plan" is on. Turn it off with `update_app` (`paymentsAutoRedirect: false`) / `bridge app update --payments-auto-redirect false`.
59
60
 
60
61
  The flag and its rule are set up in the **feature-control** guide.
@@ -79,6 +80,10 @@ The flag and its rule are set up in the **feature-control** guide.
79
80
  3. At the limit, send the decorated request with curl: it answers `402 QUOTA_EXCEEDED`. In the app, the upgrade dialog opens and names the limit.
80
81
  4. A paid plan opens Stripe Checkout and returns to the subscription page with the new plan.
81
82
 
83
+ ## Exceptions
84
+
85
+ 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.
86
+
82
87
  ## Where this connects
83
88
 
84
89
  - **Fit together:** plans, limits, roles and flags side by side (`bridge guide fit-together`).
@@ -45,7 +45,7 @@ Product questions. Ask, wait, never guess.
45
45
  - **Keep Owner as the one role with everything.** Never strip it: in the default setup it is the only role that can delete, and every workspace needs someone who can.
46
46
  - **Make the lowest sensible role the default**, by creating it with the default setting on or with `set_default_role` / `bridge role set-default <key>` for a role that exists. Reason: a forgotten role on an invitation then gives too little, not too much.
47
47
  - **When changing a role's privileges, send the full list.** Reason: the update replaces the list, and anything left out is revoked.
48
- - **When the action calls the backend, put the same flag on the endpoint** (NestJS `@RequireFlag`). Reason: the backend reads the same rule, and anyone can call the API directly.
48
+ - **When the action calls the backend, put the same flag on the endpoint** (NestJS `@RequireFeatureFlag`). Reason: the backend reads the same rule, and anyone can call the API directly.
49
49
 
50
50
  ## Do it
51
51
 
@@ -60,7 +60,7 @@ Product questions. Ask, wait, never guess.
60
60
  | Gate a route, endpoint or feature on a privilege | `create_feature_flag` (rule on `privileges`) | `bridge flag create --key reports --state on-with-rule --rule '<json>'` |
61
61
  | Code for the framework | `get_integration_guide` (topic `feature-flags` or `team`) | `bridge guide flags --framework nestjs`, `bridge guide svelte team` |
62
62
 
63
- On the NestJS backend the flag goes on the handler: `@RequireFlag('reports')`, whose rule is `privileges contains "REPORTS_VIEW"`. A person without it gets `403 FEATURE_NOT_PERMITTED`. `@RequirePrivilege` still exists for a check that must never change at runtime; it is the exception, and a hard-coded role check is never the answer.
63
+ On the NestJS backend the flag goes on the handler: `@RequireFeatureFlag('reports')`, whose rule is `privileges contains "REPORTS_VIEW"`. A person without it gets `403 FEATURE_NOT_PERMITTED`. `@RequirePrivilege` is for API tokens only (a machine calling with `x-api-key`), never for a person, and a hard-coded role check is never the answer.
64
64
 
65
65
  Setting the default on one role clears it from the previous default.
66
66
 
@@ -20,7 +20,7 @@ Product questions. Ask, wait, never guess.
20
20
  1. **Is the app for single people or for teams?** With teams, a customer invites colleagues into a shared workspace. For single people, each person simply has a workspace of their own and never sees the word.
21
21
  2. **How does someone get in the first time?** Either they sign up and get a new workspace of their own, or they can only join by invitation from someone already inside.
22
22
  3. **When someone is invited, which role should they get?** Read the app's real roles with `list_roles` first and offer those. See the **roles** guide: in the default setup a new app's default role is Member, while older apps often still default to Owner.
23
- 4. **Does the number of people per workspace depend on the plan?** ("1 user on Free, 10 on Pro.") If yes, that is a seat limit: see the **payments** guide.
23
+ 4. **Does the number of people per workspace depend on the plan?** ("1 user on Free, 10 on Pro.") If yes, that is a seat limit: see the **payments** guide. Then ask: **do invites go through Bridge's built-in team page, or through your own invite handler?**
24
24
  5. **Can one person belong to several workspaces?** Bridge supports it and shows a workspace picker at sign-in; the question is whether the developer wants to offer it in their product.
25
25
 
26
26
  **The tools will not guess these.** Called without one, a tool changes nothing and answers `DECISION_NEEDED` with the question to ask; the matching bridge command stops the same way: `invite_user` (`role`). It offers the app's role keys and names the default one.
@@ -31,7 +31,8 @@ Product questions. Ask, wait, never guess.
31
31
 
32
32
  - **Self-signup on for a product people try on their own; off for invitation-only products.** Take it from the answer to question 2 and set `tenantSelfSignup` to match. Reason: with it off, a stranger who signs up has nowhere to land.
33
33
  - **Use Bridge's team panel for inviting and managing people** (`TeamManagementPanel` in SvelteKit) rather than a page of your own. Reason: it already handles invitations, role changes and removal against the right permissions.
34
- - **Seats are the built-in `users` limit.** Reason: Bridge counts workspace members itself, so there is nothing for the app to count or report.
34
+ - **Gate the team link and the team page with a flag ruled on a privilege**, for example a flag `team-admin` with `privileges contains "USER_WRITE"` after reading `list_roles`, used as `<FeatureFlag key="team-admin">` around the link and a route rule with `featureFlag: 'team-admin'` on the page. Never a list of role names in code. Reason: every gate in app code is a flag, and a privilege rule survives renamed roles.
35
+ - **Seats are a plan limit you name (`seats`), a gauge counted from membership.** Reason: Bridge counts the workspace's active members itself, pending invites included, so there is nothing for the app to count or report. Check it where invites happen: `<TeamManagementPanel seatsMetric="seats" />` on the built-in team page, or `@RequireQuota('seats')` on the app's own invite handler. Bridge's invite API does not refuse at the limit by itself. Never a flag or an entitlement.
35
36
  - **Never create workspaces from the app's own code for sign-ups.** Reason: Bridge creates one at signup when self-signup is on; creating them elsewhere makes duplicates.
36
37
  - **Create workspaces by hand only for setup and support** (an existing customer you are migrating, or a demo).
37
38
 
@@ -43,7 +44,7 @@ Product questions. Ask, wait, never guess.
43
44
  | Create a workspace by hand | `create_tenant` | `bridge tenant create --name … --owner-email …` |
44
45
  | Invite someone into a workspace | `invite_user` | `bridge user invite --email … --role … --tenant-id …` |
45
46
  | Change someone's role | `update_user` | `bridge user update --email … --role … --tenant-id …` |
46
- | Limit seats per plan | `set_plan_quota` (metric `users`, kind `gauge`) | `bridge plan quota set pro --metric users --limit 10 --policy hard --kind gauge` |
47
+ | Limit seats per plan | `set_plan_quota` (metric `seats`, kind `gauge`, source `membership`) | `bridge plan quota set pro --metric seats --limit 10 --policy hard --kind gauge --source membership` |
47
48
  | Code for the framework | `get_integration_guide` (topic `team`) | `bridge guide svelte team` |
48
49
 
49
50
  Pass the role key the developer chose on every invitation you make on their behalf; without one, `invite_user` and `bridge user invite` invite nobody and ask which role.