@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.
- package/dist/{AssistantMessageInterface-CuaMDs2i.d.ts → AssistantMessageInterface-1Kbz8RVs.d.ts} +34 -1
- package/dist/{AssistantMessageInterface-Du3ShThp.d.mts → AssistantMessageInterface-CoxJyuFM.d.mts} +34 -1
- package/dist/{BlockNoteEditor-BUSOUPQ7.js → BlockNoteEditor-PTD2V4CH.js} +19 -19
- package/dist/{BlockNoteEditor-BUSOUPQ7.js.map → BlockNoteEditor-PTD2V4CH.js.map} +1 -1
- package/dist/{BlockNoteEditor-W42IQTGL.mjs → BlockNoteEditor-Q2WEDMKJ.mjs} +4 -4
- package/dist/{BlockNoteViewer-FQ23TZ3J.mjs → BlockNoteViewer-WO6GTDSI.mjs} +2 -2
- package/dist/{BlockNoteViewer-MCX53AR4.js → BlockNoteViewer-WOXEJ6RM.js} +3 -3
- package/dist/{BlockNoteViewer-MCX53AR4.js.map → BlockNoteViewer-WOXEJ6RM.js.map} +1 -1
- package/dist/billing/index.js +401 -401
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-O5MKEU7G.mjs → chunk-2O3SFHQ7.mjs} +36 -38
- package/dist/{chunk-O5MKEU7G.mjs.map → chunk-2O3SFHQ7.mjs.map} +1 -1
- package/dist/{chunk-FXWO5J2R.js → chunk-3D2MCVRZ.js} +7 -7
- package/dist/{chunk-FXWO5J2R.js.map → chunk-3D2MCVRZ.js.map} +1 -1
- package/dist/{chunk-VR65NRE4.js → chunk-3YJF7QY4.js} +1338 -1115
- package/dist/chunk-3YJF7QY4.js.map +1 -0
- package/dist/{chunk-GFLEFIAW.mjs → chunk-GJHKOG5L.mjs} +1399 -1176
- package/dist/chunk-GJHKOG5L.mjs.map +1 -0
- package/dist/{chunk-JZUBFAAY.mjs → chunk-HAI7G6MD.mjs} +2 -2
- package/dist/{chunk-YCHPWXHD.js → chunk-IV4DTYXN.js} +36 -38
- package/dist/chunk-IV4DTYXN.js.map +1 -0
- package/dist/client/index.d.mts +13 -2
- package/dist/client/index.d.ts +13 -2
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +87 -28
- package/dist/components/index.d.ts +87 -28
- package/dist/components/index.js +8 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +7 -3
- package/dist/contexts/index.d.mts +11 -2
- package/dist/contexts/index.d.ts +11 -2
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +27 -5
- package/dist/core/index.d.ts +27 -5
- package/dist/core/index.js +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.js +101 -101
- package/dist/features/tokenusage/index.mjs +3 -3
- package/dist/index.d.mts +2 -3
- package/dist/index.d.ts +2 -3
- package/dist/index.js +3 -3
- package/dist/index.mjs +2 -2
- package/dist/{oauth.interface-CD90ycYz.d.ts → oauth.interface-BZKZAPRw.d.ts} +8 -1
- package/dist/{oauth.interface-DRE3NAKc.d.mts → oauth.interface-nVwnzxJw.d.mts} +8 -1
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -1
- package/src/components/forms/__tests__/EditorSheet.test.tsx +5 -3
- package/src/core/utils/__tests__/translateResponse.test.ts +3 -1
- package/src/features/assistant/components/containers/AssistantContainer.tsx +105 -36
- package/src/features/assistant/components/containers/__tests__/AssistantContainer.spec.tsx +163 -1
- package/src/features/assistant/components/parts/AssistantComposer.tsx +38 -15
- package/src/features/assistant/components/parts/AssistantEmptyState.tsx +5 -3
- package/src/features/assistant/components/parts/AssistantSidebar.tsx +26 -7
- package/src/features/assistant/components/parts/__tests__/AssistantComposer.spec.tsx +8 -0
- package/src/features/assistant/components/parts/__tests__/AssistantSidebar.spec.tsx +7 -0
- package/src/features/assistant/contexts/AssistantContext.tsx +91 -7
- package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +196 -0
- package/src/features/assistant-action/components/AssistantContainerWithApprovals.tsx +23 -1
- package/src/features/assistant-message/components/EntityMentionLink.tsx +28 -0
- package/src/features/assistant-message/components/__tests__/EntityMentionLink.spec.tsx +58 -0
- package/src/features/assistant-message/components/index.ts +1 -0
- package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +1 -1
- package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +2 -2
- package/src/features/auth/components/forms/__tests__/Logout.spec.tsx +3 -2
- package/src/features/oauth/components/OAuthClientForm.tsx +21 -4
- package/src/features/oauth/components/__tests__/OAuthClientForm.spec.tsx +63 -0
- package/src/features/oauth/components/consent/OAuthCompanySelector.tsx +54 -0
- package/src/features/oauth/components/consent/OAuthConsentActions.tsx +5 -2
- package/src/features/oauth/components/consent/OAuthConsentHeader.tsx +10 -6
- package/src/features/oauth/components/consent/OAuthConsentScreen.tsx +33 -13
- package/src/features/oauth/components/consent/OAuthScopeList.tsx +4 -1
- package/src/features/oauth/components/consent/__tests__/OAuthConsentHeader.spec.tsx +25 -0
- package/src/features/oauth/components/consent/__tests__/OAuthConsentScreen.spec.tsx +126 -0
- package/src/features/oauth/components/consent/index.ts +1 -0
- package/src/features/oauth/data/__tests__/oauth.service.spec.ts +103 -0
- package/src/features/oauth/data/oauth.service.ts +3 -13
- package/src/features/oauth/data/oauth.ts +30 -1
- package/src/features/oauth/hooks/useOAuthConsent.ts +34 -3
- package/src/features/oauth/interfaces/oauth.interface.ts +11 -25
- package/src/hooks/__tests__/useDataListRetriever.test.ts +1 -1
- package/dist/AssistantInterface-RvZzlcA_.d.ts +0 -36
- package/dist/AssistantInterface-jL56dyNc.d.mts +0 -36
- package/dist/chunk-GFLEFIAW.mjs.map +0 -1
- package/dist/chunk-VR65NRE4.js.map +0 -1
- package/dist/chunk-YCHPWXHD.js.map +0 -1
- /package/dist/{BlockNoteEditor-W42IQTGL.mjs.map → BlockNoteEditor-Q2WEDMKJ.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-FQ23TZ3J.mjs.map → BlockNoteViewer-WO6GTDSI.mjs.map} +0 -0
- /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
|
+
});
|
|
@@ -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(
|
|
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., '
|
|
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 };
|