@tribe-nest/forge 3.19.0 → 3.21.0
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/package.json
CHANGED
|
@@ -69,13 +69,26 @@ export function useUpdateAccount() {
|
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Change the member's password.
|
|
74
|
+
*
|
|
75
|
+
* Tenant-scoped (`/public/sessions/password`), because that is where a fan's
|
|
76
|
+
* credential lives — see the note at the top of `useAuthActions.ts`. The global
|
|
77
|
+
* `/public/accounts/password` this used to call wrote a hash that login never
|
|
78
|
+
* reads: it returned 200, invalidated the session, and left the old password
|
|
79
|
+
* working.
|
|
80
|
+
*
|
|
81
|
+
* Requires an association-scoped session (the endpoint is behind
|
|
82
|
+
* `requireAssociation`). Every current login path mints one; a member still
|
|
83
|
+
* holding a pre-cutover account-scoped token gets a 401 and needs to sign in
|
|
84
|
+
* again — honest, where the old call was silently a no-op.
|
|
85
|
+
*/
|
|
73
86
|
export function useChangePassword() {
|
|
74
87
|
const { client } = useForge();
|
|
75
88
|
|
|
76
89
|
return useMutation<unknown, unknown, { currentPassword: string; newPassword: string }>({
|
|
77
90
|
mutationFn: async (body) => {
|
|
78
|
-
const res = await client.put("/public/
|
|
91
|
+
const res = await client.put("/public/sessions/password", body);
|
|
79
92
|
return res.data;
|
|
80
93
|
},
|
|
81
94
|
});
|
|
@@ -2,26 +2,70 @@ import { useForge } from "../../provider/ForgeProvider";
|
|
|
2
2
|
import { useMutation } from "@tanstack/react-query";
|
|
3
3
|
|
|
4
4
|
// Stateless account actions (no session state) — kept out of PublicAuthContext.
|
|
5
|
+
//
|
|
6
|
+
// ## Why these hit `/public/sessions/*` and not `/public/accounts/*`
|
|
7
|
+
//
|
|
8
|
+
// A member's credential for a tenant lives on their `account_associations` row,
|
|
9
|
+
// not on `accounts` — that is the hash `POST /public/sessions` checks at login
|
|
10
|
+
// (see `resolveAssociationLogin` on the backend). `accounts.password` is the
|
|
11
|
+
// CREATOR/admin-dashboard credential; for a fan it is at most a lazy-heal
|
|
12
|
+
// fallback, gated on the account having some other relationship to the profile.
|
|
13
|
+
//
|
|
14
|
+
// These three calls used to point at the global `/public/accounts/*` endpoints,
|
|
15
|
+
// which write `accounts.password`. Every one of them returned 200 and changed
|
|
16
|
+
// nothing that login reads:
|
|
17
|
+
//
|
|
18
|
+
// - "Change password" logged the member out (a credential change invalidates
|
|
19
|
+
// sessions), then refused the new password and kept accepting the old one.
|
|
20
|
+
// - "Forgot password" was worse: it reset a hash login never consults, so the
|
|
21
|
+
// member — who by definition no longer knows the old password — was locked
|
|
22
|
+
// out with a reset link that appeared to work.
|
|
23
|
+
//
|
|
24
|
+
// The tenant-scoped endpoints below are the ones the credential actually lives
|
|
25
|
+
// behind, and they invalidate only this context's sessions.
|
|
5
26
|
|
|
6
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Request a password-reset email for this tenant.
|
|
29
|
+
*
|
|
30
|
+
* `profileId` comes from the provider (one deploy = one tenant); pass it
|
|
31
|
+
* explicitly only when rendering outside a pinned context.
|
|
32
|
+
*/
|
|
7
33
|
export function useForgotPassword() {
|
|
8
|
-
const { client } = useForge();
|
|
34
|
+
const { client, profileId } = useForge();
|
|
9
35
|
|
|
10
|
-
return useMutation<unknown, unknown, { email: string; origin?: string }>({
|
|
11
|
-
mutationFn: async ({ email, origin }) => {
|
|
12
|
-
const
|
|
36
|
+
return useMutation<unknown, unknown, { email: string; origin?: string; profileId?: string }>({
|
|
37
|
+
mutationFn: async ({ email, origin, profileId: overrideProfileId }) => {
|
|
38
|
+
const contextId = overrideProfileId ?? profileId;
|
|
39
|
+
if (!contextId) {
|
|
40
|
+
// Better than posting without it: the endpoint would 400 on schema
|
|
41
|
+
// validation and the form would show "something went wrong".
|
|
42
|
+
throw new Error("useForgotPassword: no profileId — pass one or render inside a ForgeProvider with profileId");
|
|
43
|
+
}
|
|
44
|
+
const res = await client.post("/public/sessions/forgot-password", {
|
|
45
|
+
profileId: contextId,
|
|
46
|
+
email,
|
|
47
|
+
// The reset link is built server-side from this, and the association
|
|
48
|
+
// endpoint requires it (the global one treated it as optional).
|
|
49
|
+
origin: origin ?? (typeof window !== "undefined" ? window.location.origin : undefined),
|
|
50
|
+
});
|
|
13
51
|
return res.data;
|
|
14
52
|
},
|
|
15
53
|
});
|
|
16
54
|
}
|
|
17
55
|
|
|
18
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Complete a password reset with the emailed token.
|
|
58
|
+
*
|
|
59
|
+
* The token is looked up on the association row, so it must be redeemed against
|
|
60
|
+
* the same surface that issued it — a token minted by the hook above will not
|
|
61
|
+
* resolve on `/public/accounts/reset-password`, and vice versa.
|
|
62
|
+
*/
|
|
19
63
|
export function useResetPassword() {
|
|
20
64
|
const { client } = useForge();
|
|
21
65
|
|
|
22
66
|
return useMutation<unknown, unknown, { token: string; password: string }>({
|
|
23
67
|
mutationFn: async ({ token, password }) => {
|
|
24
|
-
const res = await client.post("/public/
|
|
68
|
+
const res = await client.post("/public/sessions/reset-password", { token, password });
|
|
25
69
|
return res.data;
|
|
26
70
|
},
|
|
27
71
|
});
|
|
@@ -5,11 +5,12 @@ import { ForgeAnalytics } from "../analytics/ForgeAnalytics";
|
|
|
5
5
|
import { PwaRegistration } from "../styled/PwaRegistration";
|
|
6
6
|
import { InstallBanner } from "../styled/InstallBanner";
|
|
7
7
|
import { CookieConsent } from "../styled/CookieConsent";
|
|
8
|
+
import { AiAgentWidget } from "../styled/AiAgentWidget";
|
|
8
9
|
import { useThemeTokens } from "../theme/ForgeThemeProvider";
|
|
9
10
|
import { useInitialSiteConfig } from "../../provider/SiteConfigProvider";
|
|
10
11
|
import { PoweredBy } from "./PoweredBy";
|
|
11
12
|
import { PreviewDiagnostics } from "./PreviewDiagnostics";
|
|
12
|
-
import { shellPoweredByEnabled, shellPwaEnabled } from "./shellGating";
|
|
13
|
+
import { shellAiAgentEnabled, shellPoweredByEnabled, shellPwaEnabled } from "./shellGating";
|
|
13
14
|
import { previewDiagnosticsEnabled, resolvePreviewDiagnostics } from "./diagnosticsGating";
|
|
14
15
|
import type { ForgeSsrDiagnostics } from "../../types/diagnostics";
|
|
15
16
|
import { captureAttributionRefFromUrl, readAttributionRef } from "../../utils/attribution";
|
|
@@ -52,6 +53,14 @@ export interface TribeNestAppProps extends Omit<ForgeProviderProps, "children">
|
|
|
52
53
|
analytics?: boolean;
|
|
53
54
|
pwa?: boolean;
|
|
54
55
|
cookieConsent?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* The website AI agent bubble. Defaults to on for a fan site and off for a
|
|
58
|
+
* mini-app; set it explicitly to override either default. Renders nothing
|
|
59
|
+
* unless the creator has also enabled the agent for the profile — the widget
|
|
60
|
+
* fetches its own config and self-hides. For custom chat UI, leave this off
|
|
61
|
+
* and build on the `useAiAgent()` hook instead.
|
|
62
|
+
*/
|
|
63
|
+
aiAgent?: boolean;
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
// Ensure the PWA `<link rel="manifest">` + a `theme-color` meta exist even if the
|
|
@@ -114,6 +123,7 @@ export function TribeNestApp({
|
|
|
114
123
|
analytics = true,
|
|
115
124
|
pwa = true,
|
|
116
125
|
cookieConsent = true,
|
|
126
|
+
aiAgent,
|
|
117
127
|
...forgeProps
|
|
118
128
|
}: TribeNestAppProps) {
|
|
119
129
|
// Register the SW + offer install only on the live published site — never in
|
|
@@ -154,6 +164,11 @@ export function TribeNestApp({
|
|
|
154
164
|
{/* Consent banner — shown on first visit, re-openable from anywhere via
|
|
155
165
|
useCookieConsent().reopen(). Hidden in the editor. */}
|
|
156
166
|
{cookieConsent && !editable && <CookieConsent useTribeNestPrivacy />}
|
|
167
|
+
{/* Website AI agent — floating bubble, self-hiding when the profile has no
|
|
168
|
+
agent enabled. Lives here rather than in each __root so enabling the
|
|
169
|
+
agent in admin is enough to make it appear; before this it shipped only
|
|
170
|
+
on the Craft stack and no code site ever rendered it. */}
|
|
171
|
+
{shellAiAgentEnabled({ aiAgent, editable, appId: forgeProps.appId }) && <AiAgentWidget />}
|
|
157
172
|
</ForgeProvider>
|
|
158
173
|
);
|
|
159
174
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { poweredByHref, shellPoweredByEnabled, shellPwaEnabled } from "./shellGating";
|
|
2
|
+
import { poweredByHref, shellAiAgentEnabled, shellPoweredByEnabled, shellPwaEnabled } from "./shellGating";
|
|
3
3
|
|
|
4
4
|
// The PWA (SW register + install prompt) must be ON only for the live published
|
|
5
5
|
// site, and OFF everywhere else (editor, preview/draft), so review Workers never
|
|
@@ -47,6 +47,31 @@ describe("shellPoweredByEnabled", () => {
|
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
// The agent bubble defaults ON for fan sites and OFF for mini-apps, and is
|
|
51
|
+
// blocked in the editor because an answer there spends the creator's AI credits.
|
|
52
|
+
// The widget itself still renders nothing when the agent is disabled for the
|
|
53
|
+
// profile — this gate is about the SURFACE, not the entitlement.
|
|
54
|
+
describe("shellAiAgentEnabled", () => {
|
|
55
|
+
it("is on by default for a fan site, live or preview", () => {
|
|
56
|
+
expect(shellAiAgentEnabled({ editable: false })).toBe(true);
|
|
57
|
+
expect(shellAiAgentEnabled({})).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("is off by default for a mini-app", () => {
|
|
61
|
+
expect(shellAiAgentEnabled({ editable: false, appId: "app-7" })).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("is off in the editor even when explicitly asked for", () => {
|
|
65
|
+
expect(shellAiAgentEnabled({ aiAgent: true, editable: true })).toBe(false);
|
|
66
|
+
expect(shellAiAgentEnabled({ editable: true })).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("lets an explicit prop override the per-surface default in both directions", () => {
|
|
70
|
+
expect(shellAiAgentEnabled({ aiAgent: false, editable: false })).toBe(false); // site opts out
|
|
71
|
+
expect(shellAiAgentEnabled({ aiAgent: true, editable: false, appId: "app-7" })).toBe(true); // app opts in
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
50
75
|
// The badge is only worth carrying if we can tell WHICH site sent the visitor,
|
|
51
76
|
// so the identifying params are the point of the link, not decoration.
|
|
52
77
|
describe("poweredByHref", () => {
|
|
@@ -24,6 +24,22 @@ export function shellPoweredByEnabled(opts: {
|
|
|
24
24
|
return !opts.hideBadge && !opts.editable && opts.state === "published";
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// The website AI agent bubble. Unlike the PWA it IS wanted on preview/draft
|
|
28
|
+
// deploys — that is where a creator checks the assistant before going live — so
|
|
29
|
+
// the only hard block is the HMR editor, where a live answer would spend the
|
|
30
|
+
// creator's AI credits every time they typed into their own site.
|
|
31
|
+
//
|
|
32
|
+
// The default differs by surface. A fan site gets it (the agent answers from the
|
|
33
|
+
// artist's content + knowledge base, which is what a visitor is there for); a
|
|
34
|
+
// mini-app does NOT, because an app is a utility and a floating "ask me about
|
|
35
|
+
// the artist" bubble is noise on top of it. `appId` is baked at build time, so
|
|
36
|
+
// this is decided without a runtime lookup. An explicit `aiAgent` prop wins
|
|
37
|
+
// either way — that is how an app opts in, or a site opts out.
|
|
38
|
+
export function shellAiAgentEnabled(opts: { aiAgent?: boolean; editable?: boolean; appId?: string }): boolean {
|
|
39
|
+
if (opts.editable) return false;
|
|
40
|
+
return opts.aiAgent ?? !opts.appId;
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
/** Marketing site the badge points at. */
|
|
28
44
|
const TRIBENEST_URL = "https://tribenest.co/";
|
|
29
45
|
|