@nebulr-group/bridge-cli 0.6.0-beta.2 → 0.6.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/commands/flag.command.d.ts.map +1 -1
- package/dist/commands/flag.command.js +19 -6
- package/dist/commands/flag.command.js.map +1 -1
- package/dist/commands/guide.command.d.ts +6 -0
- package/dist/commands/guide.command.d.ts.map +1 -1
- package/dist/commands/guide.command.js +101 -1
- package/dist/commands/guide.command.js.map +1 -1
- package/dist/commands/journeys.d.ts +2 -1
- package/dist/commands/journeys.d.ts.map +1 -1
- package/dist/commands/journeys.js +15 -1
- package/dist/commands/journeys.js.map +1 -1
- package/dist/commands/plan.command.d.ts +39 -0
- package/dist/commands/plan.command.d.ts.map +1 -1
- package/dist/commands/plan.command.js +134 -10
- package/dist/commands/plan.command.js.map +1 -1
- package/dist/commands/role.command.d.ts.map +1 -1
- package/dist/commands/role.command.js +2 -1
- package/dist/commands/role.command.js.map +1 -1
- package/dist/commands/tenant.command.d.ts.map +1 -1
- package/dist/commands/tenant.command.js +12 -5
- package/dist/commands/tenant.command.js.map +1 -1
- package/dist/commands/token.command.d.ts.map +1 -1
- package/dist/commands/token.command.js +6 -0
- package/dist/commands/token.command.js.map +1 -1
- package/dist/commands/user.command.d.ts.map +1 -1
- package/dist/commands/user.command.js +6 -0
- package/dist/commands/user.command.js.map +1 -1
- package/dist/flag-coherence.d.ts +42 -0
- package/dist/flag-coherence.d.ts.map +1 -0
- package/dist/flag-coherence.js +103 -0
- package/dist/flag-coherence.js.map +1 -0
- package/dist/preview.d.ts +9 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/preview.js +63 -0
- package/dist/preview.js.map +1 -0
- package/dist/prompts/auth-master-integration-prompt.md +7 -5
- package/dist/prompts/billing/master.md +35 -18
- package/dist/prompts/decisions/feature-control.md +105 -0
- package/dist/prompts/decisions/going-live.md +59 -0
- package/dist/prompts/decisions/login.md +68 -0
- package/dist/prompts/decisions/look-and-feel.md +62 -0
- package/dist/prompts/decisions/payments.md +81 -0
- package/dist/prompts/decisions/roles.md +72 -0
- package/dist/prompts/decisions/teams.md +56 -0
- package/dist/prompts/flags/master.md +8 -8
- package/dist/prompts/integration-success.md +4 -5
- package/dist/prompts/mcp-tools.json +77 -0
- package/dist/prompts/mechanisms.md +284 -0
- package/dist/prompts/orientation.md +40 -0
- package/dist/shape.d.ts +25 -0
- package/dist/shape.d.ts.map +1 -0
- package/dist/shape.js +74 -0
- package/dist/shape.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { getManagementClient } from './config.js';
|
|
2
|
+
/** Attributes whose value names a plan (the JWT `plan` claim, billing slug). */
|
|
3
|
+
const PLAN_ATTRIBUTES = ['tenant.plan', 'bridge:billing.plan'];
|
|
4
|
+
/** Attributes whose value names a role (the JWT `role` claim). */
|
|
5
|
+
const ROLE_ATTRIBUTES = ['user.role'];
|
|
6
|
+
/** Operators whose values are compared for equality with the attribute. */
|
|
7
|
+
const EQUALITY_OPERATORS = ['eq', 'neq', 'in', 'not_in'];
|
|
8
|
+
function sameValue(a, b) {
|
|
9
|
+
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
10
|
+
}
|
|
11
|
+
function listed(entries) {
|
|
12
|
+
return entries.length ? entries.map((e) => e.key).join(', ') : '(none)';
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The warnings for one flag. Pure. `catalog` omitted skips the
|
|
16
|
+
* dangling-reference check (the caller could not read plans/roles).
|
|
17
|
+
*/
|
|
18
|
+
export function flagCoherenceWarnings(flag, catalog) {
|
|
19
|
+
const warnings = [];
|
|
20
|
+
const state = flag.state ?? 'off';
|
|
21
|
+
const valueType = flag.valueType ?? 'boolean';
|
|
22
|
+
const onValue = flag.onValue !== undefined ? flag.onValue : true;
|
|
23
|
+
const offValue = flag.offValue !== undefined ? flag.offValue : false;
|
|
24
|
+
if (state === 'on') {
|
|
25
|
+
if (valueType === 'boolean' && onValue === false) {
|
|
26
|
+
warnings.push(`Flag "${flag.key}" is switched on, but its onValue is false, so it returns false for ` +
|
|
27
|
+
'everyone: it looks on and behaves off. Set onValue to true, or switch the flag off.');
|
|
28
|
+
}
|
|
29
|
+
else if (sameValue(onValue, offValue)) {
|
|
30
|
+
warnings.push(`Flag "${flag.key}" is switched on, but its onValue equals its offValue ` +
|
|
31
|
+
`(${JSON.stringify(onValue)}), so switching it on or off changes nothing.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (catalog && flag.rule) {
|
|
35
|
+
for (const branch of flag.rule.branches ?? []) {
|
|
36
|
+
for (const condition of branch.conditions ?? []) {
|
|
37
|
+
if (!EQUALITY_OPERATORS.includes(condition.operator))
|
|
38
|
+
continue;
|
|
39
|
+
const isPlan = PLAN_ATTRIBUTES.includes(condition.attribute);
|
|
40
|
+
const isRole = ROLE_ATTRIBUTES.includes(condition.attribute);
|
|
41
|
+
if (!isPlan && !isRole)
|
|
42
|
+
continue;
|
|
43
|
+
const known = isPlan ? catalog.plans : catalog.roles;
|
|
44
|
+
const keys = new Set(known.map((k) => k.key));
|
|
45
|
+
const what = isPlan ? 'plan' : 'role';
|
|
46
|
+
for (const value of condition.values ?? []) {
|
|
47
|
+
if (typeof value !== 'string' || keys.has(value))
|
|
48
|
+
continue;
|
|
49
|
+
// The token carries the plan/role KEY, so a display name never matches.
|
|
50
|
+
const byName = known.find((k) => k.name === value);
|
|
51
|
+
if (byName) {
|
|
52
|
+
warnings.push(`Flag "${flag.key}" has a rule on ${condition.attribute} ${condition.operator} ` +
|
|
53
|
+
`"${value}", which matches the ${what} named "${value}" by name; rules compare the ` +
|
|
54
|
+
`${what} key — use "${byName.key}".`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
warnings.push(`Flag "${flag.key}" has a rule on ${condition.attribute} ${condition.operator} ` +
|
|
58
|
+
`"${value}", but the app has no ${what} "${value}", so no real ` +
|
|
59
|
+
`${isPlan ? 'workspace' : 'user'} can carry it. Existing ${what} keys: ` +
|
|
60
|
+
`${listed(known)}.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return warnings;
|
|
66
|
+
}
|
|
67
|
+
/** True when any condition reads a plan or role attribute (worth fetching the catalog). */
|
|
68
|
+
export function ruleReadsPlanOrRole(rule) {
|
|
69
|
+
if (!rule)
|
|
70
|
+
return false;
|
|
71
|
+
return (rule.branches ?? []).some((b) => (b.conditions ?? []).some((c) => PLAN_ATTRIBUTES.includes(c.attribute) || ROLE_ATTRIBUTES.includes(c.attribute)));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Plans and roles for the dangling-reference check. Never throws: a login
|
|
75
|
+
* that cannot read plans or roles gets no dangling-reference warning rather
|
|
76
|
+
* than a failed flag command — a warning never blocks.
|
|
77
|
+
*/
|
|
78
|
+
export async function loadCoherenceCatalog() {
|
|
79
|
+
try {
|
|
80
|
+
const client = getManagementClient();
|
|
81
|
+
const [plans, roles] = await Promise.all([client.plans.list(), client.roles.list()]);
|
|
82
|
+
return {
|
|
83
|
+
plans: (plans ?? []).map((p) => ({ key: String(p.key), name: p.name })),
|
|
84
|
+
roles: (roles ?? []).map((r) => ({ key: String(r.key), name: r.name })),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Warnings for a list of flags, fetching the catalog once and only if a rule needs it. */
|
|
92
|
+
export async function warningsForFlags(flags) {
|
|
93
|
+
const catalog = flags.some((f) => ruleReadsPlanOrRole(f.rule)) ? await loadCoherenceCatalog() : undefined;
|
|
94
|
+
return flags.map((f) => flagCoherenceWarnings(f, catalog));
|
|
95
|
+
}
|
|
96
|
+
/** A flag with its warnings attached — only when there are any, so a coherent flag prints as before. */
|
|
97
|
+
export async function withFlagWarnings(flag) {
|
|
98
|
+
if (!flag || typeof flag !== 'object')
|
|
99
|
+
return flag;
|
|
100
|
+
const [warnings] = await warningsForFlags([flag]);
|
|
101
|
+
return warnings.length ? { ...flag, warnings } : flag;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=flag-coherence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flag-coherence.js","sourceRoot":"","sources":["../src/flag-coherence.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class PreviewNotSupportedError extends Error {
|
|
2
|
+
readonly path: string;
|
|
3
|
+
readonly serverBody?: unknown | undefined;
|
|
4
|
+
readonly code = "PREVIEW_NOT_SUPPORTED";
|
|
5
|
+
constructor(path: string, serverBody?: unknown | undefined);
|
|
6
|
+
}
|
|
7
|
+
export declare function previewPath(path: string): string;
|
|
8
|
+
export declare function dryRunPreview(path: string, headers?: Record<string, string>): Promise<unknown>;
|
|
9
|
+
//# sourceMappingURL=preview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AAmBA,qBAAa,wBAAyB,SAAQ,KAAK;IAErC,QAAQ,CAAC,IAAI,EAAE,MAAM;IAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO;IADhE,QAAQ,CAAC,IAAI,2BAA2B;gBACnB,IAAI,EAAE,MAAM,EAAW,UAAU,CAAC,EAAE,OAAO,YAAA;CAOjE;AAcD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAgBpG"}
|
package/dist/preview.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { HttpError } from '@nebulr-group/bridge-auth-core';
|
|
2
|
+
import { getManagementHttp } from './config.js';
|
|
3
|
+
/*
|
|
4
|
+
* TBP-709 (E) — `--dry-run` on the destructive commands.
|
|
5
|
+
*
|
|
6
|
+
* The preview is the server's: every delete route has a read-only sibling,
|
|
7
|
+
* `GET <route>/deletion-preview`, that answers with what the delete would
|
|
8
|
+
* remove and removes nothing. It passes the same guards as the delete, so a
|
|
9
|
+
* login without the destructive privilege gets the same 403 — and it is the
|
|
10
|
+
* same answer the MCP tools' `dryRun` shows.
|
|
11
|
+
*
|
|
12
|
+
* Why a GET on its own path, and never `?dryRun=true` on the DELETE: this CLI
|
|
13
|
+
* is released separately from the server and is pointed at servers that
|
|
14
|
+
* predate previews. Such a server ignores an unknown query parameter and
|
|
15
|
+
* PERFORMS THE DELETE; it answers an unknown path with 404. So a 404 here
|
|
16
|
+
* means "no previews on this server", and the command stops — it never falls
|
|
17
|
+
* back to the delete.
|
|
18
|
+
*/
|
|
19
|
+
export class PreviewNotSupportedError extends Error {
|
|
20
|
+
path;
|
|
21
|
+
serverBody;
|
|
22
|
+
code = 'PREVIEW_NOT_SUPPORTED';
|
|
23
|
+
constructor(path, serverBody) {
|
|
24
|
+
super(`This Bridge server does not support deletion previews yet (GET ${path} answered 404). ` +
|
|
25
|
+
'Nothing was deleted. Run without --dry-run only if you mean to delete.');
|
|
26
|
+
this.path = path;
|
|
27
|
+
this.serverBody = serverBody;
|
|
28
|
+
this.name = 'PreviewNotSupportedError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/*
|
|
32
|
+
* A server that HAS the route still 404s a record that does not exist, with
|
|
33
|
+
* its own message ("tenant not found"). An unknown route gets the framework's
|
|
34
|
+
* "Cannot GET …" (or a bare "Not Found" from a proxy in front). Only the
|
|
35
|
+
* former is passed through as-is; either way the command fails and deletes
|
|
36
|
+
* nothing — this only decides which message the user reads.
|
|
37
|
+
*/
|
|
38
|
+
function isRecordNotFound(body) {
|
|
39
|
+
const message = body && typeof body === 'object' ? body.message : undefined;
|
|
40
|
+
return typeof message === 'string' && !/^Cannot GET\b/.test(message) && message !== 'Not Found';
|
|
41
|
+
}
|
|
42
|
+
export function previewPath(path) {
|
|
43
|
+
return `${path}/deletion-preview`;
|
|
44
|
+
}
|
|
45
|
+
export async function dryRunPreview(path, headers) {
|
|
46
|
+
const route = previewPath(path);
|
|
47
|
+
let preview;
|
|
48
|
+
try {
|
|
49
|
+
preview = await getManagementHttp().get(route, headers);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (err instanceof HttpError && err.status === 404 && !isRecordNotFound(err.body)) {
|
|
53
|
+
throw new PreviewNotSupportedError(route, err.body);
|
|
54
|
+
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...(preview && typeof preview === 'object' ? preview : {}),
|
|
59
|
+
dryRun: true,
|
|
60
|
+
next: 'Nothing was changed. Run the same command without --dry-run to go ahead.',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=preview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview.js","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IAE5B;IAAuB;IADnC,IAAI,GAAG,uBAAuB,CAAC;IACxC,YAAqB,IAAY,EAAW,UAAoB;QAC9D,KAAK,CACH,kEAAkE,IAAI,kBAAkB;YACtF,wEAAwE,CAC3E,CAAC;QAJiB,SAAI,GAAJ,IAAI,CAAQ;QAAW,eAAU,GAAV,UAAU,CAAU;QAK9D,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AAED;;;;;;GAMG;AACH,SAAS,gBAAgB,CAAC,IAAa;IACrC,MAAM,OAAO,GAAG,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAE,IAA8B,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IACvG,OAAO,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,OAAO,KAAK,WAAW,CAAC;AAClG,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,GAAG,IAAI,mBAAmB,CAAC;AACpC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAY,EAAE,OAAgC;IAChF,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,OAAgC,CAAC;IACrC,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,iBAAiB,EAAE,CAAC,GAAG,CAA0B,KAAK,EAAE,OAAO,CAAC,CAAC;IACnF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,SAAS,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAClF,MAAM,IAAI,wBAAwB,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;QACtD,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IACD,OAAO;QACL,GAAG,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1D,MAAM,EAAE,IAAI;QACZ,IAAI,EAAE,0EAA0E;KACjF,CAAC;AACJ,CAAC"}
|
|
@@ -90,12 +90,12 @@ bridge guide <framework> # hosted-auth audit checklist
|
|
|
90
90
|
bridge guide <framework> sdk-auth # SDK-auth audit checklist (in-app forms)
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
The framework guide tells you precisely what must exist (bootstrap call, provider wrapping,
|
|
93
|
+
The framework guide tells you precisely what must exist (bootstrap call, provider wrapping, the auth pages, environment variables). Apply that checklist to the project on disk.
|
|
94
94
|
|
|
95
95
|
**General decision matrix** (independent of framework):
|
|
96
96
|
|
|
97
|
-
- **Wiring complete AND
|
|
98
|
-
- **Bootstrap wiring complete BUT auth
|
|
97
|
+
- **Wiring complete AND the auth pages are served** → Bridge is fully integrated. Skip to **Step 6b** and output the success message.
|
|
98
|
+
- **Bootstrap wiring complete BUT the auth pages are missing** → Bridge is partially integrated. Without them signup verification, password reset and the SSO callback land on a 404. On SvelteKit the fix is one file, `src/routes/auth/[...bridge]/+page.svelte` rendering `<BridgeAuthRoutes />`; hand-written pages that only render a Bridge component can then be deleted. On other frameworks the guide names the files. Add only what's missing.
|
|
99
99
|
- **Bootstrap wiring incomplete** (regardless of route state) → Offer to complete the initial setup (proceed to Step 4).
|
|
100
100
|
- **Bridge is NOT installed** → Continue with Steps 2–6 for fresh setup.
|
|
101
101
|
|
|
@@ -193,9 +193,11 @@ For each confirmed project, fetch the framework-specific integration prompt.
|
|
|
193
193
|
| Express | `bridge guide express` |
|
|
194
194
|
|
|
195
195
|
Follow the per-framework guide instructions verbatim. Pass these values from Step 3:
|
|
196
|
-
- `appId` — same for all projects
|
|
196
|
+
- `appId` — same for all projects; it goes in the project's `.env` (`VITE_BRIDGE_APP_ID` on SvelteKit, `BRIDGE_APP_ID` on NestJS), which the plugin reads itself
|
|
197
197
|
- `packageManager` — detected in Step 1 (use it for all install commands)
|
|
198
198
|
|
|
199
|
+
**What the handover looks like.** The guides are short because the plugins serve their own pages. On SvelteKit the whole frontend integration is the `.env` line, `src/routes/+layout.ts` (`export const load = bridgeBootstrap({ rules })`), `src/routes/+layout.svelte` (`<BridgeBootstrap>`) and one auth file, `src/routes/auth/[...bridge]/+page.svelte` — about fifteen lines. On NestJS it is `BridgeModule.forRoot({ guard: { global: true } })` in `AppModule` and the `.env` line. Do not write login, signup, callback or password pages by hand, and no fetch helper for tokens. `bridge guide mechanisms` explains the levels and customisation rungs if the developer asks for more.
|
|
200
|
+
|
|
199
201
|
**Order:** Frontend first, then backend. This lets you verify login works before adding backend guards.
|
|
200
202
|
|
|
201
203
|
## Step 5 — Route protection defaults
|
|
@@ -248,6 +250,6 @@ After delivering the message, the integration is complete.
|
|
|
248
250
|
After auth is wired and the success banner is printed, the developer commonly wants flags and billing next. Mention these one-liners so they know how to continue:
|
|
249
251
|
|
|
250
252
|
- **Feature Flags 2.0** → `bridge guide flags` (auto-detects framework, sets up flag evaluation, telemetry, realtime)
|
|
251
|
-
- **Billing 2.0** → `bridge guide billing` (
|
|
253
|
+
- **Billing 2.0** → `bridge guide billing` (plans, the subscription pages, the paywall, plan limits enforced by the backend)
|
|
252
254
|
|
|
253
255
|
Do not run them automatically — the developer decides when they want each track.
|
|
@@ -29,7 +29,7 @@ Scan the current directory and its immediate subdirectories for `package.json` f
|
|
|
29
29
|
|
|
30
30
|
3. **Bridge Auth installed?** — look for `@nebulr-group/bridge-<framework>` in dependencies. Billing is scoped to a workspace/tenant and requires Bridge Auth. If not present, stop and run `bridge guide` first.
|
|
31
31
|
|
|
32
|
-
4. **Billing already wired?** — check whether
|
|
32
|
+
4. **Billing already wired?** — check whether the subscription pages are served (on SvelteKit: `src/routes/subscription/[...bridge]/+page.svelte` rendering `<BridgeBillingRoutes />`) and whether `bridge plan list` returns at least one plan. If billing looks complete, jump to **Step 1b**.
|
|
33
33
|
|
|
34
34
|
5. **Existing billing system?** — look for `stripe`, `@stripe/*`, `paddle-*`, `lemon-squeezy`, `chargebee`. Flag any found.
|
|
35
35
|
|
|
@@ -38,11 +38,12 @@ Scan the current directory and its immediate subdirectories for `package.json` f
|
|
|
38
38
|
If Step 1 found billing already partially or fully set up, audit it before doing anything:
|
|
39
39
|
|
|
40
40
|
- Run `bridge plan list` — are plans defined?
|
|
41
|
-
- Check
|
|
41
|
+
- Check the subscription pages are served (SvelteKit: the `subscription/[...bridge]` file above; other frameworks: the route the per-framework guide names)
|
|
42
42
|
- Check the root layout for `<BridgeBillingNotice />`
|
|
43
|
+
- Check every backend handler that creates a limited thing carries the plan-limit decorator (NestJS: `@RequireQuota`)
|
|
43
44
|
|
|
44
45
|
**Decision:**
|
|
45
|
-
- **Fully wired** (plans exist,
|
|
46
|
+
- **Fully wired** (plans exist, subscription pages served, notice in layout, limits on the backend) → skip to **Step 6** and output the success message
|
|
46
47
|
- **Partially wired** → tell the user exactly what's missing and only add what's absent
|
|
47
48
|
- **Plans missing** → continue from Step 3 (pricing model)
|
|
48
49
|
|
|
@@ -92,13 +93,16 @@ From their answer, map everything to confirmation tables before creating anythin
|
|
|
92
93
|
|
|
93
94
|
**Quotas** (per-resource limits that differ between plans):
|
|
94
95
|
|
|
95
|
-
| plan | metric | limit | policy |
|
|
96
|
-
|
|
97
|
-
| `free` | `ai_completions` | 100 | hard |
|
|
98
|
-
| `pro` | `ai_completions` | 1000 | hard |
|
|
96
|
+
| plan | metric | limit | policy | kind |
|
|
97
|
+
|------|--------|-------|--------|------|
|
|
98
|
+
| `free` | `ai_completions` | 100 | hard | counter |
|
|
99
|
+
| `pro` | `ai_completions` | 1000 | hard | counter |
|
|
100
|
+
| `free` | `projects` | 3 | hard | gauge |
|
|
99
101
|
|
|
100
102
|
Use `hard` when overage should be blocked. Use `metered` when overage should bill via Stripe.
|
|
101
103
|
|
|
104
|
+
**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.
|
|
105
|
+
|
|
102
106
|
**Entitlements** (features on for some plans, off for others):
|
|
103
107
|
|
|
104
108
|
| plan | key | value |
|
|
@@ -106,6 +110,8 @@ Use `hard` when overage should be blocked. Use `metered` when overage should bil
|
|
|
106
110
|
| `free` | `advanced_analytics` | off |
|
|
107
111
|
| `pro` | `advanced_analytics` | on |
|
|
108
112
|
|
|
113
|
+
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`.
|
|
114
|
+
|
|
109
115
|
If there are no per-plan limits or feature differences, the quotas and entitlements tables are empty — skip those commands below.
|
|
110
116
|
|
|
111
117
|
**Do not create anything until the developer confirms all tables.**
|
|
@@ -135,9 +141,12 @@ and payment failures will not sync. Tell the developer and follow the repair hin
|
|
|
135
141
|
If not connected, ask the developer for their Stripe keys (from `dashboard.stripe.com/apikeys`):
|
|
136
142
|
|
|
137
143
|
```bash
|
|
138
|
-
bridge
|
|
144
|
+
bridge setup payments --stripe-key sk_test_... --stripe-public-key pk_test_...
|
|
139
145
|
```
|
|
140
146
|
|
|
147
|
+
This stores the keys **and** switches payments on in one step. (`bridge stripe connect` only
|
|
148
|
+
stores the keys and leaves payments off.)
|
|
149
|
+
|
|
141
150
|
Now set any **remaining** prices — one command per Prices-table row not already covered by the
|
|
142
151
|
`plan create` call above. `plan price set` is idempotent (keyed on currency + interval), so a
|
|
143
152
|
tier with monthly **and** yearly pricing is just one more call against the same plan key:
|
|
@@ -152,9 +161,12 @@ feature at the cap (entitlement flips off); a `metered` quota bills overage per
|
|
|
152
161
|
unit and never blocks — it requires a per-unit price:
|
|
153
162
|
|
|
154
163
|
```bash
|
|
155
|
-
# Hard cap (block at the limit)
|
|
164
|
+
# Hard cap (block at the limit) — a counter, the default
|
|
156
165
|
bridge plan quota set <plan> --metric <key> --limit <n> --policy hard
|
|
157
166
|
|
|
167
|
+
# Hard cap on something that exists (a gauge: projects, tickets)
|
|
168
|
+
bridge plan quota set <plan> --metric <key> --limit <n> --policy hard --kind gauge
|
|
169
|
+
|
|
158
170
|
# Metered: first <limit> units free, then --price-amount per unit (limit 0 = pure
|
|
159
171
|
# per-unit, billed from unit 1). Currency defaults to the plan's price currency.
|
|
160
172
|
bridge plan quota set <plan> --metric <key> --limit <n> --policy metered --price-amount <perUnit> [--price-currency <cur>]
|
|
@@ -162,6 +174,9 @@ bridge plan quota set <plan> --metric <key> --limit <n> --policy metered --price
|
|
|
162
174
|
|
|
163
175
|
Run one command per row. (There is no `plan entitlement set` command —
|
|
164
176
|
entitlements are derived from `hard` quotas automatically; do not invent one.)
|
|
177
|
+
Check the result with `bridge plan quota list`, which lists every metric with its kind.
|
|
178
|
+
|
|
179
|
+
**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.
|
|
165
180
|
|
|
166
181
|
## Step 4 — Fetch and apply the per-framework guide
|
|
167
182
|
|
|
@@ -193,6 +208,7 @@ opts out** — it's the expected first-run experience.
|
|
|
193
208
|
- The per-framework guide (Step 4) mounts the plugin's subscription pages, which include a
|
|
194
209
|
default paywall page (on SvelteKit: `/subscription/plan`, served by the subscription
|
|
195
210
|
catch-all). Confirm that part ran. Do **not** create a separate welcome page on your own.
|
|
211
|
+
- The paywall applies once the app has plans; `paymentsAutoRedirect` (below) switches it.
|
|
196
212
|
- A dedicated onboarding page such as `/welcome` is a product choice: **ask the developer**
|
|
197
213
|
whether they want one. Only if they say yes, add it as the per-framework guide shows and
|
|
198
214
|
point `billing.paywallRoute` at it.
|
|
@@ -205,8 +221,8 @@ If the developer does **not** want a forced paywall, disable it with one command
|
|
|
205
221
|
bridge app update --payments-auto-redirect false
|
|
206
222
|
```
|
|
207
223
|
|
|
208
|
-
With the paywall off, users can enter the app without choosing a plan;
|
|
209
|
-
|
|
224
|
+
With the paywall off, users can enter the app without choosing a plan; the subscription pages
|
|
225
|
+
still work for self-serve upgrades.
|
|
210
226
|
|
|
211
227
|
## Step 5 — Verify
|
|
212
228
|
|
|
@@ -219,6 +235,7 @@ For each integrated project, the agent verifies — do not hand this to the deve
|
|
|
219
235
|
5. Complete a test payment — redirected back with the updated plan showing
|
|
220
236
|
6. Cancel a payment — redirected back to the subscription page
|
|
221
237
|
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
|
|
238
|
+
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
|
|
222
239
|
|
|
223
240
|
If anything fails, diagnose and fix before moving on.
|
|
224
241
|
|
|
@@ -238,7 +255,7 @@ If anything fails, diagnose and fix before moving on.
|
|
|
238
255
|
Billing is live in [project-name].
|
|
239
256
|
|
|
240
257
|
✅ Plans — [plan-names]
|
|
241
|
-
✅
|
|
258
|
+
✅ Subscription page — [subscription-route]
|
|
242
259
|
✅ Paywall — [paywall-summary]
|
|
243
260
|
✅ Lifecycle notices — auto-render on trial / payment failure / cancel
|
|
244
261
|
✅ Quotas and entitlements — [quota-summary]
|
|
@@ -258,18 +275,18 @@ If anything fails, diagnose and fix before moving on.
|
|
|
258
275
|
Fill every `[placeholder]` with real values from the integration:
|
|
259
276
|
- `[project-name]` — folder name and/or `package.json` name
|
|
260
277
|
- `[plan-names]` — the plan keys (e.g. `free`, `premium`)
|
|
261
|
-
- `[
|
|
278
|
+
- `[subscription-route]` — where the subscription page is served (SvelteKit default `/subscription`)
|
|
262
279
|
- `[paywall-summary]` — e.g. "new users sent to `/subscription/plan` to pick a plan" (or "off — `paymentsAutoRedirect false`")
|
|
263
|
-
- `[quota-summary]` — metric keys and
|
|
280
|
+
- `[quota-summary]` — metric keys, limits and kind, and the backend handlers that enforce them, or "none"
|
|
264
281
|
- `[what-i-actually-did]` / `[what-i-changed]` — every file created or modified, every command run
|
|
265
282
|
|
|
266
283
|
## Step 7 — Offer follow-on tracks
|
|
267
284
|
|
|
268
285
|
After the success banner, mention what they can add next:
|
|
269
286
|
|
|
270
|
-
- **
|
|
271
|
-
- **
|
|
272
|
-
- **Portal** —
|
|
287
|
+
- **Plan limits on the backend** — if the backend was not wired in this session: `bridge guide billing --framework nestjs` (one decorator per handler)
|
|
288
|
+
- **Limits in the UI** — an upgrade dialog opens on its own when the backend refuses; disabling a button before the click or showing usage numbers is in the frontend guide, Step 3
|
|
289
|
+
- **Portal** — "Manage billing" is already on the subscription page; the per-framework guide shows how to place it elsewhere
|
|
273
290
|
|
|
274
291
|
Do not run these automatically — the developer decides when they want each.
|
|
275
292
|
|
|
@@ -282,4 +299,4 @@ Do not run these automatically — the developer decides when they want each.
|
|
|
282
299
|
| `hard` | Entitlement flips off at cap; no overage | Seat counts, included features |
|
|
283
300
|
| `metered` | Overage bills as a Stripe metered price; usage continues | API calls, storage |
|
|
284
301
|
|
|
285
|
-
|
|
302
|
+
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.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Feature control — decision guide
|
|
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.
|
|
4
|
+
|
|
5
|
+
> Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). How the pieces work underneath: `bridge guide mechanisms`.
|
|
6
|
+
|
|
7
|
+
## The rule that decides everything else
|
|
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.
|
|
12
|
+
|
|
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.
|
|
14
|
+
|
|
15
|
+
## Start from what is there
|
|
16
|
+
|
|
17
|
+
| Read | MCP | CLI |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Every flag with its rule, and warnings on flags that cannot mean what they look like | `list_feature_flags` | `bridge flag list` |
|
|
20
|
+
| 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` |
|
|
22
|
+
|
|
23
|
+
## Ask the developer
|
|
24
|
+
|
|
25
|
+
Product questions. Ask, wait, never guess.
|
|
26
|
+
|
|
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.)
|
|
31
|
+
|
|
32
|
+
## Decide yourself
|
|
33
|
+
|
|
34
|
+
- **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
|
+
- **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.
|
|
37
|
+
- **Always give the rule an `otherwiseValue`, and make it the safe answer (usually `false`).** Reason: anyone the rule does not match gets it.
|
|
38
|
+
- **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
|
+
- **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".
|
|
40
|
+
- **Treat a warning from `list_feature_flags` as a bug to fix now.**
|
|
41
|
+
|
|
42
|
+
## The attributes a rule can test
|
|
43
|
+
|
|
44
|
+
These are the exact names and values. A rule on any other name matches nothing unless the app sends that attribute itself.
|
|
45
|
+
|
|
46
|
+
**Sent by the browser SDKs** (SvelteKit, React, Next.js, Angular) for a signed-in person, read from their token:
|
|
47
|
+
|
|
48
|
+
| Attribute | Value | Compared |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `user.id` | the person's id | exactly |
|
|
51
|
+
| `user.email` | their email | exactly |
|
|
52
|
+
| `user.role` | the role **key** as created, e.g. `OWNER`, `ADMIN` | exactly: `admin` does not match `ADMIN` |
|
|
53
|
+
| `tenant.id` | the workspace id | exactly |
|
|
54
|
+
| `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 |
|
|
56
|
+
|
|
57
|
+
**Also sent by the browser SDKs, from the workspace's subscription:**
|
|
58
|
+
|
|
59
|
+
| Attribute | Value |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `bridge:billing.plan` | the plan key, compared exactly |
|
|
62
|
+
| `bridge:billing.subscription.status` | `trial`, `active`, `past_due`, `cancel_at_period_end` or `canceled` |
|
|
63
|
+
| `bridge:billing.trial` | `true` or `false` |
|
|
64
|
+
| `bridge:billing.quota.<metric>.used` / `.limit` / `.remaining` | numbers |
|
|
65
|
+
| `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) |
|
|
67
|
+
|
|
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.
|
|
69
|
+
|
|
70
|
+
**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
|
+
|
|
72
|
+
Operators: `eq`, `neq`, `in`, `not_in`, `contains`, `not_contains`, `gt`, `lt`, `between`, `regex`, `exists`, `not_exists`.
|
|
73
|
+
|
|
74
|
+
A condition's values are always a list, even for a single value.
|
|
75
|
+
|
|
76
|
+
## Do it
|
|
77
|
+
|
|
78
|
+
| Step | MCP | CLI |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| Create a flag with its rule | `create_feature_flag` | `bridge flag create --key analytics --state on-with-rule --rule '<json>'` |
|
|
81
|
+
| Change a rule or values | `update_feature_flag` | `bridge flag update --key analytics --rule '<json>'` |
|
|
82
|
+
| 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` |
|
|
84
|
+
| Switch at a set time | `set_flag_schedule` | `bridge flag schedule set analytics --at … --state on` |
|
|
85
|
+
| Copy flags between apps | `export_feature_flags`, `import_feature_flags` | `bridge flag export`, `bridge flag import <file>` |
|
|
86
|
+
| Code for the framework | `get_integration_guide` (topic `feature-flags`) | `bridge guide flags --framework svelte` |
|
|
87
|
+
|
|
88
|
+
A rule for "Pro only":
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{ "branches": [{ "conditions": [{ "attribute": "tenant.plan", "operator": "eq", "values": ["pro"] }], "returnValue": true }],
|
|
92
|
+
"otherwiseValue": false, "rolloutPct": 100 }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Prove it
|
|
96
|
+
|
|
97
|
+
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
|
+
2. `list_feature_flags` / `bridge flag list` shows no warnings on the flag.
|
|
99
|
+
3. In the app, as a test user on each side of the rule.
|
|
100
|
+
|
|
101
|
+
## Where this connects
|
|
102
|
+
|
|
103
|
+
- **Payments:** plan keys, and plan features versus flags.
|
|
104
|
+
- **Roles:** role keys for `user.role`, and why a flag is not a permission.
|
|
105
|
+
- **Going live:** flags are per app; a production app needs its own (`export_feature_flags`, then `import_feature_flags`).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Going live — decision guide
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
> Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). How the pieces work underneath: `bridge guide mechanisms`.
|
|
6
|
+
|
|
7
|
+
## Start from the checklist
|
|
8
|
+
|
|
9
|
+
`get_setup_status` / `bridge setup status` lists every item (redirect URIs, default callback, allowed origins, UI URL, sign-in methods and provider credentials, email sender, Stripe, plans, first real sign-in) as configured, missing or needing attention, each with its next step. Its `productionNeeds` is what must still change before real users arrive: localhost addresses, test Stripe keys, an unconfirmed sender. Work from that list, not from memory.
|
|
10
|
+
|
|
11
|
+
`list_apps` shows which apps this connection can reach. `diagnose_integration` / `bridge diagnose` compares the project's env files with the app they point at.
|
|
12
|
+
|
|
13
|
+
## Ask the developer
|
|
14
|
+
|
|
15
|
+
Product questions. Ask, wait, never guess.
|
|
16
|
+
|
|
17
|
+
1. **Is the app you have been building with already serving real users?** If yes, it is the production app. If no, see "a separate production app" below.
|
|
18
|
+
2. **What is the production address of the frontend** (and of the backend, if it calls Bridge)?
|
|
19
|
+
3. **For each social sign-in provider: the production client id and secret,** if they use a different provider app in production.
|
|
20
|
+
4. **The live Stripe keys** (`sk_live_…`, `pk_live_…`), from the Stripe account that should receive the money. Never invent, guess or reuse a key.
|
|
21
|
+
5. **Which address and name should emails come from** in production?
|
|
22
|
+
|
|
23
|
+
## Decide yourself
|
|
24
|
+
|
|
25
|
+
- **A separate Bridge app for production** when the current one has no real users. Reason: test users, test Stripe keys and localhost addresses stay out of production, and nothing in development can touch a real customer. Create it with `create_app`; it does not become the default, so pass its id as `app` or call `use_app`. Copy flags across with `export_feature_flags` then `import_feature_flags`, and recreate each plan with `apply_plan`.
|
|
26
|
+
- **Production env files carry the production app id and no base URL.** Reason: an unset base URL means Bridge's production API; a leftover stage or local base URL sends a production app id to the wrong place, and sign-in fails.
|
|
27
|
+
- **Add production addresses; remove localhost ones only from the production app.** Reason: removing them from the development app breaks local work.
|
|
28
|
+
- **Add redirect URIs one at a time and send allowed origins as a full list** (read first). Reason: the first keeps the list; the second replaces it.
|
|
29
|
+
- **Register the production callback in every social provider's console** (`setup_sso` returns it). Reason: providers refuse callbacks they do not know.
|
|
30
|
+
- **Never put a secret key in a browser variable** (`VITE_*`, `NEXT_PUBLIC_*`). Reason: everything in those ships to every visitor. `diagnose_integration` flags it.
|
|
31
|
+
- **A test user on a production app needs the developer's say-so.** Reason: it creates a workspace in the app real users share; the tool refuses without confirmation.
|
|
32
|
+
|
|
33
|
+
## Do it
|
|
34
|
+
|
|
35
|
+
| Step | MCP | CLI |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| Readiness checklist | `get_setup_status` | `bridge setup status` |
|
|
38
|
+
| Check a project's env files | `diagnose_integration` | `bridge diagnose` |
|
|
39
|
+
| Create a production app | `create_app`, then `use_app` | — (create it in the dashboard, then `bridge auth login --app <id>`) |
|
|
40
|
+
| Production URL, default callback, allowed origins | `update_app` | `bridge app update --ui-url … --default-callback-uri … --allowed-origins …` |
|
|
41
|
+
| Production callback URL | `add_redirect_uri` | `bridge app redirect-uris add <url>` |
|
|
42
|
+
| Live Stripe keys | `setup_payments` | `bridge setup payments --stripe-key sk_live_… --stripe-public-key pk_live_…` |
|
|
43
|
+
| Email sender | `setup_communication` | `bridge setup communication --from-address … --from-name …` |
|
|
44
|
+
| Copy flags | `export_feature_flags`, `import_feature_flags` | `bridge flag export --out flags.json`, `bridge flag import flags.json` |
|
|
45
|
+
| Copy a plan | `apply_plan` | `bridge plan apply --spec @plan.json` |
|
|
46
|
+
|
|
47
|
+
## Prove it
|
|
48
|
+
|
|
49
|
+
1. `get_setup_status` / `bridge setup status` again: show the developer what is left. Nothing in `productionNeeds` should be a surprise to them.
|
|
50
|
+
2. With the developer's agreement, `create_test_user` then `verify_login` (`bridge test-user create --confirm "<app name>"`, then `bridge test-user verify`) against the production app: a real sign-in from the production origin.
|
|
51
|
+
3. `get_stripe_status` / `bridge stripe status`: live keys, webhook working.
|
|
52
|
+
4. The first real sign-in appears on the checklist once someone signs up.
|
|
53
|
+
|
|
54
|
+
## Where this connects
|
|
55
|
+
|
|
56
|
+
- **Login:** redirect URIs, origins and provider credentials.
|
|
57
|
+
- **Payments:** live keys, and plans on the production app.
|
|
58
|
+
- **Feature control:** flags are per app.
|
|
59
|
+
- **Look and feel:** a confirmed sender address.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Login — decision guide
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
> Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). How the pieces work underneath: `bridge guide mechanisms`.
|
|
6
|
+
|
|
7
|
+
## Start from what is there
|
|
8
|
+
|
|
9
|
+
Read before you ask anything, so your questions are about changes, not about the current state.
|
|
10
|
+
|
|
11
|
+
| Read | MCP | CLI |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Sign-in methods and token lifetimes | `get_auth_config` | `bridge auth config` |
|
|
14
|
+
| URLs, redirect URIs, allowed origins | `get_app` | `bridge app get` |
|
|
15
|
+
| The exact env variables for this project | `get_environment_info` | — |
|
|
16
|
+
|
|
17
|
+
A new app has password, magic link and passkeys on, two-factor off, and no social providers.
|
|
18
|
+
|
|
19
|
+
## Ask the developer
|
|
20
|
+
|
|
21
|
+
Product questions. Ask, wait, never guess.
|
|
22
|
+
|
|
23
|
+
1. **Which ways should people be able to sign in?** Password is always there. Offer magic link, passkeys, Google, GitHub, Microsoft (Azure AD), LinkedIn, Facebook. Company SSO (SAML or OIDC) is set up in the Bridge dashboard, not from here.
|
|
24
|
+
2. **Should two-factor sign-in be available?**
|
|
25
|
+
3. **Should sign-in happen on Bridge's hosted page, or inside the app on its own address?** Both use the same one route file; the look-and-feel guide covers what each looks like.
|
|
26
|
+
4. **Can anyone sign up, or only people who are invited?** This is a teams question: see the **teams** guide before changing it.
|
|
27
|
+
5. **For each social provider they pick: the client id and client secret** from that provider's console. Never invent, guess or reuse a credential.
|
|
28
|
+
|
|
29
|
+
## Decide yourself
|
|
30
|
+
|
|
31
|
+
Apply these without asking. Say the reason in one line.
|
|
32
|
+
|
|
33
|
+
- **Callback URL is `<frontend-url>/auth/oauth-callback`,** registered as a redirect URI and as the default callback. Reason: the sign-in route file serves that address, and Bridge refuses any callback it has not seen.
|
|
34
|
+
- **Add redirect URIs one at a time** with `add_redirect_uri` / `bridge app redirect-uris add`. Reason: they keep the rest of the list; nothing is lost.
|
|
35
|
+
- **Before changing allowed origins, read the current list and pass it back with the new origin added.** Reason: `update_app` and `bridge app update --allowed-origins` replace the whole list.
|
|
36
|
+
- **Set the UI URL to the frontend's address.** Reason: links in invitation and password-reset emails are built from it; unset, people land on Bridge's hosted pages.
|
|
37
|
+
- **Leave token lifetimes at their defaults** unless the developer raises it. Reason: the defaults are safe. There are no password-complexity rules to set; if asked, say so.
|
|
38
|
+
- **Write no sign-in pages by hand, and no token fetch helper.** Reason: the plugin serves every sign-in page (login, signup, callback, set password, forgot password, magic link, passkey setup, workspace selection) from one file.
|
|
39
|
+
- **Protect every route by default; make only `/auth/*` public.** Reason: a new page is private until someone decides otherwise.
|
|
40
|
+
|
|
41
|
+
## Do it
|
|
42
|
+
|
|
43
|
+
| Step | MCP | CLI |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| Turn magic link, passkeys or two-factor on or off | `update_auth_methods` | `bridge app update --magic-link-enabled true` (also `--passkeys-enabled`, `--mfa-enabled`) |
|
|
46
|
+
| Add a social provider with its credentials | `setup_sso` | `bridge setup sso --provider google --client-id … --client-secret …` |
|
|
47
|
+
| Register the callback URL | `add_redirect_uri` | `bridge app redirect-uris add <url>` |
|
|
48
|
+
| Set UI URL, default callback, allowed origins | `update_app` | `bridge app update --ui-url … --default-callback-uri … --allowed-origins …` |
|
|
49
|
+
| Change token lifetimes (seconds) | `update_password_policy` | `bridge auth password-policy --access-token-ttl 900` |
|
|
50
|
+
| Code for the framework | `get_integration_guide` (topic `auth`) | `bridge guide svelte` (hosted) or `bridge guide svelte sdk-auth` (in-app) |
|
|
51
|
+
|
|
52
|
+
`setup_sso` returns a callback URL. The developer must paste it into the provider's console, or that provider's sign-in fails with a redirect mismatch. Always show it to them with that instruction. `update_auth_methods` can switch a provider on but never saves its credentials.
|
|
53
|
+
|
|
54
|
+
## Prove it
|
|
55
|
+
|
|
56
|
+
Do not say "it should work". Show it working.
|
|
57
|
+
|
|
58
|
+
1. `diagnose_integration` / `bridge diagnose`: compares the project's env files with the real app and names every difference with its fix.
|
|
59
|
+
2. `create_test_user`, then `verify_login` / `bridge test-user create`, then `bridge test-user verify`: a real sign-in, with the token checked.
|
|
60
|
+
3. If sign-in works there but not in the app, the app code is the difference.
|
|
61
|
+
4. When a real person cannot sign in: `debug_auth_issue` / `bridge event auth-attempts --user <email>` gives each failed attempt with its reason and fix.
|
|
62
|
+
|
|
63
|
+
## Where this connects
|
|
64
|
+
|
|
65
|
+
- **Teams:** who can sign up, and whether they create a workspace or join one.
|
|
66
|
+
- **Roles:** what a person can do once signed in.
|
|
67
|
+
- **Look and feel:** hosted or in-app sign-in, and how the pages look.
|
|
68
|
+
- **Going live:** production URLs and redirect URIs replace the localhost ones.
|