@carlonicora/nextjs-jsonapi 3.23.0 → 3.24.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.
Files changed (93) hide show
  1. package/dist/{AssistantMessageInterface-CuaMDs2i.d.ts → AssistantMessageInterface-1Kbz8RVs.d.ts} +34 -1
  2. package/dist/{AssistantMessageInterface-Du3ShThp.d.mts → AssistantMessageInterface-CoxJyuFM.d.mts} +34 -1
  3. package/dist/{BlockNoteEditor-BUSOUPQ7.js → BlockNoteEditor-PTD2V4CH.js} +19 -19
  4. package/dist/{BlockNoteEditor-BUSOUPQ7.js.map → BlockNoteEditor-PTD2V4CH.js.map} +1 -1
  5. package/dist/{BlockNoteEditor-W42IQTGL.mjs → BlockNoteEditor-Q2WEDMKJ.mjs} +4 -4
  6. package/dist/{BlockNoteViewer-FQ23TZ3J.mjs → BlockNoteViewer-WO6GTDSI.mjs} +2 -2
  7. package/dist/{BlockNoteViewer-MCX53AR4.js → BlockNoteViewer-WOXEJ6RM.js} +3 -3
  8. package/dist/{BlockNoteViewer-MCX53AR4.js.map → BlockNoteViewer-WOXEJ6RM.js.map} +1 -1
  9. package/dist/billing/index.js +401 -401
  10. package/dist/billing/index.mjs +3 -3
  11. package/dist/{chunk-O5MKEU7G.mjs → chunk-2O3SFHQ7.mjs} +36 -38
  12. package/dist/{chunk-O5MKEU7G.mjs.map → chunk-2O3SFHQ7.mjs.map} +1 -1
  13. package/dist/{chunk-FXWO5J2R.js → chunk-3D2MCVRZ.js} +7 -7
  14. package/dist/{chunk-FXWO5J2R.js.map → chunk-3D2MCVRZ.js.map} +1 -1
  15. package/dist/{chunk-VR65NRE4.js → chunk-3YJF7QY4.js} +1338 -1115
  16. package/dist/chunk-3YJF7QY4.js.map +1 -0
  17. package/dist/{chunk-GFLEFIAW.mjs → chunk-GJHKOG5L.mjs} +1399 -1176
  18. package/dist/chunk-GJHKOG5L.mjs.map +1 -0
  19. package/dist/{chunk-JZUBFAAY.mjs → chunk-HAI7G6MD.mjs} +2 -2
  20. package/dist/{chunk-YCHPWXHD.js → chunk-IV4DTYXN.js} +36 -38
  21. package/dist/chunk-IV4DTYXN.js.map +1 -0
  22. package/dist/client/index.d.mts +13 -2
  23. package/dist/client/index.d.ts +13 -2
  24. package/dist/client/index.js +4 -4
  25. package/dist/client/index.mjs +3 -3
  26. package/dist/components/index.d.mts +87 -28
  27. package/dist/components/index.d.ts +87 -28
  28. package/dist/components/index.js +8 -4
  29. package/dist/components/index.js.map +1 -1
  30. package/dist/components/index.mjs +7 -3
  31. package/dist/contexts/index.d.mts +11 -2
  32. package/dist/contexts/index.d.ts +11 -2
  33. package/dist/contexts/index.js +4 -4
  34. package/dist/contexts/index.mjs +3 -3
  35. package/dist/core/index.d.mts +27 -5
  36. package/dist/core/index.d.ts +27 -5
  37. package/dist/core/index.js +2 -2
  38. package/dist/core/index.mjs +1 -1
  39. package/dist/features/help/index.js +37 -37
  40. package/dist/features/help/index.mjs +3 -3
  41. package/dist/features/tokenusage/index.js +101 -101
  42. package/dist/features/tokenusage/index.mjs +3 -3
  43. package/dist/index.d.mts +2 -3
  44. package/dist/index.d.ts +2 -3
  45. package/dist/index.js +3 -3
  46. package/dist/index.mjs +2 -2
  47. package/dist/{oauth.interface-CD90ycYz.d.ts → oauth.interface-BZKZAPRw.d.ts} +8 -1
  48. package/dist/{oauth.interface-DRE3NAKc.d.mts → oauth.interface-nVwnzxJw.d.mts} +8 -1
  49. package/dist/server/index.js +3 -3
  50. package/dist/server/index.mjs +1 -1
  51. package/package.json +1 -1
  52. package/src/components/forms/__tests__/EditorSheet.test.tsx +5 -3
  53. package/src/core/utils/__tests__/translateResponse.test.ts +3 -1
  54. package/src/features/assistant/components/containers/AssistantContainer.tsx +105 -36
  55. package/src/features/assistant/components/containers/__tests__/AssistantContainer.spec.tsx +163 -1
  56. package/src/features/assistant/components/parts/AssistantComposer.tsx +38 -15
  57. package/src/features/assistant/components/parts/AssistantEmptyState.tsx +5 -3
  58. package/src/features/assistant/components/parts/AssistantSidebar.tsx +26 -7
  59. package/src/features/assistant/components/parts/__tests__/AssistantComposer.spec.tsx +8 -0
  60. package/src/features/assistant/components/parts/__tests__/AssistantSidebar.spec.tsx +7 -0
  61. package/src/features/assistant/contexts/AssistantContext.tsx +91 -7
  62. package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +196 -0
  63. package/src/features/assistant-action/components/AssistantContainerWithApprovals.tsx +23 -1
  64. package/src/features/assistant-message/components/EntityMentionLink.tsx +28 -0
  65. package/src/features/assistant-message/components/__tests__/EntityMentionLink.spec.tsx +58 -0
  66. package/src/features/assistant-message/components/index.ts +1 -0
  67. package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +1 -1
  68. package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +2 -2
  69. package/src/features/auth/components/forms/__tests__/Logout.spec.tsx +3 -2
  70. package/src/features/oauth/components/OAuthClientForm.tsx +21 -4
  71. package/src/features/oauth/components/__tests__/OAuthClientForm.spec.tsx +63 -0
  72. package/src/features/oauth/components/consent/OAuthCompanySelector.tsx +54 -0
  73. package/src/features/oauth/components/consent/OAuthConsentActions.tsx +5 -2
  74. package/src/features/oauth/components/consent/OAuthConsentHeader.tsx +10 -6
  75. package/src/features/oauth/components/consent/OAuthConsentScreen.tsx +33 -13
  76. package/src/features/oauth/components/consent/OAuthScopeList.tsx +4 -1
  77. package/src/features/oauth/components/consent/__tests__/OAuthConsentHeader.spec.tsx +25 -0
  78. package/src/features/oauth/components/consent/__tests__/OAuthConsentScreen.spec.tsx +126 -0
  79. package/src/features/oauth/components/consent/index.ts +1 -0
  80. package/src/features/oauth/data/__tests__/oauth.service.spec.ts +103 -0
  81. package/src/features/oauth/data/oauth.service.ts +3 -13
  82. package/src/features/oauth/data/oauth.ts +30 -1
  83. package/src/features/oauth/hooks/useOAuthConsent.ts +34 -3
  84. package/src/features/oauth/interfaces/oauth.interface.ts +11 -25
  85. package/src/hooks/__tests__/useDataListRetriever.test.ts +1 -1
  86. package/dist/AssistantInterface-RvZzlcA_.d.ts +0 -36
  87. package/dist/AssistantInterface-jL56dyNc.d.mts +0 -36
  88. package/dist/chunk-GFLEFIAW.mjs.map +0 -1
  89. package/dist/chunk-VR65NRE4.js.map +0 -1
  90. package/dist/chunk-YCHPWXHD.js.map +0 -1
  91. /package/dist/{BlockNoteEditor-W42IQTGL.mjs.map → BlockNoteEditor-Q2WEDMKJ.mjs.map} +0 -0
  92. /package/dist/{BlockNoteViewer-FQ23TZ3J.mjs.map → BlockNoteViewer-WO6GTDSI.mjs.map} +0 -0
  93. /package/dist/{chunk-JZUBFAAY.mjs.map → chunk-HAI7G6MD.mjs.map} +0 -0
@@ -0,0 +1,126 @@
1
+ import { render, renderHook, screen, waitFor } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import type { UseOAuthConsentReturn } from "../../../hooks/useOAuthConsent";
4
+ import { OAuthConsentScreen } from "../OAuthConsentScreen";
5
+
6
+ const { hookState, getAuthorizationInfo, approveAuthorization, currentUserContext } = vi.hoisted(() => ({
7
+ hookState: { value: null as UseOAuthConsentReturn | null },
8
+ getAuthorizationInfo: vi.fn(),
9
+ approveAuthorization: vi.fn(),
10
+ currentUserContext: vi.fn(),
11
+ }));
12
+
13
+ // The screen tests drive a controllable hook; the hook tests (hookState.value = null) run the real one.
14
+ vi.mock("../../../hooks/useOAuthConsent", async () => {
15
+ const actual = await vi.importActual<typeof import("../../../hooks/useOAuthConsent")>(
16
+ "../../../hooks/useOAuthConsent",
17
+ );
18
+ return {
19
+ ...actual,
20
+ useOAuthConsent: (
21
+ params: Parameters<typeof actual.useOAuthConsent>[0],
22
+ options?: Parameters<typeof actual.useOAuthConsent>[1],
23
+ ) => hookState.value ?? actual.useOAuthConsent(params, options),
24
+ };
25
+ });
26
+
27
+ vi.mock("../../../data/oauth.service", () => ({
28
+ OAuthService: {
29
+ getAuthorizationInfo: (params: unknown) => getAuthorizationInfo(params),
30
+ approveAuthorization: (params: unknown) => approveAuthorization(params),
31
+ denyAuthorization: vi.fn(),
32
+ },
33
+ }));
34
+
35
+ vi.mock("../../../../user/contexts/CurrentUserContext", () => ({
36
+ useCurrentUserContextOptional: () => currentUserContext(),
37
+ useCurrentUserContext: () => currentUserContext(),
38
+ }));
39
+
40
+ const PARAMS = { clientId: "client-1", redirectUri: "https://example.com/callback" };
41
+ const CLIENT = { id: "client-1", name: "Claude" } as any;
42
+ const C1 = { id: "c1", name: "A" };
43
+ const C2 = { id: "c2", name: "B" };
44
+
45
+ function makeHook(overrides: Partial<UseOAuthConsentReturn> = {}): UseOAuthConsentReturn {
46
+ return {
47
+ clientInfo: { client: CLIENT, scopes: [], companies: [C1] },
48
+ isLoading: false,
49
+ error: null,
50
+ approve: vi.fn(),
51
+ deny: vi.fn(),
52
+ isSubmitting: false,
53
+ companyId: "c1",
54
+ setCompanyId: vi.fn(),
55
+ ...overrides,
56
+ };
57
+ }
58
+
59
+ beforeEach(() => {
60
+ vi.clearAllMocks();
61
+ hookState.value = null;
62
+ currentUserContext.mockReturnValue({ company: { id: "c2" } });
63
+ });
64
+
65
+ describe("OAuthConsentScreen", () => {
66
+ it("renders no studio picker for a single company", () => {
67
+ hookState.value = makeHook();
68
+ render(<OAuthConsentScreen params={PARAMS} appName="Test" />);
69
+ expect(screen.queryByLabelText("oauth.consent.company_label")).toBeNull();
70
+ });
71
+
72
+ it("renders the studio picker for two companies", () => {
73
+ hookState.value = makeHook({
74
+ clientInfo: { client: CLIENT, scopes: [], companies: [C1, C2] },
75
+ companyId: "c2",
76
+ });
77
+ render(<OAuthConsentScreen params={PARAMS} appName="Test" />);
78
+ expect(screen.getByLabelText("oauth.consent.company_label")).toBeInTheDocument();
79
+ expect(screen.getByRole("heading", { name: "oauth.consent.company_label" })).toBeInTheDocument();
80
+ });
81
+
82
+ it("renders translated keys, not hardcoded English", () => {
83
+ hookState.value = makeHook();
84
+ render(<OAuthConsentScreen params={PARAMS} appName="Test" />);
85
+ expect(screen.getByText("oauth.consent.authorize")).toBeInTheDocument();
86
+ expect(screen.queryByText("Authorize")).toBeNull();
87
+ });
88
+ });
89
+
90
+ describe("useOAuthConsent", () => {
91
+ const renderConsentHook = async (options?: { defaultCompanyId?: string }) => {
92
+ const { useOAuthConsent } = await import("../../../hooks/useOAuthConsent");
93
+ return renderHook(() => useOAuthConsent(PARAMS, options));
94
+ };
95
+
96
+ it("defaultCompanyId from the host app wins over the package user context", async () => {
97
+ currentUserContext.mockReturnValue(undefined);
98
+ getAuthorizationInfo.mockResolvedValue({ client: CLIENT, scopes: [], companies: [C1, C2] });
99
+ const { result } = await renderConsentHook({ defaultCompanyId: "c2" });
100
+ await waitFor(() => expect(result.current.companyId).toBe("c2"));
101
+ });
102
+
103
+ it("defaults companyId to the current web-session company when it is in the list", async () => {
104
+ getAuthorizationInfo.mockResolvedValue({ client: CLIENT, scopes: [], companies: [C1, C2] });
105
+ const { result } = await renderConsentHook();
106
+ await waitFor(() => expect(result.current.companyId).toBe("c2"));
107
+ });
108
+
109
+ it("defaults to the first company when the current one is not in the list", async () => {
110
+ currentUserContext.mockReturnValue({ company: { id: "c9" } });
111
+ getAuthorizationInfo.mockResolvedValue({ client: CLIENT, scopes: [], companies: [C1, C2] });
112
+ const { result } = await renderConsentHook();
113
+ await waitFor(() => expect(result.current.companyId).toBe("c1"));
114
+ });
115
+
116
+ it("approve sends the selected company", async () => {
117
+ getAuthorizationInfo.mockResolvedValue({ client: CLIENT, scopes: [], companies: [C1, C2] });
118
+ approveAuthorization.mockResolvedValue({ redirectUrl: "" });
119
+ const { result } = await renderConsentHook();
120
+ await waitFor(() => expect(result.current.companyId).toBe("c2"));
121
+
122
+ await result.current.approve();
123
+
124
+ expect(approveAuthorization).toHaveBeenCalledWith(expect.objectContaining({ companyId: "c2" }));
125
+ });
126
+ });
@@ -2,3 +2,4 @@ export * from "./OAuthConsentHeader";
2
2
  export * from "./OAuthScopeList";
3
3
  export * from "./OAuthConsentActions";
4
4
  export * from "./OAuthConsentScreen";
5
+ export * from "./OAuthCompanySelector";
@@ -0,0 +1,103 @@
1
+ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import { AbstractService } from "../../../../core/abstracts/AbstractService";
4
+ import { ApiRequestDataTypeInterface } from "../../../../core/interfaces/ApiRequestDataTypeInterface";
5
+ import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
6
+ import { OAuthConsentRequest } from "../../interfaces/oauth.interface";
7
+ import { OAuthClient } from "../oauth";
8
+ import { OAuthService } from "../oauth.service";
9
+
10
+ // ModuleRegistry is backed by a globalThis symbol shared across test files in a
11
+ // worker, hence the register-if-absent guard (same idiom as HandbookThreadService.spec.ts).
12
+ const registerIfAbsent = (key: string, module: ApiRequestDataTypeInterface) => {
13
+ try {
14
+ ModuleRegistry.get(key as any);
15
+ } catch {
16
+ ModuleRegistry.register(key, module);
17
+ }
18
+ };
19
+
20
+ beforeAll(() => {
21
+ registerIfAbsent("OAuth", { name: "oauth-clients", model: OAuthClient } as any);
22
+ });
23
+
24
+ type CallApiParams = { method: string; endpoint: string; input?: unknown; overridesJsonApiCreation?: boolean };
25
+ const callApiWithMeta = () => vi.spyOn(AbstractService as any, "callApiWithMeta");
26
+ const firstCall = (spy: ReturnType<typeof callApiWithMeta>): CallApiParams => spy.mock.calls[0][0] as CallApiParams;
27
+
28
+ const fullRequest: OAuthConsentRequest = {
29
+ clientId: "client-1",
30
+ redirectUri: "https://example.test/cb",
31
+ scope: "read mcp",
32
+ state: "xyz",
33
+ codeChallenge: "challenge",
34
+ codeChallengeMethod: "S256",
35
+ companyId: "11111111-1111-4111-8111-111111111111",
36
+ };
37
+
38
+ describe("OAuthClient authorization bodies (RFC 6749 wire format)", () => {
39
+ it("builds the approve body with snake_case keys in wire order", () => {
40
+ const body = new OAuthClient().createApproveAuthorizationJsonApi(fullRequest);
41
+
42
+ expect(JSON.stringify(body)).toBe(
43
+ JSON.stringify({
44
+ client_id: "client-1",
45
+ redirect_uri: "https://example.test/cb",
46
+ scope: "read mcp",
47
+ state: "xyz",
48
+ code_challenge: "challenge",
49
+ code_challenge_method: "S256",
50
+ company_id: "11111111-1111-4111-8111-111111111111",
51
+ }),
52
+ );
53
+ });
54
+
55
+ it("omits optional approve fields that were not supplied", () => {
56
+ const body = new OAuthClient().createApproveAuthorizationJsonApi({
57
+ clientId: "client-1",
58
+ redirectUri: "https://example.test/cb",
59
+ });
60
+
61
+ expect(JSON.stringify(body)).toBe('{"client_id":"client-1","redirect_uri":"https://example.test/cb"}');
62
+ });
63
+
64
+ it("builds the deny body with only client_id, redirect_uri and state", () => {
65
+ const body = new OAuthClient().createDenyAuthorizationJsonApi(fullRequest);
66
+
67
+ expect(JSON.stringify(body)).toBe(
68
+ '{"client_id":"client-1","redirect_uri":"https://example.test/cb","state":"xyz"}',
69
+ );
70
+ });
71
+ });
72
+
73
+ describe("OAuthService consent calls", () => {
74
+ beforeEach(() => vi.restoreAllMocks());
75
+
76
+ it("approveAuthorization sends the model's approve body and returns the redirect url", async () => {
77
+ const spy = callApiWithMeta().mockResolvedValue({ meta: { redirectUrl: "https://example.test/cb?code=1" } });
78
+
79
+ const result = await OAuthService.approveAuthorization(fullRequest);
80
+
81
+ const call = firstCall(spy);
82
+ expect(call.method).toBe("POST");
83
+ expect(call.endpoint).toContain("oauth/authorize/approve");
84
+ expect(call.overridesJsonApiCreation).toBe(true);
85
+ expect(call.input).toEqual(new OAuthClient().createApproveAuthorizationJsonApi(fullRequest));
86
+ expect(result).toEqual({ redirectUrl: "https://example.test/cb?code=1" });
87
+ });
88
+
89
+ it("denyAuthorization sends the model's deny body and returns the redirect url", async () => {
90
+ const spy = callApiWithMeta().mockResolvedValue({
91
+ meta: { redirectUrl: "https://example.test/cb?error=access_denied" },
92
+ });
93
+
94
+ const result = await OAuthService.denyAuthorization(fullRequest);
95
+
96
+ const call = firstCall(spy);
97
+ expect(call.method).toBe("POST");
98
+ expect(call.endpoint).toContain("oauth/authorize/deny");
99
+ expect(call.overridesJsonApiCreation).toBe(true);
100
+ expect(call.input).toEqual(new OAuthClient().createDenyAuthorizationJsonApi(fullRequest));
101
+ expect(result).toEqual({ redirectUrl: "https://example.test/cb?error=access_denied" });
102
+ });
103
+ });
@@ -7,6 +7,7 @@ import {
7
7
  OAuthConsentInfo,
8
8
  OAuthConsentRequest,
9
9
  } from "../interfaces/oauth.interface";
10
+ import { OAuthClient } from "./oauth";
10
11
 
11
12
  /**
12
13
  * Service for OAuth client management and authorization consent flow.
@@ -151,14 +152,7 @@ export class OAuthService extends AbstractService {
151
152
  type: Modules.OAuth,
152
153
  method: HttpMethod.POST,
153
154
  endpoint: new EndpointCreator({ endpoint: "oauth/authorize/approve" }).generate(),
154
- input: {
155
- client_id: params.clientId,
156
- redirect_uri: params.redirectUri,
157
- scope: params.scope,
158
- state: params.state,
159
- code_challenge: params.codeChallenge,
160
- code_challenge_method: params.codeChallengeMethod,
161
- },
155
+ input: new OAuthClient().createApproveAuthorizationJsonApi(params),
162
156
  overridesJsonApiCreation: true,
163
157
  });
164
158
 
@@ -176,11 +170,7 @@ export class OAuthService extends AbstractService {
176
170
  type: Modules.OAuth,
177
171
  method: HttpMethod.POST,
178
172
  endpoint: new EndpointCreator({ endpoint: "oauth/authorize/deny" }).generate(),
179
- input: {
180
- client_id: params.clientId,
181
- redirect_uri: params.redirectUri,
182
- state: params.state,
183
- },
173
+ input: new OAuthClient().createDenyAuthorizationJsonApi(params),
184
174
  overridesJsonApiCreation: true,
185
175
  });
186
176
 
@@ -1,5 +1,5 @@
1
1
  import { AbstractApiData, JsonApiHydratedDataInterface } from "../../../core";
2
- import { OAuthClientInput, OAuthClientInterface } from "../interfaces/oauth.interface";
2
+ import { OAuthClientInput, OAuthClientInterface, OAuthConsentRequest } from "../interfaces/oauth.interface";
3
3
 
4
4
  /**
5
5
  * OAuth client data model
@@ -84,4 +84,33 @@ export class OAuthClient extends AbstractApiData implements OAuthClientInterface
84
84
 
85
85
  return response;
86
86
  }
87
+
88
+ /**
89
+ * Body for POST /oauth/authorize/approve.
90
+ * The authorize endpoints speak the RFC 6749 wire format (snake_case keys,
91
+ * no JSON:API envelope), so this dedicated method owns that shape and the
92
+ * service sends it with `overridesJsonApiCreation`.
93
+ */
94
+ createApproveAuthorizationJsonApi(params: OAuthConsentRequest) {
95
+ return {
96
+ client_id: params.clientId,
97
+ redirect_uri: params.redirectUri,
98
+ scope: params.scope,
99
+ state: params.state,
100
+ code_challenge: params.codeChallenge,
101
+ code_challenge_method: params.codeChallengeMethod,
102
+ company_id: params.companyId,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Body for POST /oauth/authorize/deny (RFC 6749 wire format, see above).
108
+ */
109
+ createDenyAuthorizationJsonApi(params: OAuthConsentRequest) {
110
+ return {
111
+ client_id: params.clientId,
112
+ redirect_uri: params.redirectUri,
113
+ state: params.state,
114
+ };
115
+ }
87
116
  }
@@ -3,6 +3,7 @@
3
3
  import { useCallback, useEffect, useState } from "react";
4
4
  import { OAuthConsentInfo, OAuthConsentRequest } from "../interfaces/oauth.interface";
5
5
  import { OAuthService } from "../data/oauth.service";
6
+ import { useCurrentUserContextOptional } from "../../user/contexts/CurrentUserContext";
6
7
 
7
8
  export interface UseOAuthConsentReturn {
8
9
  /** Client and scope info for consent display */
@@ -17,6 +18,10 @@ export interface UseOAuthConsentReturn {
17
18
  deny: () => Promise<void>;
18
19
  /** Whether approve/deny is in progress */
19
20
  isSubmitting: boolean;
21
+ /** The studio (company) the authorization is granted for */
22
+ companyId: string | undefined;
23
+ /** Select the studio (company) the authorization is granted for */
24
+ setCompanyId: (id: string) => void;
20
25
  }
21
26
 
22
27
  /**
@@ -37,11 +42,35 @@ export interface UseOAuthConsentReturn {
37
42
  * // On button click: approve() or deny()
38
43
  * ```
39
44
  */
40
- export function useOAuthConsent(params: OAuthConsentRequest): UseOAuthConsentReturn {
45
+ export function useOAuthConsent(
46
+ params: OAuthConsentRequest,
47
+ options?: {
48
+ /**
49
+ * The studio open in the host app's session. Apps that keep the signed-in
50
+ * user in their own context (not the package UserProvider) pass it here;
51
+ * it wins over the package context.
52
+ */
53
+ defaultCompanyId?: string;
54
+ },
55
+ ): UseOAuthConsentReturn {
41
56
  const [clientInfo, setClientInfo] = useState<OAuthConsentInfo | null>(null);
42
57
  const [isLoading, setIsLoading] = useState(true);
43
58
  const [error, setError] = useState<Error | null>(null);
44
59
  const [isSubmitting, setIsSubmitting] = useState(false);
60
+ const [companyId, setCompanyId] = useState<string | undefined>(undefined);
61
+ const contextCompanyId = useCurrentUserContextOptional()?.company?.id;
62
+ const currentCompanyId = options?.defaultCompanyId ?? contextCompanyId;
63
+
64
+ // Seed the studio choice once the user's companies arrive: the studio open in
65
+ // the web session when it is in the list, otherwise the first one.
66
+ useEffect(() => {
67
+ const companies = clientInfo?.companies ?? [];
68
+ if (companies.length === 0) return;
69
+ if (companyId && companies.some((company) => company.id === companyId)) return;
70
+
71
+ const current = currentCompanyId ? companies.find((company) => company.id === currentCompanyId) : undefined;
72
+ setCompanyId(current?.id ?? companies[0]?.id);
73
+ }, [clientInfo, currentCompanyId, companyId]);
45
74
 
46
75
  // Fetch client info on mount
47
76
  useEffect(() => {
@@ -83,7 +112,7 @@ export function useOAuthConsent(params: OAuthConsentRequest): UseOAuthConsentRet
83
112
  setError(null);
84
113
 
85
114
  try {
86
- const result = await OAuthService.approveAuthorization(params);
115
+ const result = await OAuthService.approveAuthorization({ ...params, companyId });
87
116
 
88
117
  // Redirect to client with authorization code
89
118
  if (result.redirectUrl) {
@@ -95,7 +124,7 @@ export function useOAuthConsent(params: OAuthConsentRequest): UseOAuthConsentRet
95
124
  setIsSubmitting(false);
96
125
  }
97
126
  // Note: Don't set isSubmitting to false on success - we're redirecting
98
- }, [params]);
127
+ }, [params, companyId]);
99
128
 
100
129
  const deny = useCallback(async (): Promise<void> => {
101
130
  setIsSubmitting(true);
@@ -123,5 +152,7 @@ export function useOAuthConsent(params: OAuthConsentRequest): UseOAuthConsentRet
123
152
  approve,
124
153
  deny,
125
154
  isSubmitting,
155
+ companyId,
156
+ setCompanyId,
126
157
  };
127
158
  }
@@ -89,13 +89,15 @@ export interface OAuthConsentRequest {
89
89
  codeChallenge?: string;
90
90
  /** PKCE method: 'S256' (recommended) or 'plain' */
91
91
  codeChallengeMethod?: string;
92
+ /** The studio (company) the issued tokens are bound to; sent as `company_id` on approve */
93
+ companyId?: string;
92
94
  }
93
95
 
94
96
  /**
95
97
  * Scope information for display in consent screen
96
98
  */
97
99
  export interface OAuthScopeInfo {
98
- /** The scope identifier (e.g., 'photographs:read') */
100
+ /** The scope identifier (e.g., 'read', 'mcp') */
99
101
  scope: string;
100
102
  /** Human-readable scope name */
101
103
  name: string;
@@ -111,6 +113,8 @@ export interface OAuthScopeInfo {
111
113
  export interface OAuthConsentInfo {
112
114
  client: OAuthClientInterface;
113
115
  scopes: OAuthScopeInfo[];
116
+ /** The caller's companies, ordered by name; the user picks one when there is more than one */
117
+ companies?: Array<{ id: string; name: string }>;
114
118
  }
115
119
 
116
120
  /**
@@ -130,36 +134,18 @@ export const OAUTH_SCOPE_DISPLAY: Record<string, OAuthScopeInfo> = {
130
134
  description: "Write access to your data",
131
135
  icon: "pencil",
132
136
  },
133
- "photographs:read": {
134
- scope: "photographs:read",
135
- name: "View Photographs",
136
- description: "Access and download your photo library",
137
- icon: "image",
138
- },
139
- "photographs:write": {
140
- scope: "photographs:write",
141
- name: "Upload Photographs",
142
- description: "Add new photos to your rolls",
143
- icon: "upload",
144
- },
145
- "rolls:read": {
146
- scope: "rolls:read",
147
- name: "View Rolls",
148
- description: "See your film rolls and collections",
149
- icon: "film",
150
- },
151
- "rolls:write": {
152
- scope: "rolls:write",
153
- name: "Manage Rolls",
154
- description: "Create and modify film rolls",
155
- icon: "folder-plus",
156
- },
157
137
  profile: {
158
138
  scope: "profile",
159
139
  name: "View Profile",
160
140
  description: "Access your name and email",
161
141
  icon: "user",
162
142
  },
143
+ mcp: {
144
+ scope: "mcp",
145
+ name: "MCP Server Access",
146
+ description: "Let an AI assistant read and act on your data through the MCP server",
147
+ icon: "shield",
148
+ },
163
149
  admin: {
164
150
  scope: "admin",
165
151
  name: "Administrative Access",
@@ -243,7 +243,7 @@ describe("useDataListRetriever", () => {
243
243
  const retriever = vi.fn().mockResolvedValue(mockData);
244
244
 
245
245
  const { result } = renderHook(() =>
246
- useDataListRetriever({
246
+ useDataListRetriever<(typeof mockData)[number]>({
247
247
  retriever,
248
248
  module: mockModule,
249
249
  }),
@@ -1,36 +0,0 @@
1
- import { A as ApiDataInterface } from './ApiDataInterface-BcZeXy5X.js';
2
-
3
- type AssistantInput = {
4
- firstMessage: string;
5
- title?: string;
6
- howToMode?: boolean;
7
- limitToHowToId?: string;
8
- handbookMode?: boolean;
9
- limitToHandbookPageId?: string;
10
- /**
11
- * BlockNote document behind `firstMessage`. Sent alongside the flattened
12
- * text so the server can re-derive the content and read `@`-mentions from
13
- * the inline nodes; the flattened text is never trusted on its own.
14
- */
15
- contentBlocks?: unknown[];
16
- /**
17
- * Resource the thread is bound to (e.g. a campaign). Confines every agent
18
- * data-access point in the run to that resource's scope.
19
- */
20
- boundContent?: {
21
- type: string;
22
- id: string;
23
- };
24
- };
25
- interface AssistantInterface extends ApiDataInterface {
26
- get title(): string;
27
- get messageCount(): number;
28
- /** Agent engine the thread runs on — `"operator"` or undefined (responder). */
29
- get engine(): string | undefined;
30
- /** JSON:API type of the resource this thread is bound to, when bound. */
31
- get boundContentType(): string | undefined;
32
- /** Id of the resource this thread is bound to, when bound. */
33
- get boundContentId(): string | undefined;
34
- }
35
-
36
- export type { AssistantInput as A, AssistantInterface as a };
@@ -1,36 +0,0 @@
1
- import { A as ApiDataInterface } from './ApiDataInterface-BcZeXy5X.mjs';
2
-
3
- type AssistantInput = {
4
- firstMessage: string;
5
- title?: string;
6
- howToMode?: boolean;
7
- limitToHowToId?: string;
8
- handbookMode?: boolean;
9
- limitToHandbookPageId?: string;
10
- /**
11
- * BlockNote document behind `firstMessage`. Sent alongside the flattened
12
- * text so the server can re-derive the content and read `@`-mentions from
13
- * the inline nodes; the flattened text is never trusted on its own.
14
- */
15
- contentBlocks?: unknown[];
16
- /**
17
- * Resource the thread is bound to (e.g. a campaign). Confines every agent
18
- * data-access point in the run to that resource's scope.
19
- */
20
- boundContent?: {
21
- type: string;
22
- id: string;
23
- };
24
- };
25
- interface AssistantInterface extends ApiDataInterface {
26
- get title(): string;
27
- get messageCount(): number;
28
- /** Agent engine the thread runs on — `"operator"` or undefined (responder). */
29
- get engine(): string | undefined;
30
- /** JSON:API type of the resource this thread is bound to, when bound. */
31
- get boundContentType(): string | undefined;
32
- /** Id of the resource this thread is bound to, when bound. */
33
- get boundContentId(): string | undefined;
34
- }
35
-
36
- export type { AssistantInput as A, AssistantInterface as a };