@ekanos/sdk 0.2.0 → 0.4.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 (68) hide show
  1. package/README.md +113 -33
  2. package/api-report.md +15 -4
  3. package/dist/components/base-activation-dialog.d.ts +15 -5
  4. package/dist/components/base-activation-dialog.js +31 -5
  5. package/dist/components/base-activation-dialog.js.map +1 -1
  6. package/dist/components/base-activation-form.d.ts +20 -5
  7. package/dist/components/base-activation-form.js +12 -5
  8. package/dist/components/base-activation-form.js.map +1 -1
  9. package/dist/components/base-marketplace-tile.js +12 -10
  10. package/dist/components/base-marketplace-tile.js.map +1 -1
  11. package/dist/components/index.d.ts +1 -1
  12. package/dist/components/index.js.map +1 -1
  13. package/dist/components/oauth-activation-form.d.ts +23 -16
  14. package/dist/components/oauth-activation-form.js +34 -20
  15. package/dist/components/oauth-activation-form.js.map +1 -1
  16. package/dist/components/widgets/use-relative-time.d.ts +7 -0
  17. package/dist/components/widgets/use-relative-time.js +55 -0
  18. package/dist/components/widgets/use-relative-time.js.map +1 -0
  19. package/dist/components/widgets/widget-ask-bar.js.map +1 -1
  20. package/dist/components/widgets/widget-context.d.ts +21 -0
  21. package/dist/components/widgets/widget-context.js.map +1 -1
  22. package/dist/components/widgets/widget-preview-provider.d.ts +1 -1
  23. package/dist/components/widgets/widget-preview-provider.js +1 -1
  24. package/dist/components/widgets/widget-preview-provider.js.map +1 -1
  25. package/dist/components/widgets/widget-state-components.d.ts +4 -1
  26. package/dist/components/widgets/widget-state-components.js +6 -4
  27. package/dist/components/widgets/widget-state-components.js.map +1 -1
  28. package/dist/components/widgets/widget.d.ts +47 -2
  29. package/dist/components/widgets/widget.js +111 -23
  30. package/dist/components/widgets/widget.js.map +1 -1
  31. package/dist/context/egress.js.map +1 -1
  32. package/dist/context/storage-validation.d.ts +24 -4
  33. package/dist/context/storage-validation.js +5 -2
  34. package/dist/context/storage-validation.js.map +1 -1
  35. package/dist/errors/public-action-error.d.ts +31 -0
  36. package/dist/errors/public-action-error.js +35 -0
  37. package/dist/errors/public-action-error.js.map +1 -0
  38. package/dist/hooks/ask-assistant-context.js.map +1 -1
  39. package/dist/hooks/fetch-integration-storage.js.map +1 -1
  40. package/dist/hooks/index.d.ts +2 -0
  41. package/dist/hooks/index.js +6 -0
  42. package/dist/hooks/index.js.map +1 -1
  43. package/dist/hooks/partner-oauth.js +3 -0
  44. package/dist/hooks/partner-oauth.js.map +1 -1
  45. package/dist/hooks/use-integration-action.d.ts +105 -0
  46. package/dist/hooks/use-integration-action.js +134 -0
  47. package/dist/hooks/use-integration-action.js.map +1 -0
  48. package/dist/hooks/use-oauth-connection-status.d.ts +78 -78
  49. package/dist/hooks/use-oauth-connection-status.js +17 -10
  50. package/dist/hooks/use-oauth-connection-status.js.map +1 -1
  51. package/dist/hooks/widget-event-context.js.map +1 -1
  52. package/dist/integration/event-surfaces.js.map +1 -1
  53. package/dist/integration/index.d.ts +3 -2
  54. package/dist/integration/index.js +5 -1
  55. package/dist/integration/index.js.map +1 -1
  56. package/dist/testing/invoke.js +9 -5
  57. package/dist/testing/invoke.js.map +1 -1
  58. package/dist/testing/mock-context.js +8 -7
  59. package/dist/testing/mock-context.js.map +1 -1
  60. package/dist/testing/mock-fetch-provider.js.map +1 -1
  61. package/dist/types/integration.d.ts +42 -24
  62. package/dist/types/integration.js.map +1 -1
  63. package/dist/types/widget-ask-context.d.ts +9 -0
  64. package/dist/types/widget-ask-context.js.map +1 -1
  65. package/dist/types/workspace-target.d.ts +3 -2
  66. package/dist/types/workspace-target.js.map +1 -1
  67. package/eslint.preset.mjs +9 -8
  68. package/package.json +37 -41
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The browser half of the widget-actions transport.
3
+ *
4
+ * A partner's `actions` declaration on `IntegrationDefinition` is a
5
+ * record of named, deterministic server handlers —
6
+ * `(ctx, input) => Promise<unknown>` — invoked by a widget's own UI (a Save
7
+ * button), never synthesized by the AI assistant. The host serves one
8
+ * generic route per action:
9
+ *
10
+ * POST /api/integrations/[slug]/actions/[actionName]
11
+ *
12
+ * and this hook is the only place a partner package should build that
13
+ * request, so a route move is one SDK change rather than a hunt through
14
+ * every widget — the same discipline `partnerOAuthConnectPath`/
15
+ * `fetchIntegrationStorage` already apply to their own routes.
16
+ *
17
+ * ```tsx
18
+ * const createRecord = useIntegrationAction<CreateRecordInput, CreateRecordOutput>(
19
+ * 'acme-crm',
20
+ * 'create-record',
21
+ * { accountId },
22
+ * );
23
+ *
24
+ * <Button onClick={() => createRecord.mutate(formValues)}>Save</Button>
25
+ * ```
26
+ *
27
+ * ── Typing is a convention, not a cross-package mechanism ───────────────────
28
+ *
29
+ * `@ekanos/sdk` is a separately published package — it cannot see a specific
30
+ * partner's `z.ZodTypeAny` instance, since that object lives in the
31
+ * partner's own server bundle and never ships to the browser, let alone into
32
+ * the SDK's own compiled types. `TInput`/`TOutput` are generic parameters
33
+ * the CALLER supplies by hand at the call site — typically by also exporting
34
+ * `type CreateRecordInput = z.infer<typeof createRecordSchema>` from the same
35
+ * package that declares the action and importing it into the widget. This
36
+ * hook treats the wire payload as `unknown` at runtime — it never parses it
37
+ * against the partner's own zod schema, only against the success/error
38
+ * envelope shape — so drift between the declared action and the widget's
39
+ * type import is a partner-package refactor problem, not a network-boundary
40
+ * one. Same discipline `fetchIntegrationStorage` already documents for
41
+ * storage ("Values are returned raw and unvalidated").
42
+ *
43
+ * ── Errors are typed, never raw fetch/transport exceptions ──────────────────
44
+ *
45
+ * A rejected mutation's `error` is always an `IntegrationActionError` —
46
+ * never a bare `Error`, a `Response`, or an unwrapped parse/network
47
+ * exception. `status` and `code` are always present; `issues` (the partner's
48
+ * own zod issues) is present only for the route's 400 `invalid_input`
49
+ * response, so a form can map failures back onto fields without
50
+ * string-matching a message.
51
+ */
52
+ import { type UseMutationResult } from '@tanstack/react-query';
53
+ export interface UseIntegrationActionOptions {
54
+ /** The account the action runs against. Must be one the caller belongs to. */
55
+ accountId: string;
56
+ /**
57
+ * Overrides the base path. Exists for harnesses and tests that serve the
58
+ * route from somewhere other than the host's own origin; production code
59
+ * should leave it alone.
60
+ */
61
+ baseUrl?: string;
62
+ }
63
+ /**
64
+ * Builds the host actions-route URL for one integration/action pair.
65
+ * Exported because a harness or a test double needs to recognize the exact
66
+ * URL this hook will request.
67
+ */
68
+ export declare function integrationActionUrl(integrationSlug: string, actionName: string, options?: {
69
+ baseUrl?: string;
70
+ }): string;
71
+ /**
72
+ * Route-defined failure codes the host actions route answers with, plus two
73
+ * transport-level codes this hook assigns itself when the response never
74
+ * reaches a valid envelope: `'network_error'` (the request itself failed —
75
+ * offline, DNS, aborted) and `'invalid_response'` (a response came back but
76
+ * its body isn't the envelope shape this transport expects).
77
+ */
78
+ export type IntegrationActionErrorCode = 'action_not_found' | 'rate_limited' | 'invalid_input' | 'handler_error' | 'network_error' | 'invalid_response' | (string & {});
79
+ export interface IntegrationActionErrorDetails {
80
+ /** HTTP status; `0` means the request never reached the server. */
81
+ status: number;
82
+ code: IntegrationActionErrorCode;
83
+ /** The partner's own zod issues — present only for a 400 `invalid_input` response. */
84
+ issues?: unknown[];
85
+ }
86
+ /**
87
+ * The typed failure a rejected `useIntegrationAction` mutation always throws.
88
+ * `status`/`code` let a caller branch without string-matching `message`;
89
+ * `issues` lets a form map the partner's own validation failures back onto
90
+ * fields.
91
+ */
92
+ export declare class IntegrationActionError extends Error {
93
+ readonly status: number;
94
+ readonly code: IntegrationActionErrorCode;
95
+ readonly issues?: unknown[];
96
+ constructor(message: string, details: IntegrationActionErrorDetails);
97
+ }
98
+ /**
99
+ * Invokes a declared partner action from a widget. `mutate`/`mutateAsync`
100
+ * take the action's `TInput` directly — the caller's own already-validated
101
+ * values, no assistant and no natural-language round trip in between. A
102
+ * resolved mutation's `data` is `TOutput`; a rejected one's `error` is always
103
+ * an `IntegrationActionError` (see the module doc comment above).
104
+ */
105
+ export declare function useIntegrationAction<TInput = unknown, TOutput = unknown>(integrationSlug: string, actionName: string, options: UseIntegrationActionOptions): UseMutationResult<TOutput, IntegrationActionError, TInput>;
@@ -0,0 +1,134 @@
1
+ 'use client';
2
+ /**
3
+ * The browser half of the widget-actions transport.
4
+ *
5
+ * A partner's `actions` declaration on `IntegrationDefinition` is a
6
+ * record of named, deterministic server handlers —
7
+ * `(ctx, input) => Promise<unknown>` — invoked by a widget's own UI (a Save
8
+ * button), never synthesized by the AI assistant. The host serves one
9
+ * generic route per action:
10
+ *
11
+ * POST /api/integrations/[slug]/actions/[actionName]
12
+ *
13
+ * and this hook is the only place a partner package should build that
14
+ * request, so a route move is one SDK change rather than a hunt through
15
+ * every widget — the same discipline `partnerOAuthConnectPath`/
16
+ * `fetchIntegrationStorage` already apply to their own routes.
17
+ *
18
+ * ```tsx
19
+ * const createRecord = useIntegrationAction<CreateRecordInput, CreateRecordOutput>(
20
+ * 'acme-crm',
21
+ * 'create-record',
22
+ * { accountId },
23
+ * );
24
+ *
25
+ * <Button onClick={() => createRecord.mutate(formValues)}>Save</Button>
26
+ * ```
27
+ *
28
+ * ── Typing is a convention, not a cross-package mechanism ───────────────────
29
+ *
30
+ * `@ekanos/sdk` is a separately published package — it cannot see a specific
31
+ * partner's `z.ZodTypeAny` instance, since that object lives in the
32
+ * partner's own server bundle and never ships to the browser, let alone into
33
+ * the SDK's own compiled types. `TInput`/`TOutput` are generic parameters
34
+ * the CALLER supplies by hand at the call site — typically by also exporting
35
+ * `type CreateRecordInput = z.infer<typeof createRecordSchema>` from the same
36
+ * package that declares the action and importing it into the widget. This
37
+ * hook treats the wire payload as `unknown` at runtime — it never parses it
38
+ * against the partner's own zod schema, only against the success/error
39
+ * envelope shape — so drift between the declared action and the widget's
40
+ * type import is a partner-package refactor problem, not a network-boundary
41
+ * one. Same discipline `fetchIntegrationStorage` already documents for
42
+ * storage ("Values are returned raw and unvalidated").
43
+ *
44
+ * ── Errors are typed, never raw fetch/transport exceptions ──────────────────
45
+ *
46
+ * A rejected mutation's `error` is always an `IntegrationActionError` —
47
+ * never a bare `Error`, a `Response`, or an unwrapped parse/network
48
+ * exception. `status` and `code` are always present; `issues` (the partner's
49
+ * own zod issues) is present only for the route's 400 `invalid_input`
50
+ * response, so a form can map failures back onto fields without
51
+ * string-matching a message.
52
+ */
53
+ import { useMutation } from '@tanstack/react-query';
54
+ const DEFAULT_BASE_URL = '/api/integrations';
55
+ /**
56
+ * Builds the host actions-route URL for one integration/action pair.
57
+ * Exported because a harness or a test double needs to recognize the exact
58
+ * URL this hook will request.
59
+ */
60
+ export function integrationActionUrl(integrationSlug, actionName, options = {}) {
61
+ var _a;
62
+ const base = (_a = options.baseUrl) !== null && _a !== void 0 ? _a : DEFAULT_BASE_URL;
63
+ return `${base}/${integrationSlug}/actions/${actionName}`;
64
+ }
65
+ /**
66
+ * The typed failure a rejected `useIntegrationAction` mutation always throws.
67
+ * `status`/`code` let a caller branch without string-matching `message`;
68
+ * `issues` lets a form map the partner's own validation failures back onto
69
+ * fields.
70
+ */
71
+ export class IntegrationActionError extends Error {
72
+ constructor(message, details) {
73
+ super(message);
74
+ this.name = 'IntegrationActionError';
75
+ this.status = details.status;
76
+ this.code = details.code;
77
+ this.issues = details.issues;
78
+ }
79
+ }
80
+ function isSuccessEnvelope(value) {
81
+ return (typeof value === 'object' &&
82
+ value !== null &&
83
+ value.success === true &&
84
+ 'data' in value);
85
+ }
86
+ function isErrorEnvelope(value) {
87
+ return (typeof value === 'object' &&
88
+ value !== null &&
89
+ value.success === false &&
90
+ typeof value.error === 'string');
91
+ }
92
+ async function runIntegrationAction(integrationSlug, actionName, input, options) {
93
+ var _a;
94
+ const url = integrationActionUrl(integrationSlug, actionName, options);
95
+ let response;
96
+ try {
97
+ response = await globalThis.fetch(url, {
98
+ method: 'POST',
99
+ headers: {
100
+ 'content-type': 'application/json',
101
+ accept: 'application/json',
102
+ },
103
+ body: JSON.stringify({ accountId: options.accountId, input }),
104
+ });
105
+ }
106
+ catch (error) {
107
+ throw new IntegrationActionError(`Could not reach '${integrationSlug}/${actionName}' (${error instanceof Error ? error.message : 'network error'}).`, { status: 0, code: 'network_error' });
108
+ }
109
+ const payload = await response.json().catch(() => null);
110
+ if (response.ok && isSuccessEnvelope(payload)) {
111
+ return payload.data;
112
+ }
113
+ if (isErrorEnvelope(payload)) {
114
+ throw new IntegrationActionError(payload.error, {
115
+ status: response.status,
116
+ code: (_a = payload.code) !== null && _a !== void 0 ? _a : 'handler_error',
117
+ issues: payload.issues,
118
+ });
119
+ }
120
+ throw new IntegrationActionError(`Could not run '${integrationSlug}/${actionName}' (${response.status} ${response.statusText}).`, { status: response.status, code: 'invalid_response' });
121
+ }
122
+ /**
123
+ * Invokes a declared partner action from a widget. `mutate`/`mutateAsync`
124
+ * take the action's `TInput` directly — the caller's own already-validated
125
+ * values, no assistant and no natural-language round trip in between. A
126
+ * resolved mutation's `data` is `TOutput`; a rejected one's `error` is always
127
+ * an `IntegrationActionError` (see the module doc comment above).
128
+ */
129
+ export function useIntegrationAction(integrationSlug, actionName, options) {
130
+ return useMutation({
131
+ mutationFn: (input) => runIntegrationAction(integrationSlug, actionName, input, options),
132
+ });
133
+ }
134
+ //# sourceMappingURL=use-integration-action.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-integration-action.js","sourceRoot":"","sources":["../../src/hooks/use-integration-action.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\n/**\n * The browser half of the widget-actions transport.\n *\n * A partner's `actions` declaration on `IntegrationDefinition` is a\n * record of named, deterministic server handlers —\n * `(ctx, input) => Promise<unknown>` — invoked by a widget's own UI (a Save\n * button), never synthesized by the AI assistant. The host serves one\n * generic route per action:\n *\n * POST /api/integrations/[slug]/actions/[actionName]\n *\n * and this hook is the only place a partner package should build that\n * request, so a route move is one SDK change rather than a hunt through\n * every widget — the same discipline `partnerOAuthConnectPath`/\n * `fetchIntegrationStorage` already apply to their own routes.\n *\n * ```tsx\n * const createRecord = useIntegrationAction<CreateRecordInput, CreateRecordOutput>(\n * 'acme-crm',\n * 'create-record',\n * { accountId },\n * );\n *\n * <Button onClick={() => createRecord.mutate(formValues)}>Save</Button>\n * ```\n *\n * ── Typing is a convention, not a cross-package mechanism ───────────────────\n *\n * `@ekanos/sdk` is a separately published package — it cannot see a specific\n * partner's `z.ZodTypeAny` instance, since that object lives in the\n * partner's own server bundle and never ships to the browser, let alone into\n * the SDK's own compiled types. `TInput`/`TOutput` are generic parameters\n * the CALLER supplies by hand at the call site — typically by also exporting\n * `type CreateRecordInput = z.infer<typeof createRecordSchema>` from the same\n * package that declares the action and importing it into the widget. This\n * hook treats the wire payload as `unknown` at runtime — it never parses it\n * against the partner's own zod schema, only against the success/error\n * envelope shape — so drift between the declared action and the widget's\n * type import is a partner-package refactor problem, not a network-boundary\n * one. Same discipline `fetchIntegrationStorage` already documents for\n * storage (\"Values are returned raw and unvalidated\").\n *\n * ── Errors are typed, never raw fetch/transport exceptions ──────────────────\n *\n * A rejected mutation's `error` is always an `IntegrationActionError` —\n * never a bare `Error`, a `Response`, or an unwrapped parse/network\n * exception. `status` and `code` are always present; `issues` (the partner's\n * own zod issues) is present only for the route's 400 `invalid_input`\n * response, so a form can map failures back onto fields without\n * string-matching a message.\n */\nimport { type UseMutationResult, useMutation } from '@tanstack/react-query';\n\nconst DEFAULT_BASE_URL = '/api/integrations';\n\nexport interface UseIntegrationActionOptions {\n /** The account the action runs against. Must be one the caller belongs to. */\n accountId: string;\n /**\n * Overrides the base path. Exists for harnesses and tests that serve the\n * route from somewhere other than the host's own origin; production code\n * should leave it alone.\n */\n baseUrl?: string;\n}\n\n/**\n * Builds the host actions-route URL for one integration/action pair.\n * Exported because a harness or a test double needs to recognize the exact\n * URL this hook will request.\n */\nexport function integrationActionUrl(\n integrationSlug: string,\n actionName: string,\n options: { baseUrl?: string } = {},\n): string {\n const base = options.baseUrl ?? DEFAULT_BASE_URL;\n return `${base}/${integrationSlug}/actions/${actionName}`;\n}\n\n/**\n * Route-defined failure codes the host actions route answers with, plus two\n * transport-level codes this hook assigns itself when the response never\n * reaches a valid envelope: `'network_error'` (the request itself failed —\n * offline, DNS, aborted) and `'invalid_response'` (a response came back but\n * its body isn't the envelope shape this transport expects).\n */\nexport type IntegrationActionErrorCode =\n | 'action_not_found'\n | 'rate_limited'\n | 'invalid_input'\n | 'handler_error'\n | 'network_error'\n | 'invalid_response'\n | (string & {});\n\nexport interface IntegrationActionErrorDetails {\n /** HTTP status; `0` means the request never reached the server. */\n status: number;\n code: IntegrationActionErrorCode;\n /** The partner's own zod issues — present only for a 400 `invalid_input` response. */\n issues?: unknown[];\n}\n\n/**\n * The typed failure a rejected `useIntegrationAction` mutation always throws.\n * `status`/`code` let a caller branch without string-matching `message`;\n * `issues` lets a form map the partner's own validation failures back onto\n * fields.\n */\nexport class IntegrationActionError extends Error {\n readonly status: number;\n readonly code: IntegrationActionErrorCode;\n readonly issues?: unknown[];\n\n constructor(message: string, details: IntegrationActionErrorDetails) {\n super(message);\n this.name = 'IntegrationActionError';\n this.status = details.status;\n this.code = details.code;\n this.issues = details.issues;\n }\n}\n\ninterface ActionSuccessEnvelope {\n success: true;\n data: unknown;\n}\n\ninterface ActionErrorEnvelope {\n success: false;\n error: string;\n code?: string;\n issues?: unknown[];\n}\n\nfunction isSuccessEnvelope(value: unknown): value is ActionSuccessEnvelope {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { success?: unknown }).success === true &&\n 'data' in value\n );\n}\n\nfunction isErrorEnvelope(value: unknown): value is ActionErrorEnvelope {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { success?: unknown }).success === false &&\n typeof (value as { error?: unknown }).error === 'string'\n );\n}\n\nasync function runIntegrationAction<TInput, TOutput>(\n integrationSlug: string,\n actionName: string,\n input: TInput,\n options: UseIntegrationActionOptions,\n): Promise<TOutput> {\n const url = integrationActionUrl(integrationSlug, actionName, options);\n\n let response: Response;\n try {\n response = await globalThis.fetch(url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'application/json',\n },\n body: JSON.stringify({ accountId: options.accountId, input }),\n });\n } catch (error) {\n throw new IntegrationActionError(\n `Could not reach '${integrationSlug}/${actionName}' (${\n error instanceof Error ? error.message : 'network error'\n }).`,\n { status: 0, code: 'network_error' },\n );\n }\n\n const payload: unknown = await response.json().catch(() => null);\n\n if (response.ok && isSuccessEnvelope(payload)) {\n return payload.data as TOutput;\n }\n\n if (isErrorEnvelope(payload)) {\n throw new IntegrationActionError(payload.error, {\n status: response.status,\n code: payload.code ?? 'handler_error',\n issues: payload.issues,\n });\n }\n\n throw new IntegrationActionError(\n `Could not run '${integrationSlug}/${actionName}' (${response.status} ${response.statusText}).`,\n { status: response.status, code: 'invalid_response' },\n );\n}\n\n/**\n * Invokes a declared partner action from a widget. `mutate`/`mutateAsync`\n * take the action's `TInput` directly — the caller's own already-validated\n * values, no assistant and no natural-language round trip in between. A\n * resolved mutation's `data` is `TOutput`; a rejected one's `error` is always\n * an `IntegrationActionError` (see the module doc comment above).\n */\nexport function useIntegrationAction<TInput = unknown, TOutput = unknown>(\n integrationSlug: string,\n actionName: string,\n options: UseIntegrationActionOptions,\n): UseMutationResult<TOutput, IntegrationActionError, TInput> {\n return useMutation({\n mutationFn: (input: TInput) =>\n runIntegrationAction<TInput, TOutput>(\n integrationSlug,\n actionName,\n input,\n options,\n ),\n });\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Result shape of an integration's check-connection server action
3
- * (the standard enhanceAction envelope).
3
+ * (the standard server-action envelope).
4
4
  */
5
5
  export interface ConnectionCheckResult {
6
6
  success: boolean;
@@ -40,26 +40,6 @@ export interface UseOAuthConnectionStatusOptions {
40
40
  * connected without a manual refresh.
41
41
  */
42
42
  export declare function useOAuthConnectionStatus(accountId: string, options: UseOAuthConnectionStatusOptions): {
43
- oauthJustCompleted: boolean;
44
- oauthError: string | null;
45
- data: {
46
- isConnected: boolean;
47
- error: string | undefined;
48
- obtainedAt?: undefined;
49
- } | {
50
- isConnected: boolean;
51
- obtainedAt: string | null | undefined;
52
- error?: undefined;
53
- };
54
- error: Error;
55
- isError: true;
56
- isPending: false;
57
- isLoading: false;
58
- isLoadingError: false;
59
- isRefetchError: true;
60
- isSuccess: false;
61
- isPlaceholderData: false;
62
- status: "error";
63
43
  dataUpdatedAt: number;
64
44
  errorUpdatedAt: number;
65
45
  failureCount: number;
@@ -78,32 +58,24 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
78
58
  error: string | undefined;
79
59
  obtainedAt?: undefined;
80
60
  } | {
61
+ error?: undefined;
81
62
  isConnected: boolean;
82
63
  obtainedAt: string | null | undefined;
83
- error?: undefined;
84
64
  }, Error>>;
85
65
  fetchStatus: import("@tanstack/react-query").FetchStatus;
86
- } | {
87
- oauthJustCompleted: boolean;
88
- oauthError: string | null;
89
- data: {
90
- isConnected: boolean;
91
- error: string | undefined;
92
- obtainedAt?: undefined;
93
- } | {
94
- isConnected: boolean;
95
- obtainedAt: string | null | undefined;
96
- error?: undefined;
97
- };
98
- error: null;
99
- isError: false;
66
+ data: undefined;
67
+ error: Error;
68
+ isError: true;
100
69
  isPending: false;
101
70
  isLoading: false;
102
- isLoadingError: false;
71
+ isLoadingError: true;
103
72
  isRefetchError: false;
104
- isSuccess: true;
73
+ isSuccess: false;
105
74
  isPlaceholderData: false;
106
- status: "success";
75
+ status: 'error';
76
+ oauthJustCompleted: boolean;
77
+ oauthError: string | null;
78
+ } | {
107
79
  dataUpdatedAt: number;
108
80
  errorUpdatedAt: number;
109
81
  failureCount: number;
@@ -122,24 +94,24 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
122
94
  error: string | undefined;
123
95
  obtainedAt?: undefined;
124
96
  } | {
97
+ error?: undefined;
125
98
  isConnected: boolean;
126
99
  obtainedAt: string | null | undefined;
127
- error?: undefined;
128
100
  }, Error>>;
129
101
  fetchStatus: import("@tanstack/react-query").FetchStatus;
130
- } | {
131
- oauthJustCompleted: boolean;
132
- oauthError: string | null;
133
102
  data: undefined;
134
- error: Error;
135
- isError: true;
136
- isPending: false;
137
- isLoading: false;
138
- isLoadingError: true;
103
+ error: null;
104
+ isError: false;
105
+ isPending: true;
106
+ isLoading: true;
107
+ isLoadingError: false;
139
108
  isRefetchError: false;
140
109
  isSuccess: false;
141
110
  isPlaceholderData: false;
142
- status: "error";
111
+ status: 'pending';
112
+ oauthJustCompleted: boolean;
113
+ oauthError: string | null;
114
+ } | {
143
115
  dataUpdatedAt: number;
144
116
  errorUpdatedAt: number;
145
117
  failureCount: number;
@@ -148,6 +120,7 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
148
120
  isFetched: boolean;
149
121
  isFetchedAfterMount: boolean;
150
122
  isFetching: boolean;
123
+ isLoading: boolean;
151
124
  isInitialLoading: boolean;
152
125
  isPaused: boolean;
153
126
  isRefetching: boolean;
@@ -158,24 +131,23 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
158
131
  error: string | undefined;
159
132
  obtainedAt?: undefined;
160
133
  } | {
134
+ error?: undefined;
161
135
  isConnected: boolean;
162
136
  obtainedAt: string | null | undefined;
163
- error?: undefined;
164
137
  }, Error>>;
165
138
  fetchStatus: import("@tanstack/react-query").FetchStatus;
166
- } | {
167
- oauthJustCompleted: boolean;
168
- oauthError: string | null;
169
139
  data: undefined;
170
140
  error: null;
171
141
  isError: false;
172
142
  isPending: true;
173
- isLoading: true;
174
143
  isLoadingError: false;
175
144
  isRefetchError: false;
176
145
  isSuccess: false;
177
146
  isPlaceholderData: false;
178
- status: "pending";
147
+ status: 'pending';
148
+ oauthJustCompleted: boolean;
149
+ oauthError: string | null;
150
+ } | {
179
151
  dataUpdatedAt: number;
180
152
  errorUpdatedAt: number;
181
153
  failureCount: number;
@@ -194,23 +166,32 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
194
166
  error: string | undefined;
195
167
  obtainedAt?: undefined;
196
168
  } | {
169
+ error?: undefined;
197
170
  isConnected: boolean;
198
171
  obtainedAt: string | null | undefined;
199
- error?: undefined;
200
172
  }, Error>>;
201
173
  fetchStatus: import("@tanstack/react-query").FetchStatus;
202
- } | {
203
- oauthJustCompleted: boolean;
204
- oauthError: string | null;
205
- data: undefined;
206
- error: null;
174
+ data: {
175
+ isConnected: boolean;
176
+ error: string | undefined;
177
+ obtainedAt?: undefined;
178
+ } | {
179
+ error?: undefined;
180
+ isConnected: boolean;
181
+ obtainedAt: string | null | undefined;
182
+ };
207
183
  isError: false;
208
- isPending: true;
184
+ error: null;
185
+ isPending: false;
186
+ isLoading: false;
209
187
  isLoadingError: false;
210
188
  isRefetchError: false;
211
- isSuccess: false;
212
- isPlaceholderData: false;
213
- status: "pending";
189
+ isSuccess: true;
190
+ isPlaceholderData: true;
191
+ status: 'success';
192
+ oauthJustCompleted: boolean;
193
+ oauthError: string | null;
194
+ } | {
214
195
  dataUpdatedAt: number;
215
196
  errorUpdatedAt: number;
216
197
  failureCount: number;
@@ -219,7 +200,6 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
219
200
  isFetched: boolean;
220
201
  isFetchedAfterMount: boolean;
221
202
  isFetching: boolean;
222
- isLoading: boolean;
223
203
  isInitialLoading: boolean;
224
204
  isPaused: boolean;
225
205
  isRefetching: boolean;
@@ -230,32 +210,32 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
230
210
  error: string | undefined;
231
211
  obtainedAt?: undefined;
232
212
  } | {
213
+ error?: undefined;
233
214
  isConnected: boolean;
234
215
  obtainedAt: string | null | undefined;
235
- error?: undefined;
236
216
  }, Error>>;
237
217
  fetchStatus: import("@tanstack/react-query").FetchStatus;
238
- } | {
239
- oauthJustCompleted: boolean;
240
- oauthError: string | null;
241
218
  data: {
242
219
  isConnected: boolean;
243
220
  error: string | undefined;
244
221
  obtainedAt?: undefined;
245
222
  } | {
223
+ error?: undefined;
246
224
  isConnected: boolean;
247
225
  obtainedAt: string | null | undefined;
248
- error?: undefined;
249
226
  };
250
- isError: false;
251
- error: null;
227
+ error: Error;
228
+ isError: true;
252
229
  isPending: false;
253
230
  isLoading: false;
254
231
  isLoadingError: false;
255
- isRefetchError: false;
256
- isSuccess: true;
257
- isPlaceholderData: true;
258
- status: "success";
232
+ isRefetchError: true;
233
+ isSuccess: false;
234
+ isPlaceholderData: false;
235
+ status: 'error';
236
+ oauthJustCompleted: boolean;
237
+ oauthError: string | null;
238
+ } | {
259
239
  dataUpdatedAt: number;
260
240
  errorUpdatedAt: number;
261
241
  failureCount: number;
@@ -274,11 +254,31 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
274
254
  error: string | undefined;
275
255
  obtainedAt?: undefined;
276
256
  } | {
257
+ error?: undefined;
277
258
  isConnected: boolean;
278
259
  obtainedAt: string | null | undefined;
279
- error?: undefined;
280
260
  }, Error>>;
281
261
  fetchStatus: import("@tanstack/react-query").FetchStatus;
262
+ data: {
263
+ isConnected: boolean;
264
+ error: string | undefined;
265
+ obtainedAt?: undefined;
266
+ } | {
267
+ error?: undefined;
268
+ isConnected: boolean;
269
+ obtainedAt: string | null | undefined;
270
+ };
271
+ error: null;
272
+ isError: false;
273
+ isPending: false;
274
+ isLoading: false;
275
+ isLoadingError: false;
276
+ isRefetchError: false;
277
+ isSuccess: true;
278
+ isPlaceholderData: false;
279
+ status: 'success';
280
+ oauthJustCompleted: boolean;
281
+ oauthError: string | null;
282
282
  };
283
283
  /**
284
284
  * Resolves the gating booleans every OAuth-backed dashboard widget needs:
@@ -42,14 +42,18 @@ export function useOAuthConnectionStatus(accountId, options) {
42
42
  refetchOnWindowFocus: false,
43
43
  });
44
44
  const errorParamName = errorParam === null || errorParam === void 0 ? void 0 : errorParam.name;
45
- // Justified useEffect: after the OAuth redirect lands back
46
- // on this page with `?oauth_success=true`, we must (1) strip the transient
47
- // query params from the URL and (2) invalidate the cached connection status
48
- // so widgets flip to connected without a manual refresh. Both are
49
- // post-render side effects on external systems (history API + query
50
- // cache), not derivable during render.
45
+ const returnedWithError = oauthError !== null;
46
+ // Justified useEffect: after the OAuth redirect lands back on this page —
47
+ // with `?oauth_success=true` OR with the integration's error param — we
48
+ // must (1) strip the transient query params from the URL, so a reload does
49
+ // not re-announce a stale success or re-show a stale error, and (2) on
50
+ // success invalidate the cached connection status so widgets flip to
51
+ // connected without a manual refresh. Both are post-render side effects
52
+ // on external systems (history API + query cache), not derivable during
53
+ // render. The error itself stays in state for this render, so the alert
54
+ // still shows once.
51
55
  useEffect(() => {
52
- if (!oauthJustCompleted)
56
+ if (!oauthJustCompleted && !returnedWithError)
53
57
  return;
54
58
  const url = new URL(window.location.href);
55
59
  url.searchParams.delete('oauth_success');
@@ -57,11 +61,14 @@ export function useOAuthConnectionStatus(accountId, options) {
57
61
  url.searchParams.delete(errorParamName);
58
62
  }
59
63
  window.history.replaceState({}, '', url.pathname + url.search);
60
- queryClient.invalidateQueries({
61
- queryKey: ['integration-data', accountId, connectionKey],
62
- });
64
+ if (oauthJustCompleted) {
65
+ queryClient.invalidateQueries({
66
+ queryKey: ['integration-data', accountId, connectionKey],
67
+ });
68
+ }
63
69
  }, [
64
70
  oauthJustCompleted,
71
+ returnedWithError,
65
72
  accountId,
66
73
  connectionKey,
67
74
  errorParamName,
@@ -1 +1 @@
1
- {"version":3,"file":"use-oauth-connection-status.js","sourceRoot":"","sources":["../../src/hooks/use-oauth-connection-status.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useEffect, useState } from 'react';\n\nimport { useQuery, useQueryClient } from '@tanstack/react-query';\n\n/**\n * Result shape of an integration's check-connection server action\n * (the standard enhanceAction envelope).\n */\nexport interface ConnectionCheckResult {\n success: boolean;\n data?: { obtainedAt?: string | null } | null;\n error?: string;\n}\n\nexport type ConnectionCheckAction = (input: {\n accountId: string;\n}) => Promise<ConnectionCheckResult>;\n\n/**\n * Optional OAuth failure query param an integration's connect route may\n * append on redirect (e.g. dropbox_error=credentials_not_configured).\n */\nexport interface OAuthErrorParam {\n name: string;\n /** Map the raw param value to a user-facing message; return null to ignore. */\n toMessage: (value: string) => string | null;\n}\n\nexport interface UseOAuthConnectionStatusOptions {\n /**\n * Cache key segment, by convention `<slug>-connection`. The full query key\n * is ['integration-data', accountId, connectionKey] so all widgets and the\n * activation form share one deduped request per dashboard load.\n */\n connectionKey: string;\n checkConnection: ConnectionCheckAction;\n /** Gate the query (e.g. on integration-active state). Defaults to true. */\n enabled?: boolean;\n errorParam?: OAuthErrorParam;\n}\n\n/**\n * Shared OAuth connection-status hook. Detects the `oauth_success=true`\n * return from an integration's connect route, strips the transient params\n * from the URL, and invalidates the cached status so dependent UI flips to\n * connected without a manual refresh.\n */\nexport function useOAuthConnectionStatus(\n accountId: string,\n options: UseOAuthConnectionStatusOptions,\n) {\n const {\n connectionKey,\n checkConnection,\n enabled = true,\n errorParam,\n } = options;\n const queryClient = useQueryClient();\n\n // Read the OAuth return params synchronously on mount (not in an effect)\n // so the first render already knows whether we just came back from OAuth.\n const [oauthJustCompleted] = useState(() => {\n if (typeof window === 'undefined') return false;\n const params = new URLSearchParams(window.location.search);\n return params.get('oauth_success') === 'true';\n });\n\n const [oauthError] = useState<string | null>(() => {\n if (!errorParam || typeof window === 'undefined') return null;\n const value = new URLSearchParams(window.location.search).get(\n errorParam.name,\n );\n return value ? errorParam.toMessage(value) : null;\n });\n\n const query = useQuery({\n queryKey: ['integration-data', accountId, connectionKey],\n queryFn: async () => {\n const result = await checkConnection({ accountId });\n\n if (!result.success) {\n return { isConnected: false, error: result.error };\n }\n\n return {\n isConnected: true,\n obtainedAt: result.data?.obtainedAt,\n };\n },\n enabled: enabled && !!accountId,\n staleTime: 5 * 60 * 1000,\n refetchOnWindowFocus: false,\n });\n\n const errorParamName = errorParam?.name;\n\n // Justified useEffect: after the OAuth redirect lands back\n // on this page with `?oauth_success=true`, we must (1) strip the transient\n // query params from the URL and (2) invalidate the cached connection status\n // so widgets flip to connected without a manual refresh. Both are\n // post-render side effects on external systems (history API + query\n // cache), not derivable during render.\n useEffect(() => {\n if (!oauthJustCompleted) return;\n\n const url = new URL(window.location.href);\n url.searchParams.delete('oauth_success');\n\n if (errorParamName) {\n url.searchParams.delete(errorParamName);\n }\n\n window.history.replaceState({}, '', url.pathname + url.search);\n\n queryClient.invalidateQueries({\n queryKey: ['integration-data', accountId, connectionKey],\n });\n }, [\n oauthJustCompleted,\n accountId,\n connectionKey,\n errorParamName,\n queryClient,\n ]);\n\n return { ...query, oauthJustCompleted, oauthError };\n}\n\n/**\n * Resolves the gating booleans every OAuth-backed dashboard widget needs:\n *\n * - `enabled` — the integration is active and an account id is present.\n * - `isConnected` — a live OAuth connection exists (access token).\n * - `dataEnabled` — safe to fire the widget's data query (active + connected).\n * - `connectionPending` — still resolving the connection status.\n * - `connectionError` — the connection check itself failed (network/5xx).\n * Distinct from `notConnected`: a transient failure should surface as an\n * error, not as a misleading \"connect X\" prompt.\n * - `notConnected` — activated, resolved without error, but no OAuth\n * connection. The widget should render its \"connect X\" inactive state.\n *\n * The underlying connection check shares one query key across all widgets,\n * so react-query dedupes it to a single request per dashboard load.\n */\nexport function useOAuthWidgetGate(\n accountId: string | undefined,\n integrationActive: boolean,\n options: Pick<\n UseOAuthConnectionStatusOptions,\n 'connectionKey' | 'checkConnection'\n >,\n) {\n const enabled = integrationActive && !!accountId;\n const query = useOAuthConnectionStatus(accountId ?? '', {\n ...options,\n enabled,\n });\n\n const connectionPending = enabled && query.isPending;\n const connectionError = enabled && query.isError;\n const isConnected = query.data?.isConnected === true;\n const dataEnabled = enabled && isConnected;\n // Only treat as \"not connected\" when the check resolved successfully with\n // no token — a query error is an error state, not a disconnected account.\n const notConnected =\n enabled && !connectionPending && !connectionError && !isConnected;\n\n return {\n enabled,\n isConnected,\n connectionPending,\n connectionError,\n dataEnabled,\n notConnected,\n };\n}\n"]}
1
+ {"version":3,"file":"use-oauth-connection-status.js","sourceRoot":"","sources":["../../src/hooks/use-oauth-connection-status.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useEffect, useState } from 'react';\n\nimport { useQuery, useQueryClient } from '@tanstack/react-query';\n\n/**\n * Result shape of an integration's check-connection server action\n * (the standard server-action envelope).\n */\nexport interface ConnectionCheckResult {\n success: boolean;\n data?: { obtainedAt?: string | null } | null;\n error?: string;\n}\n\nexport type ConnectionCheckAction = (input: {\n accountId: string;\n}) => Promise<ConnectionCheckResult>;\n\n/**\n * Optional OAuth failure query param an integration's connect route may\n * append on redirect (e.g. dropbox_error=credentials_not_configured).\n */\nexport interface OAuthErrorParam {\n name: string;\n /** Map the raw param value to a user-facing message; return null to ignore. */\n toMessage: (value: string) => string | null;\n}\n\nexport interface UseOAuthConnectionStatusOptions {\n /**\n * Cache key segment, by convention `<slug>-connection`. The full query key\n * is ['integration-data', accountId, connectionKey] so all widgets and the\n * activation form share one deduped request per dashboard load.\n */\n connectionKey: string;\n checkConnection: ConnectionCheckAction;\n /** Gate the query (e.g. on integration-active state). Defaults to true. */\n enabled?: boolean;\n errorParam?: OAuthErrorParam;\n}\n\n/**\n * Shared OAuth connection-status hook. Detects the `oauth_success=true`\n * return from an integration's connect route, strips the transient params\n * from the URL, and invalidates the cached status so dependent UI flips to\n * connected without a manual refresh.\n */\nexport function useOAuthConnectionStatus(\n accountId: string,\n options: UseOAuthConnectionStatusOptions,\n) {\n const {\n connectionKey,\n checkConnection,\n enabled = true,\n errorParam,\n } = options;\n const queryClient = useQueryClient();\n\n // Read the OAuth return params synchronously on mount (not in an effect)\n // so the first render already knows whether we just came back from OAuth.\n const [oauthJustCompleted] = useState(() => {\n if (typeof window === 'undefined') return false;\n const params = new URLSearchParams(window.location.search);\n return params.get('oauth_success') === 'true';\n });\n\n const [oauthError] = useState<string | null>(() => {\n if (!errorParam || typeof window === 'undefined') return null;\n const value = new URLSearchParams(window.location.search).get(\n errorParam.name,\n );\n return value ? errorParam.toMessage(value) : null;\n });\n\n const query = useQuery({\n queryKey: ['integration-data', accountId, connectionKey],\n queryFn: async () => {\n const result = await checkConnection({ accountId });\n\n if (!result.success) {\n return { isConnected: false, error: result.error };\n }\n\n return {\n isConnected: true,\n obtainedAt: result.data?.obtainedAt,\n };\n },\n enabled: enabled && !!accountId,\n staleTime: 5 * 60 * 1000,\n refetchOnWindowFocus: false,\n });\n\n const errorParamName = errorParam?.name;\n\n const returnedWithError = oauthError !== null;\n\n // Justified useEffect: after the OAuth redirect lands back on this page —\n // with `?oauth_success=true` OR with the integration's error param — we\n // must (1) strip the transient query params from the URL, so a reload does\n // not re-announce a stale success or re-show a stale error, and (2) on\n // success invalidate the cached connection status so widgets flip to\n // connected without a manual refresh. Both are post-render side effects\n // on external systems (history API + query cache), not derivable during\n // render. The error itself stays in state for this render, so the alert\n // still shows once.\n useEffect(() => {\n if (!oauthJustCompleted && !returnedWithError) return;\n\n const url = new URL(window.location.href);\n url.searchParams.delete('oauth_success');\n\n if (errorParamName) {\n url.searchParams.delete(errorParamName);\n }\n\n window.history.replaceState({}, '', url.pathname + url.search);\n\n if (oauthJustCompleted) {\n queryClient.invalidateQueries({\n queryKey: ['integration-data', accountId, connectionKey],\n });\n }\n }, [\n oauthJustCompleted,\n returnedWithError,\n accountId,\n connectionKey,\n errorParamName,\n queryClient,\n ]);\n\n return { ...query, oauthJustCompleted, oauthError };\n}\n\n/**\n * Resolves the gating booleans every OAuth-backed dashboard widget needs:\n *\n * - `enabled` — the integration is active and an account id is present.\n * - `isConnected` — a live OAuth connection exists (access token).\n * - `dataEnabled` — safe to fire the widget's data query (active + connected).\n * - `connectionPending` — still resolving the connection status.\n * - `connectionError` — the connection check itself failed (network/5xx).\n * Distinct from `notConnected`: a transient failure should surface as an\n * error, not as a misleading \"connect X\" prompt.\n * - `notConnected` — activated, resolved without error, but no OAuth\n * connection. The widget should render its \"connect X\" inactive state.\n *\n * The underlying connection check shares one query key across all widgets,\n * so react-query dedupes it to a single request per dashboard load.\n */\nexport function useOAuthWidgetGate(\n accountId: string | undefined,\n integrationActive: boolean,\n options: Pick<\n UseOAuthConnectionStatusOptions,\n 'connectionKey' | 'checkConnection'\n >,\n) {\n const enabled = integrationActive && !!accountId;\n const query = useOAuthConnectionStatus(accountId ?? '', {\n ...options,\n enabled,\n });\n\n const connectionPending = enabled && query.isPending;\n const connectionError = enabled && query.isError;\n const isConnected = query.data?.isConnected === true;\n const dataEnabled = enabled && isConnected;\n // Only treat as \"not connected\" when the check resolved successfully with\n // no token — a query error is an error state, not a disconnected account.\n const notConnected =\n enabled && !connectionPending && !connectionError && !isConnected;\n\n return {\n enabled,\n isConnected,\n connectionPending,\n connectionError,\n dataEnabled,\n notConnected,\n };\n}\n"]}