@geniee-inc/sfa-kit 1.6.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/GUIDE.md +585 -0
- package/LICENSE.md +21 -0
- package/README.md +31 -0
- package/package.json +36 -0
- package/src/index.d.ts +29 -0
- package/src/index.js +21 -0
- package/src/lib/auth/DevToolbar.d.ts +38 -0
- package/src/lib/auth/DevToolbar.js +271 -0
- package/src/lib/auth/SfaLoginGate.d.ts +53 -0
- package/src/lib/auth/SfaLoginGate.js +462 -0
- package/src/lib/auth/StandalonePreviewContext.d.ts +27 -0
- package/src/lib/auth/StandalonePreviewContext.js +14 -0
- package/src/lib/auth/embed-protocol.d.ts +40 -0
- package/src/lib/auth/embed-protocol.js +56 -0
- package/src/lib/auth/exchange-code.d.ts +14 -0
- package/src/lib/auth/exchange-code.js +45 -0
- package/src/lib/auth/keystone-auth.d.ts +16 -0
- package/src/lib/auth/keystone-auth.js +84 -0
- package/src/lib/auth/session-store.d.ts +19 -0
- package/src/lib/auth/session-store.js +73 -0
- package/src/lib/auth/types.d.ts +33 -0
- package/src/lib/auth/types.js +1 -0
- package/src/lib/auth/useEmbedContext.d.ts +22 -0
- package/src/lib/auth/useEmbedContext.js +31 -0
- package/src/lib/components/EmptyState.d.ts +18 -0
- package/src/lib/components/EmptyState.js +36 -0
- package/src/lib/components/ErrorBoundary.d.ts +31 -0
- package/src/lib/components/ErrorBoundary.js +100 -0
- package/src/lib/components/FieldRenderer.d.ts +11 -0
- package/src/lib/components/FieldRenderer.js +57 -0
- package/src/lib/components/RecordList.d.ts +20 -0
- package/src/lib/components/RecordList.js +139 -0
- package/src/lib/components/RecordView.d.ts +14 -0
- package/src/lib/components/RecordView.js +153 -0
- package/src/lib/errors.d.ts +15 -0
- package/src/lib/errors.js +15 -0
- package/src/lib/hooks/useCreateRecord.d.ts +17 -0
- package/src/lib/hooks/useCreateRecord.js +48 -0
- package/src/lib/hooks/useDeleteRecord.d.ts +16 -0
- package/src/lib/hooks/useDeleteRecord.js +48 -0
- package/src/lib/hooks/useDescribe.d.ts +16 -0
- package/src/lib/hooks/useDescribe.js +40 -0
- package/src/lib/hooks/useFieldTypes.d.ts +15 -0
- package/src/lib/hooks/useFieldTypes.js +39 -0
- package/src/lib/hooks/useObjectCatalog.d.ts +15 -0
- package/src/lib/hooks/useObjectCatalog.js +39 -0
- package/src/lib/hooks/useRecord.d.ts +15 -0
- package/src/lib/hooks/useRecord.js +49 -0
- package/src/lib/hooks/useRecordList.d.ts +19 -0
- package/src/lib/hooks/useRecordList.js +82 -0
- package/src/lib/hooks/useRelatedRecords.d.ts +65 -0
- package/src/lib/hooks/useRelatedRecords.js +227 -0
- package/src/lib/hooks/useUpdateRecord.d.ts +23 -0
- package/src/lib/hooks/useUpdateRecord.js +55 -0
- package/src/lib/provider/SfaClient.d.ts +98 -0
- package/src/lib/provider/SfaClient.js +688 -0
- package/src/lib/provider/SfaProvider.d.ts +38 -0
- package/src/lib/provider/SfaProvider.js +44 -0
- package/src/lib/provider/types.d.ts +184 -0
- package/src/lib/provider/types.js +1 -0
- package/src/lib/provider/useSfaClient.d.ts +11 -0
- package/src/lib/provider/useSfaClient.js +24 -0
- package/src/lib/theme.d.ts +55 -0
- package/src/lib/theme.js +68 -0
- package/src/lib/version.d.ts +1 -0
- package/src/lib/version.js +1 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { __awaiter } from "tslib";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { SfaProvider } from '../provider/SfaProvider';
|
|
5
|
+
import { color, font, radius, space, baseContainerStyle, baseButtonStyle, } from '../theme';
|
|
6
|
+
import { SFA_KIT_VERSION } from '../version';
|
|
7
|
+
import { DevToolbar } from './DevToolbar';
|
|
8
|
+
import { parseInboundMessage, sendToHost } from './embed-protocol';
|
|
9
|
+
import { exchangeCode } from './exchange-code';
|
|
10
|
+
import { authenticate } from './keystone-auth';
|
|
11
|
+
import { loadSession, saveSession, clearSession, loadLastOrgId, } from './session-store';
|
|
12
|
+
import { StandalonePreviewCtx } from './StandalonePreviewContext';
|
|
13
|
+
import { EmbedContextValue } from './useEmbedContext';
|
|
14
|
+
const DEFAULT_API_BASE = 'https://keystone-api.chikyu.mobi';
|
|
15
|
+
const REFRESH_BUFFER_SECONDS = 120;
|
|
16
|
+
/** After this many consecutive exchange failures, stop retrying and show session_expired. */
|
|
17
|
+
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
18
|
+
/** Seconds to wait for the host to send a code before showing a connection error. */
|
|
19
|
+
const CODE_TIMEOUT_SECONDS = 15;
|
|
20
|
+
/**
|
|
21
|
+
* @description Returns true when the page is running inside an iframe.
|
|
22
|
+
*/
|
|
23
|
+
function isEmbedded() {
|
|
24
|
+
try {
|
|
25
|
+
return window.parent !== window;
|
|
26
|
+
}
|
|
27
|
+
catch (_a) {
|
|
28
|
+
// Cross-origin access to window.parent throws; that means we're embedded.
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @description Returns true when the page was served with a content token (`_t`).
|
|
34
|
+
*
|
|
35
|
+
* The presence of `_t` means the Keystone content endpoint already authenticated
|
|
36
|
+
* the request, so the page is running in the SFA embed context. This lets us
|
|
37
|
+
* auto-detect embed mode without requiring the server to inject `trustedOrigin`.
|
|
38
|
+
*/
|
|
39
|
+
function hasContentToken() {
|
|
40
|
+
try {
|
|
41
|
+
return new URLSearchParams(window.location.search).has('_t');
|
|
42
|
+
}
|
|
43
|
+
catch (_a) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* @description Derives the trusted origin for postMessage validation.
|
|
49
|
+
*
|
|
50
|
+
* Priority: explicit `trustedOrigin` prop > `document.referrer` (the parent
|
|
51
|
+
* page that loaded this iframe). Falls back to undefined if neither is available.
|
|
52
|
+
*/
|
|
53
|
+
function resolveTrustedOrigin(explicitOrigin) {
|
|
54
|
+
if (explicitOrigin != null)
|
|
55
|
+
return explicitOrigin;
|
|
56
|
+
try {
|
|
57
|
+
if (document.referrer)
|
|
58
|
+
return new URL(document.referrer).origin;
|
|
59
|
+
}
|
|
60
|
+
catch (_a) {
|
|
61
|
+
// malformed referrer
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* @description Universal auth wrapper for sfa-kit pages.
|
|
67
|
+
*
|
|
68
|
+
* Auto-detects its context:
|
|
69
|
+
* - **Embedded (iframe with `_t`)**: when the page is inside an iframe and
|
|
70
|
+
* was served with a content token (`_t` query parameter), uses the
|
|
71
|
+
* postMessage code-exchange flow. No login form. The trusted origin for
|
|
72
|
+
* postMessage validation is derived from `document.referrer` (or the
|
|
73
|
+
* explicit `trustedOrigin` prop if provided).
|
|
74
|
+
* - **Standalone**: shows a GENIEE SFA/CRM-branded login form. Org is resolved
|
|
75
|
+
* automatically from the API (auto-selected for multi-org accounts when
|
|
76
|
+
* defaultOrganizationId is set, or the first org is used by default).
|
|
77
|
+
*/
|
|
78
|
+
export function SfaLoginGate({ apiBase = DEFAULT_API_BASE, trustedOrigin, defaultOrganizationId, objectKey, level, children, }) {
|
|
79
|
+
const shouldEmbed = isEmbedded() && (trustedOrigin != null || hasContentToken());
|
|
80
|
+
const resolvedOrigin = resolveTrustedOrigin(trustedOrigin);
|
|
81
|
+
if (shouldEmbed && resolvedOrigin != null) {
|
|
82
|
+
return (_jsx(EmbedFlow, { trustedOrigin: resolvedOrigin, apiBase: apiBase, children: children }));
|
|
83
|
+
}
|
|
84
|
+
return (_jsx(StandaloneFlow, { apiBase: apiBase, defaultOrganizationId: defaultOrganizationId, objectKey: objectKey, level: level, children: children }));
|
|
85
|
+
}
|
|
86
|
+
function EmbedFlow({ trustedOrigin, apiBase, children, }) {
|
|
87
|
+
const [phase, setPhase] = useState({ step: 'waiting_for_code' });
|
|
88
|
+
const refreshTimerRef = useRef(null);
|
|
89
|
+
const exchangingRef = useRef(false);
|
|
90
|
+
const failureCountRef = useRef(0);
|
|
91
|
+
const normalizedBase = useMemo(() => apiBase.replace(/\/+$/, ''), [apiBase]);
|
|
92
|
+
const clearRefreshTimer = useCallback(() => {
|
|
93
|
+
if (refreshTimerRef.current != null) {
|
|
94
|
+
clearTimeout(refreshTimerRef.current);
|
|
95
|
+
refreshTimerRef.current = null;
|
|
96
|
+
}
|
|
97
|
+
}, []);
|
|
98
|
+
const scheduleRefresh = useCallback((expiresInSeconds) => {
|
|
99
|
+
clearRefreshTimer();
|
|
100
|
+
const refreshAfterMs = (expiresInSeconds - REFRESH_BUFFER_SECONDS) * 1000;
|
|
101
|
+
if (refreshAfterMs <= 0) {
|
|
102
|
+
sendToHost({ type: 'sfa-kit:code-request' }, trustedOrigin);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
refreshTimerRef.current = setTimeout(() => {
|
|
106
|
+
sendToHost({ type: 'sfa-kit:code-request' }, trustedOrigin);
|
|
107
|
+
}, refreshAfterMs);
|
|
108
|
+
}, [trustedOrigin, clearRefreshTimer]);
|
|
109
|
+
const handleCodeReceived = useCallback((code, context) => __awaiter(this, void 0, void 0, function* () {
|
|
110
|
+
if (exchangingRef.current)
|
|
111
|
+
return;
|
|
112
|
+
exchangingRef.current = true;
|
|
113
|
+
setPhase({ step: 'exchanging' });
|
|
114
|
+
try {
|
|
115
|
+
const session = yield exchangeCode(normalizedBase, code);
|
|
116
|
+
failureCountRef.current = 0;
|
|
117
|
+
setPhase({ step: 'authenticated', session, context });
|
|
118
|
+
const expiresInSeconds = Math.max(0, Math.floor((session.expiresAt - Date.now()) / 1000));
|
|
119
|
+
scheduleRefresh(expiresInSeconds);
|
|
120
|
+
}
|
|
121
|
+
catch (thrown) {
|
|
122
|
+
failureCountRef.current += 1;
|
|
123
|
+
if (failureCountRef.current >= MAX_CONSECUTIVE_FAILURES) {
|
|
124
|
+
setPhase({ step: 'session_expired' });
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const message = thrown instanceof Error ? thrown.message : 'Code exchange failed';
|
|
128
|
+
setPhase({ step: 'error', message });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
exchangingRef.current = false;
|
|
133
|
+
}
|
|
134
|
+
}), [normalizedBase, scheduleRefresh]);
|
|
135
|
+
const handleAuthError = useCallback(() => {
|
|
136
|
+
clearRefreshTimer();
|
|
137
|
+
failureCountRef.current += 1;
|
|
138
|
+
if (failureCountRef.current >= MAX_CONSECUTIVE_FAILURES) {
|
|
139
|
+
setPhase({ step: 'session_expired' });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
sendToHost({ type: 'sfa-kit:code-request' }, trustedOrigin);
|
|
143
|
+
setPhase({ step: 'waiting_for_code' });
|
|
144
|
+
}, [trustedOrigin, clearRefreshTimer]);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const handleMessage = (event) => {
|
|
147
|
+
var _a;
|
|
148
|
+
const parsed = parseInboundMessage(event, trustedOrigin);
|
|
149
|
+
if (!parsed)
|
|
150
|
+
return;
|
|
151
|
+
if (parsed.type === 'sfa-kit:code') {
|
|
152
|
+
void handleCodeReceived(parsed.code, (_a = parsed.context) !== null && _a !== void 0 ? _a : null);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
window.addEventListener('message', handleMessage);
|
|
156
|
+
sendToHost({ type: 'sfa-kit:ready' }, trustedOrigin);
|
|
157
|
+
// If the host never sends a code (e.g. mintSfaKitExchangeCode is not
|
|
158
|
+
// implemented or the host page doesn't support the embed protocol),
|
|
159
|
+
// show an error instead of hanging on "Connecting..." forever.
|
|
160
|
+
const codeTimeout = setTimeout(() => {
|
|
161
|
+
setPhase(current => {
|
|
162
|
+
if (current.step !== 'waiting_for_code')
|
|
163
|
+
return current;
|
|
164
|
+
return {
|
|
165
|
+
step: 'error',
|
|
166
|
+
message: 'SFA/CRM との接続がタイムアウトしました。ページを再読み込みしてください。',
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}, CODE_TIMEOUT_SECONDS * 1000);
|
|
170
|
+
return () => {
|
|
171
|
+
window.removeEventListener('message', handleMessage);
|
|
172
|
+
clearTimeout(codeTimeout);
|
|
173
|
+
clearRefreshTimer();
|
|
174
|
+
};
|
|
175
|
+
}, [trustedOrigin, handleCodeReceived, clearRefreshTimer]);
|
|
176
|
+
const auth = useMemo(() => {
|
|
177
|
+
if (phase.step !== 'authenticated')
|
|
178
|
+
return null;
|
|
179
|
+
return { accessToken: phase.session.accessToken };
|
|
180
|
+
}, [phase]);
|
|
181
|
+
if (phase.step === 'session_expired') {
|
|
182
|
+
return (_jsx(EmbedStatus, { title: "\u30BB\u30C3\u30B7\u30E7\u30F3\u671F\u9650\u5207\u308C", message: "SFA/CRM \u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u304C\u7D42\u4E86\u3057\u307E\u3057\u305F\u3002\u30DA\u30FC\u30B8\u3092\u518D\u8AAD\u307F\u8FBC\u307F\u3057\u3066\u304F\u3060\u3055\u3044\u3002", showReload: true }));
|
|
183
|
+
}
|
|
184
|
+
if (phase.step === 'error') {
|
|
185
|
+
return (_jsx(EmbedStatus, { title: "\u63A5\u7D9A\u30A8\u30E9\u30FC", message: phase.message, showReload: true }));
|
|
186
|
+
}
|
|
187
|
+
if (phase.step === 'waiting_for_code' || phase.step === 'exchanging') {
|
|
188
|
+
return (_jsx(EmbedStatus, { title: phase.step === 'exchanging' ? '認証中...' : 'SFA/CRM に接続中...', message: "\u30DB\u30B9\u30C8\u30A2\u30D7\u30EA\u30B1\u30FC\u30B7\u30E7\u30F3\u304B\u3089\u306E\u5FDC\u7B54\u3092\u5F85\u3063\u3066\u3044\u307E\u3059\u3002" }));
|
|
189
|
+
}
|
|
190
|
+
if (!auth)
|
|
191
|
+
return null;
|
|
192
|
+
return (_jsx(EmbedContextValue.Provider, { value: phase.context, children: _jsx(SfaProvider, { apiBase: normalizedBase, auth: auth, mode: "direct", organizationId: phase.session.organizationId, onAuthError: handleAuthError, children: children }) }));
|
|
193
|
+
}
|
|
194
|
+
function EmbedStatus({ title, message, showReload, }) {
|
|
195
|
+
const handleReload = useCallback(() => {
|
|
196
|
+
window.location.reload();
|
|
197
|
+
}, []);
|
|
198
|
+
return (_jsx("div", { style: styles.backdrop, children: _jsxs("div", { style: Object.assign(Object.assign({}, styles.card), { textAlign: 'center' }), children: [_jsxs("div", { style: loginStyles.logoGroup, children: [_jsx(GenieeLogoMark, {}), _jsxs("div", { style: loginStyles.logoText, children: [_jsx("span", { style: loginStyles.logoGeniee, children: "GENIEE" }), _jsx("span", { style: loginStyles.logoProduct, children: "SFA/CRM" })] })] }), _jsx("h2", { style: styles.title, children: title }), _jsx("p", { style: styles.subtitle, children: message }), showReload && (_jsx("button", { type: "button", style: styles.reloadButton, onClick: handleReload, children: "\u30DA\u30FC\u30B8\u3092\u518D\u8AAD\u307F\u8FBC\u307F" })), _jsxs("div", { style: styles.versionFooter, children: ["@geniee-inc/sfa-kit v", SFA_KIT_VERSION] })] }) }));
|
|
199
|
+
}
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Standalone flow (login form + DevToolbar)
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
function StandaloneFlow({ apiBase, defaultOrganizationId, objectKey, level, children, }) {
|
|
204
|
+
const [session, setSession] = useState(() => loadSession());
|
|
205
|
+
const [activeBase, setActiveBase] = useState(() => { var _a; return (_a = session === null || session === void 0 ? void 0 : session.apiBase) !== null && _a !== void 0 ? _a : apiBase.replace(/\/+$/, ''); });
|
|
206
|
+
const [status, setStatus] = useState('idle');
|
|
207
|
+
const [error, setError] = useState(null);
|
|
208
|
+
// NOTE: level を渡さない既存ページのために、objectKey の有無から補う。
|
|
209
|
+
// 以前は objectKey があること自体が「record レベル」を意味していた
|
|
210
|
+
const resolvedLevel = level !== null && level !== void 0 ? level : (objectKey != null && objectKey !== '' ? 'record' : 'object');
|
|
211
|
+
// Preview context state managed by DevToolbar.
|
|
212
|
+
// When objectKey is provided, pre-populate it.
|
|
213
|
+
const [previewObjectKey, setPreviewObjectKey] = useState(objectKey !== null && objectKey !== void 0 ? objectKey : '');
|
|
214
|
+
const [previewRecordId, setPreviewRecordId] = useState('');
|
|
215
|
+
const [fetchGeneration, setFetchGeneration] = useState(0);
|
|
216
|
+
const triggerFetch = useCallback(() => {
|
|
217
|
+
setFetchGeneration(prev => prev + 1);
|
|
218
|
+
}, []);
|
|
219
|
+
const previewControls = useMemo(() => ({
|
|
220
|
+
state: previewObjectKey !== ''
|
|
221
|
+
? Object.assign({ object_key: previewObjectKey }, (previewRecordId !== '' ? { record_id: previewRecordId } : {})) : null,
|
|
222
|
+
setObjectKey: setPreviewObjectKey,
|
|
223
|
+
setRecordId: setPreviewRecordId,
|
|
224
|
+
triggerFetch,
|
|
225
|
+
fetchGeneration,
|
|
226
|
+
}), [previewObjectKey, previewRecordId, triggerFetch, fetchGeneration]);
|
|
227
|
+
const completeLogin = useCallback((newSession, normalizedBase) => {
|
|
228
|
+
const sessionWithBase = Object.assign(Object.assign({}, newSession), { apiBase: normalizedBase });
|
|
229
|
+
saveSession(sessionWithBase);
|
|
230
|
+
setSession(sessionWithBase);
|
|
231
|
+
setActiveBase(normalizedBase);
|
|
232
|
+
setStatus('idle');
|
|
233
|
+
}, []);
|
|
234
|
+
const handleLogin = useCallback((email_1, password_1, base_1, orgId_1, ...args_1) => __awaiter(this, [email_1, password_1, base_1, orgId_1, ...args_1], void 0, function* (email, password, base, orgId, isRetry = false) {
|
|
235
|
+
var _a, _b, _c;
|
|
236
|
+
setStatus('loading');
|
|
237
|
+
setError(null);
|
|
238
|
+
const normalizedBase = base.replace(/\/+$/, '');
|
|
239
|
+
try {
|
|
240
|
+
const result = yield authenticate(normalizedBase, email, password, orgId);
|
|
241
|
+
if (result.status === 'choose_organization') {
|
|
242
|
+
if (isRetry) {
|
|
243
|
+
// Prevent infinite recursion: if org selection already failed once,
|
|
244
|
+
// don't retry (the API returned 409 again despite an explicit orgId).
|
|
245
|
+
setError('組織の自動選択に失敗しました。別の組織をお試しください。');
|
|
246
|
+
setStatus('error');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// Auto-select priority:
|
|
250
|
+
// 1. User-entered orgId (already passed to authenticate, shouldn't 409)
|
|
251
|
+
// 2. defaultOrganizationId prop
|
|
252
|
+
// 3. Last successfully authenticated org (from localStorage)
|
|
253
|
+
// 4. First available org
|
|
254
|
+
const orgs = result.availableOrganizations;
|
|
255
|
+
const lastOrgIdStr = loadLastOrgId();
|
|
256
|
+
const lastOrgId = lastOrgIdStr != null ? parseInt(lastOrgIdStr, 10) : null;
|
|
257
|
+
const autoOrg = (_c = (_b = (_a = (orgId != null
|
|
258
|
+
? orgs.find(o => o.organization_id === orgId)
|
|
259
|
+
: undefined)) !== null && _a !== void 0 ? _a : (defaultOrganizationId != null
|
|
260
|
+
? orgs.find(o => o.organization_id === defaultOrganizationId)
|
|
261
|
+
: undefined)) !== null && _b !== void 0 ? _b : (lastOrgId != null
|
|
262
|
+
? orgs.find(o => o.organization_id === lastOrgId)
|
|
263
|
+
: undefined)) !== null && _c !== void 0 ? _c : orgs[0];
|
|
264
|
+
if (autoOrg) {
|
|
265
|
+
yield handleLogin(email, password, base, autoOrg.organization_id, true);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
setError('アカウントに利用可能な組織がありません');
|
|
269
|
+
setStatus('error');
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
completeLogin(result.session, normalizedBase);
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
setError(e instanceof Error ? e.message : 'ログインに失敗しました');
|
|
276
|
+
setStatus('error');
|
|
277
|
+
}
|
|
278
|
+
}), [completeLogin, defaultOrganizationId]);
|
|
279
|
+
const handleLogout = useCallback(() => {
|
|
280
|
+
clearSession();
|
|
281
|
+
setSession(null);
|
|
282
|
+
}, []);
|
|
283
|
+
const auth = useMemo(() => (session ? { accessToken: session.accessToken } : null), [session]);
|
|
284
|
+
if (!session || !auth) {
|
|
285
|
+
return (_jsx(LoginForm, { defaultApiBase: apiBase, defaultOrganizationId: defaultOrganizationId, onSubmit: handleLogin, status: status, error: error }));
|
|
286
|
+
}
|
|
287
|
+
return (_jsx(SfaProvider, { apiBase: activeBase, auth: auth, mode: "direct", organizationId: session.organizationId, onAuthError: handleLogout, children: _jsxs(StandalonePreviewCtx.Provider, { value: previewControls, children: [_jsx(DevToolbar, { organizationId: session.organizationId, preview: previewControls, objectKey: objectKey, level: resolvedLevel, onLogout: handleLogout }), children] }) }));
|
|
288
|
+
}
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// GENIEE SFA/CRM-branded login form
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
function LoginForm({ defaultApiBase, defaultOrganizationId, onSubmit, status, error, }) {
|
|
293
|
+
const [email, setEmail] = useState('');
|
|
294
|
+
const [password, setPassword] = useState('');
|
|
295
|
+
const [apiBase, setApiBase] = useState(defaultApiBase);
|
|
296
|
+
// Pre-fill with the org used at build time (metadata fetch), fall back to last login
|
|
297
|
+
const [orgIdInput, setOrgIdInput] = useState(() => {
|
|
298
|
+
var _a, _b;
|
|
299
|
+
return (_b = (_a = (defaultOrganizationId != null ? String(defaultOrganizationId) : null)) !== null && _a !== void 0 ? _a : loadLastOrgId()) !== null && _b !== void 0 ? _b : '';
|
|
300
|
+
});
|
|
301
|
+
const [showConfig, setShowConfig] = useState(false);
|
|
302
|
+
const handleSubmit = (e) => {
|
|
303
|
+
e.preventDefault();
|
|
304
|
+
if (!email.trim() || !password)
|
|
305
|
+
return;
|
|
306
|
+
const orgId = orgIdInput.trim()
|
|
307
|
+
? parseInt(orgIdInput.trim(), 10)
|
|
308
|
+
: undefined;
|
|
309
|
+
onSubmit(email.trim(), password, apiBase.trim(), Number.isFinite(orgId) ? orgId : undefined);
|
|
310
|
+
};
|
|
311
|
+
return (_jsx("div", { style: loginStyles.backdrop, children: _jsxs("form", { style: loginStyles.card, onSubmit: handleSubmit, children: [_jsxs("div", { style: loginStyles.logoGroup, children: [_jsx(GenieeLogoMark, {}), _jsxs("div", { style: loginStyles.logoText, children: [_jsx("span", { style: loginStyles.logoGeniee, children: "GENIEE" }), _jsx("span", { style: loginStyles.logoProduct, children: "SFA/CRM" })] })] }), error && (_jsx("div", { role: "alert", style: styles.error, children: error })), _jsxs("label", { style: loginStyles.label, children: [_jsx("span", { style: loginStyles.labelText, children: "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9*" }), _jsx("input", { style: loginStyles.input, type: "text", value: email, onChange: e => setEmail(e.target.value), autoComplete: "email", required: true, disabled: status === 'loading' })] }), _jsxs("label", { style: loginStyles.label, children: [_jsx("span", { style: loginStyles.labelText, children: "\u30D1\u30B9\u30EF\u30FC\u30C9*" }), _jsx("input", { style: loginStyles.input, type: "password", value: password, onChange: e => setPassword(e.target.value), autoComplete: "current-password", required: true, disabled: status === 'loading' })] }), _jsx("button", { style: Object.assign(Object.assign({}, loginStyles.submitButton), { opacity: status === 'loading' ? 0.7 : 1 }), type: "submit", disabled: status === 'loading', children: status === 'loading' ? 'ログイン中...' : 'ログイン' }), _jsx("button", { type: "button", style: loginStyles.configToggle, onClick: () => setShowConfig(prev => !prev), children: showConfig ? '▾ 接続設定を隠す' : '▸ 接続設定' }), showConfig && (_jsxs("div", { style: loginStyles.configSection, children: [_jsxs("label", { style: loginStyles.label, children: [_jsx("span", { style: loginStyles.configLabel, children: "API Base" }), _jsx("input", { style: loginStyles.input, type: "text", value: apiBase, onChange: e => setApiBase(e.target.value), placeholder: "https://keystone-api.chikyu.mobi", disabled: status === 'loading' })] }), _jsxs("label", { style: loginStyles.label, children: [_jsx("span", { style: loginStyles.configLabel, children: "Organization ID" }), _jsx("input", { style: loginStyles.input, type: "text", value: orgIdInput, onChange: e => setOrgIdInput(e.target.value), placeholder: "\u81EA\u52D5\u9078\u629E (\u7A7A\u6B04)", disabled: status === 'loading' })] })] })), _jsxs("div", { style: styles.versionFooter, children: ["@geniee-inc/sfa-kit v", SFA_KIT_VERSION] })] }) }));
|
|
312
|
+
}
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// Geniee logo mark (inline SVG)
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
function GenieeLogoMark() {
|
|
317
|
+
return (_jsxs("svg", { viewBox: "0 0 40 40", width: "40", height: "40", style: { flexShrink: 0 }, "aria-hidden": "true", children: [_jsx("circle", { cx: "20", cy: "20", r: "19", fill: "#2C3E82" }), _jsx("circle", { cx: "22", cy: "13", r: "4.5", fill: "#ffffff" }), _jsx("path", { d: "M27 19 C33 22 30 37 20 37 C13 37 11 29 14 26 C16 24 20 22 24 19 Z", fill: "#ffffff" })] }));
|
|
318
|
+
}
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// Styles
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
const GENIEE_DARK_BLUE = '#2C3E82';
|
|
323
|
+
const loginStyles = {
|
|
324
|
+
backdrop: Object.assign(Object.assign({}, baseContainerStyle), { display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', backgroundColor: '#f7f8fa' }),
|
|
325
|
+
card: {
|
|
326
|
+
width: '100%',
|
|
327
|
+
maxWidth: 440,
|
|
328
|
+
padding: `${space.xl + 8}px ${space.xl}px ${space.xl}px`,
|
|
329
|
+
backgroundColor: '#ffffff',
|
|
330
|
+
borderRadius: 8,
|
|
331
|
+
border: `1px solid ${color.border}`,
|
|
332
|
+
display: 'flex',
|
|
333
|
+
flexDirection: 'column',
|
|
334
|
+
gap: space.lg,
|
|
335
|
+
boxShadow: '0 2px 12px rgba(0,0,0,0.06)',
|
|
336
|
+
},
|
|
337
|
+
logoGroup: {
|
|
338
|
+
display: 'flex',
|
|
339
|
+
alignItems: 'center',
|
|
340
|
+
justifyContent: 'center',
|
|
341
|
+
gap: 12,
|
|
342
|
+
marginBottom: space.sm,
|
|
343
|
+
},
|
|
344
|
+
logoText: {
|
|
345
|
+
display: 'flex',
|
|
346
|
+
flexDirection: 'column',
|
|
347
|
+
},
|
|
348
|
+
logoGeniee: {
|
|
349
|
+
fontSize: 13,
|
|
350
|
+
fontWeight: 700,
|
|
351
|
+
color: GENIEE_DARK_BLUE,
|
|
352
|
+
letterSpacing: '0.08em',
|
|
353
|
+
lineHeight: 1,
|
|
354
|
+
},
|
|
355
|
+
logoProduct: {
|
|
356
|
+
fontSize: 28,
|
|
357
|
+
fontWeight: 700,
|
|
358
|
+
color: GENIEE_DARK_BLUE,
|
|
359
|
+
lineHeight: 1.1,
|
|
360
|
+
letterSpacing: '-0.01em',
|
|
361
|
+
},
|
|
362
|
+
label: {
|
|
363
|
+
display: 'flex',
|
|
364
|
+
flexDirection: 'column',
|
|
365
|
+
gap: 0,
|
|
366
|
+
position: 'relative',
|
|
367
|
+
},
|
|
368
|
+
labelText: {
|
|
369
|
+
fontSize: 12,
|
|
370
|
+
fontWeight: 400,
|
|
371
|
+
color: color.textSecondary,
|
|
372
|
+
position: 'absolute',
|
|
373
|
+
top: -8,
|
|
374
|
+
left: 12,
|
|
375
|
+
backgroundColor: '#ffffff',
|
|
376
|
+
padding: '0 4px',
|
|
377
|
+
zIndex: 1,
|
|
378
|
+
},
|
|
379
|
+
input: {
|
|
380
|
+
padding: '14px 14px',
|
|
381
|
+
border: `1px solid ${color.border}`,
|
|
382
|
+
borderRadius: 4,
|
|
383
|
+
fontSize: 16,
|
|
384
|
+
color: color.textPrimary,
|
|
385
|
+
backgroundColor: '#ffffff',
|
|
386
|
+
fontFamily: font.family,
|
|
387
|
+
outline: 'none',
|
|
388
|
+
transition: 'border-color 0.15s',
|
|
389
|
+
lineHeight: '22px',
|
|
390
|
+
},
|
|
391
|
+
submitButton: Object.assign(Object.assign({}, baseButtonStyle), { padding: '14px 16px', backgroundColor: GENIEE_DARK_BLUE, color: '#ffffff', border: 'none', fontSize: 16, fontWeight: 700, borderRadius: 4, marginTop: space.xs }),
|
|
392
|
+
configToggle: {
|
|
393
|
+
background: 'none',
|
|
394
|
+
border: 'none',
|
|
395
|
+
color: color.textSecondary,
|
|
396
|
+
fontSize: font.sizeSm,
|
|
397
|
+
cursor: 'pointer',
|
|
398
|
+
textAlign: 'left',
|
|
399
|
+
padding: 0,
|
|
400
|
+
fontFamily: font.family,
|
|
401
|
+
},
|
|
402
|
+
configSection: {
|
|
403
|
+
display: 'flex',
|
|
404
|
+
flexDirection: 'column',
|
|
405
|
+
gap: space.md,
|
|
406
|
+
padding: space.sm,
|
|
407
|
+
backgroundColor: color.bgDefault,
|
|
408
|
+
borderRadius: radius.sm,
|
|
409
|
+
border: `1px solid ${color.border}`,
|
|
410
|
+
},
|
|
411
|
+
configLabel: {
|
|
412
|
+
fontSize: 11,
|
|
413
|
+
fontWeight: 400,
|
|
414
|
+
color: color.textSecondary,
|
|
415
|
+
position: 'absolute',
|
|
416
|
+
top: -8,
|
|
417
|
+
left: 12,
|
|
418
|
+
backgroundColor: color.bgDefault,
|
|
419
|
+
padding: '0 4px',
|
|
420
|
+
zIndex: 1,
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
const styles = {
|
|
424
|
+
backdrop: Object.assign(Object.assign({}, baseContainerStyle), { display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', backgroundColor: color.bgDefault }),
|
|
425
|
+
card: {
|
|
426
|
+
width: '100%',
|
|
427
|
+
maxWidth: 420,
|
|
428
|
+
padding: space.xl,
|
|
429
|
+
backgroundColor: color.bgSurface,
|
|
430
|
+
borderRadius: radius.sm,
|
|
431
|
+
border: `1px solid ${color.border}`,
|
|
432
|
+
display: 'flex',
|
|
433
|
+
flexDirection: 'column',
|
|
434
|
+
gap: space.md,
|
|
435
|
+
},
|
|
436
|
+
title: {
|
|
437
|
+
fontSize: 20,
|
|
438
|
+
fontWeight: font.weightMedium,
|
|
439
|
+
margin: 0,
|
|
440
|
+
color: color.textPrimary,
|
|
441
|
+
},
|
|
442
|
+
subtitle: {
|
|
443
|
+
fontSize: font.sizeMd,
|
|
444
|
+
color: color.textSecondary,
|
|
445
|
+
margin: 0,
|
|
446
|
+
},
|
|
447
|
+
error: {
|
|
448
|
+
padding: `10px ${space.sm}px`,
|
|
449
|
+
backgroundColor: color.errorLight,
|
|
450
|
+
border: `1px solid ${color.error}`,
|
|
451
|
+
borderRadius: radius.sm,
|
|
452
|
+
color: color.error,
|
|
453
|
+
fontSize: font.sizeMd,
|
|
454
|
+
},
|
|
455
|
+
reloadButton: Object.assign(Object.assign({}, baseButtonStyle), { padding: '12px 24px', backgroundColor: GENIEE_DARK_BLUE, color: '#ffffff', border: 'none', fontSize: font.sizeMd, fontWeight: font.weightMedium, borderRadius: 4 }),
|
|
456
|
+
versionFooter: {
|
|
457
|
+
textAlign: 'center',
|
|
458
|
+
fontSize: font.sizeXs,
|
|
459
|
+
color: color.textMuted,
|
|
460
|
+
marginTop: space.xs,
|
|
461
|
+
},
|
|
462
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description State managed by the DevToolbar in standalone (non-embedded) mode.
|
|
3
|
+
* Mirrors the shape of EmbedContext so useEmbedContext() can fall back to it.
|
|
4
|
+
*/
|
|
5
|
+
export interface StandalonePreviewState {
|
|
6
|
+
readonly object_key: string;
|
|
7
|
+
readonly record_id?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface StandalonePreviewControls {
|
|
10
|
+
readonly state: StandalonePreviewState | null;
|
|
11
|
+
readonly setObjectKey: (key: string) => void;
|
|
12
|
+
readonly setRecordId: (id: string) => void;
|
|
13
|
+
readonly triggerFetch: () => void;
|
|
14
|
+
/** Increments on each triggerFetch call so consumers can react. */
|
|
15
|
+
readonly fetchGeneration: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* @description React context carrying standalone preview state.
|
|
19
|
+
* Populated only in standalone mode by StandaloneFlow's DevToolbar.
|
|
20
|
+
* useEmbedContext() checks this as a fallback when EmbedContext is null.
|
|
21
|
+
*/
|
|
22
|
+
export declare const StandalonePreviewCtx: import("react").Context<StandalonePreviewControls | null>;
|
|
23
|
+
/**
|
|
24
|
+
* @description Hook to access standalone preview controls from the DevToolbar.
|
|
25
|
+
* Returns null when not in standalone mode (i.e. when embedded).
|
|
26
|
+
*/
|
|
27
|
+
export declare function useStandalonePreview(): StandalonePreviewControls | null;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* @description React context carrying standalone preview state.
|
|
4
|
+
* Populated only in standalone mode by StandaloneFlow's DevToolbar.
|
|
5
|
+
* useEmbedContext() checks this as a fallback when EmbedContext is null.
|
|
6
|
+
*/
|
|
7
|
+
export const StandalonePreviewCtx = createContext(null);
|
|
8
|
+
/**
|
|
9
|
+
* @description Hook to access standalone preview controls from the DevToolbar.
|
|
10
|
+
* Returns null when not in standalone mode (i.e. when embedded).
|
|
11
|
+
*/
|
|
12
|
+
export function useStandalonePreview() {
|
|
13
|
+
return useContext(StandalonePreviewCtx);
|
|
14
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description postMessage protocol between the SFA/CRM host and
|
|
3
|
+
* an embedded sfa-kit iframe. Both sides MUST validate origins:
|
|
4
|
+
* the sender uses an explicit targetOrigin, the receiver checks event.origin.
|
|
5
|
+
*/
|
|
6
|
+
/** Sent by the iframe after its message listener is registered. */
|
|
7
|
+
export interface EmbedReadyMessage {
|
|
8
|
+
readonly type: 'sfa-kit:ready';
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* @description Runtime context injected by the SFA/CRM host alongside the
|
|
12
|
+
* exchange code. Tells the embedded page which object and (for record-level
|
|
13
|
+
* pages) which record it should display.
|
|
14
|
+
*/
|
|
15
|
+
export interface EmbedContext {
|
|
16
|
+
readonly object_key: string;
|
|
17
|
+
readonly record_id?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Sent by the host to deliver a single-use exchange code. */
|
|
20
|
+
export interface EmbedCodeMessage {
|
|
21
|
+
readonly type: 'sfa-kit:code';
|
|
22
|
+
readonly code: string;
|
|
23
|
+
readonly context?: EmbedContext;
|
|
24
|
+
}
|
|
25
|
+
/** Sent by the iframe to request a fresh code (pre-expiry refresh or 401 fallback). */
|
|
26
|
+
export interface EmbedCodeRequestMessage {
|
|
27
|
+
readonly type: 'sfa-kit:code-request';
|
|
28
|
+
}
|
|
29
|
+
export type EmbedInboundMessage = EmbedCodeMessage;
|
|
30
|
+
export type EmbedOutboundMessage = EmbedReadyMessage | EmbedCodeRequestMessage;
|
|
31
|
+
/**
|
|
32
|
+
* @description Type-guard for messages arriving from the host.
|
|
33
|
+
* Returns the typed message only when both the shape and the origin match.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseInboundMessage(event: MessageEvent, trustedOrigin: string): EmbedInboundMessage | null;
|
|
36
|
+
/**
|
|
37
|
+
* @description Send a typed message to the host frame with an explicit targetOrigin.
|
|
38
|
+
* Never uses '*' as targetOrigin.
|
|
39
|
+
*/
|
|
40
|
+
export declare function sendToHost(message: EmbedOutboundMessage, targetOrigin: string): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description postMessage protocol between the SFA/CRM host and
|
|
3
|
+
* an embedded sfa-kit iframe. Both sides MUST validate origins:
|
|
4
|
+
* the sender uses an explicit targetOrigin, the receiver checks event.origin.
|
|
5
|
+
*/
|
|
6
|
+
const VALID_INBOUND_TYPES = new Set(['sfa-kit:code']);
|
|
7
|
+
/**
|
|
8
|
+
* @description Type-guard for messages arriving from the host.
|
|
9
|
+
* Returns the typed message only when both the shape and the origin match.
|
|
10
|
+
*/
|
|
11
|
+
export function parseInboundMessage(event, trustedOrigin) {
|
|
12
|
+
if (event.origin !== trustedOrigin)
|
|
13
|
+
return null;
|
|
14
|
+
const data = event.data;
|
|
15
|
+
if (typeof data !== 'object' || data == null)
|
|
16
|
+
return null;
|
|
17
|
+
const candidate = data;
|
|
18
|
+
if (typeof candidate['type'] !== 'string')
|
|
19
|
+
return null;
|
|
20
|
+
if (!VALID_INBOUND_TYPES.has(candidate['type']))
|
|
21
|
+
return null;
|
|
22
|
+
if (candidate['type'] === 'sfa-kit:code') {
|
|
23
|
+
if (typeof candidate['code'] !== 'string')
|
|
24
|
+
return null;
|
|
25
|
+
if (candidate['code'].length === 0)
|
|
26
|
+
return null;
|
|
27
|
+
const result = {
|
|
28
|
+
type: 'sfa-kit:code',
|
|
29
|
+
code: candidate['code'],
|
|
30
|
+
};
|
|
31
|
+
const rawContext = candidate['context'];
|
|
32
|
+
if (typeof rawContext === 'object' && rawContext != null) {
|
|
33
|
+
const ctx = rawContext;
|
|
34
|
+
if (typeof ctx['object_key'] === 'string' &&
|
|
35
|
+
ctx['object_key'].length > 0) {
|
|
36
|
+
const context = { object_key: ctx['object_key'] };
|
|
37
|
+
if (typeof ctx['record_id'] === 'string' &&
|
|
38
|
+
ctx['record_id'].length > 0) {
|
|
39
|
+
return Object.assign(Object.assign({}, result), { context: Object.assign(Object.assign({}, context), { record_id: ctx['record_id'] }) });
|
|
40
|
+
}
|
|
41
|
+
return Object.assign(Object.assign({}, result), { context });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* @description Send a typed message to the host frame with an explicit targetOrigin.
|
|
50
|
+
* Never uses '*' as targetOrigin.
|
|
51
|
+
*/
|
|
52
|
+
export function sendToHost(message, targetOrigin) {
|
|
53
|
+
if (!window.parent || window.parent === window)
|
|
54
|
+
return;
|
|
55
|
+
window.parent.postMessage(message, targetOrigin);
|
|
56
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SfaSession } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* @description Exchange a single-use code for a Keystone JWT via POST /v1/auth/exchange.
|
|
4
|
+
* The code is consumed on verification regardless of outcome. On any failure,
|
|
5
|
+
* obtain a fresh code from the host rather than retrying the same one.
|
|
6
|
+
*/
|
|
7
|
+
export declare function exchangeCode(apiBase: string, code: string): Promise<SfaSession>;
|
|
8
|
+
/**
|
|
9
|
+
* @description Error from the exchange endpoint with the HTTP status preserved.
|
|
10
|
+
*/
|
|
11
|
+
export declare class ExchangeError extends Error {
|
|
12
|
+
readonly httpStatus: number;
|
|
13
|
+
constructor(message: string, httpStatus: number);
|
|
14
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { __awaiter } from "tslib";
|
|
2
|
+
import { authTimeoutSignal, decodeJwtPayload } from './keystone-auth';
|
|
3
|
+
/**
|
|
4
|
+
* @description Exchange a single-use code for a Keystone JWT via POST /v1/auth/exchange.
|
|
5
|
+
* The code is consumed on verification regardless of outcome. On any failure,
|
|
6
|
+
* obtain a fresh code from the host rather than retrying the same one.
|
|
7
|
+
*/
|
|
8
|
+
export function exchangeCode(apiBase, code) {
|
|
9
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
10
|
+
var _a;
|
|
11
|
+
const response = yield globalThis.fetch(`${apiBase}/v1/auth/exchange`, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: { 'Content-Type': 'application/json' },
|
|
14
|
+
body: JSON.stringify({ code }),
|
|
15
|
+
signal: authTimeoutSignal(),
|
|
16
|
+
});
|
|
17
|
+
if (response.status === 503) {
|
|
18
|
+
throw new ExchangeError('SFA/CRM is temporarily unavailable. A new code will be requested.', 503);
|
|
19
|
+
}
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
const text = yield response.text().catch(() => '');
|
|
22
|
+
throw new ExchangeError(`Exchange failed: ${response.status} ${response.statusText}${text ? ` — ${text}` : ''}`, response.status);
|
|
23
|
+
}
|
|
24
|
+
const data = (yield response.json());
|
|
25
|
+
if (!data.access_token || typeof data.expires_in !== 'number') {
|
|
26
|
+
throw new ExchangeError('Exchange response missing access_token or expires_in', 0);
|
|
27
|
+
}
|
|
28
|
+
const claims = decodeJwtPayload(data.access_token);
|
|
29
|
+
return {
|
|
30
|
+
accessToken: data.access_token,
|
|
31
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
32
|
+
organizationId: String((_a = claims['organization_id']) !== null && _a !== void 0 ? _a : ''),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* @description Error from the exchange endpoint with the HTTP status preserved.
|
|
38
|
+
*/
|
|
39
|
+
export class ExchangeError extends Error {
|
|
40
|
+
constructor(message, httpStatus) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = 'ExchangeError';
|
|
43
|
+
this.httpStatus = httpStatus;
|
|
44
|
+
}
|
|
45
|
+
}
|