@geniee-inc/sfa-kit 1.13.0 → 1.14.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
CHANGED
|
@@ -18,7 +18,7 @@ The package ships built JavaScript and TypeScript declarations only. It contains
|
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
20
|
import pkg from '@geniee-inc/sfa-kit/package.json';
|
|
21
|
-
// pkg.version -> '1.
|
|
21
|
+
// pkg.version -> '1.14.0'
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
Two additional subpaths are available: `@geniee-inc/sfa-kit/signing` exposes bundle-signing utilities (`computeContentHash`, `computeSignature`, `createManifest`, `createManifestFromDirectory`) for Node.js environments, and `./package.json` exposes the manifest. Internal modules cannot be imported directly.
|
|
@@ -38,7 +38,7 @@ Every page uses `SfaLoginGate` as the outermost wrapper. It auto-detects the hos
|
|
|
38
38
|
|
|
39
39
|
### Props
|
|
40
40
|
|
|
41
|
-
- **`apiBase`** (optional) — Overrides the
|
|
41
|
+
- **`apiBase`** (optional) — Overrides the auto-detected Keystone API endpoint. When omitted, the endpoint is resolved from a hostname: the embedding SFA/CRM host's when the page runs inside the iframe, the page's own when it runs standalone. Non-production environments (`stg.chikyu.mobi`, `dev01`-`dev09`, `devdc`, `deva0`, `dev-docker`, `dev.chikyu.local`, `localhost`, `127.0.0.1`, `[::1]`, and every Amplify host of the new frontend under `*.amplify.axiom.chikyu.mobi`: PR previews plus the `devNN.waf.` / `stg.waf.` record-detail hosts) use `https://keystone-api-staging.chikyu.mobi`; everything else (`app.chikyu.net`, any hostname not in that list) uses `https://keystone-api-production.chikyu.mobi`. Production is the fallback on purpose so a page that reaches a customer never silently talks to staging.
|
|
42
42
|
- **`trustedOrigin`** (optional) — The origin of the SFA/CRM host page (e.g. `"https://app.chikyu.net"`). When provided and the page runs inside an iframe, `SfaLoginGate` uses the postMessage code-exchange flow (`POST /v1/auth/exchange`) instead of a login form. Injected automatically by Keystone's content endpoint at serve time.
|
|
43
43
|
- **`defaultOrganizationId`** (optional) — The org ID to auto-select when the user's account belongs to multiple organizations. **Set this to the org that generated the page's metadata** (the org used in Step 0 schema discovery). Without this prop, multi-org accounts auto-select the first available org, which may not be the intended one. Use an env var, not a literal: `defaultOrganizationId={Number(process.env.NEXT_PUBLIC_DEFAULT_ORG_ID)}`.
|
|
44
44
|
- **`objectKey`** (optional) — The object this page is built for (e.g. `"companies"`). The standalone DevToolbar shows it in the page-info tooltip and in the record ID placeholder. Pass it at **both** levels; omit it only if the page genuinely has no object.
|
package/package.json
CHANGED
|
@@ -8,10 +8,10 @@ import { DevToolbar } from './DevToolbar';
|
|
|
8
8
|
import { parseInboundMessage, sendToHost } from './embed-protocol';
|
|
9
9
|
import { exchangeCode } from './exchange-code';
|
|
10
10
|
import { authenticate } from './keystone-auth';
|
|
11
|
+
import { KEYSTONE_PRODUCTION_API_BASE, resolveApiBase, } from './resolve-api-base';
|
|
11
12
|
import { loadSession, saveSession, clearSession, loadLastOrgId, } from './session-store';
|
|
12
13
|
import { StandalonePreviewCtx } from './StandalonePreviewContext';
|
|
13
14
|
import { EmbedContextValue } from './useEmbedContext';
|
|
14
|
-
const DEFAULT_API_BASE = 'https://keystone-api-staging.chikyu.mobi';
|
|
15
15
|
const REFRESH_BUFFER_SECONDS = 120;
|
|
16
16
|
/** After this many consecutive exchange failures, stop retrying and show session_expired. */
|
|
17
17
|
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
@@ -62,6 +62,21 @@ function resolveTrustedOrigin(explicitOrigin) {
|
|
|
62
62
|
}
|
|
63
63
|
return undefined;
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* @description The origin this page is being served from.
|
|
67
|
+
*
|
|
68
|
+
* Used to pick the Keystone API when the page runs standalone: a preview
|
|
69
|
+
* opened directly, or a Studio page on a developer's machine, has no
|
|
70
|
+
* embedding host to judge by, so its own hostname is the only signal.
|
|
71
|
+
*/
|
|
72
|
+
function resolvePageOrigin() {
|
|
73
|
+
try {
|
|
74
|
+
return window.location.origin;
|
|
75
|
+
}
|
|
76
|
+
catch (_a) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
65
80
|
/**
|
|
66
81
|
* @description Universal auth wrapper for sfa-kit pages.
|
|
67
82
|
*
|
|
@@ -75,13 +90,15 @@ function resolveTrustedOrigin(explicitOrigin) {
|
|
|
75
90
|
* automatically from the API (auto-selected for multi-org accounts when
|
|
76
91
|
* defaultOrganizationId is set, or the first org is used by default).
|
|
77
92
|
*/
|
|
78
|
-
export function SfaLoginGate({ apiBase
|
|
93
|
+
export function SfaLoginGate({ apiBase, trustedOrigin, defaultOrganizationId, objectKey, level, children, }) {
|
|
79
94
|
const shouldEmbed = isEmbedded() && (trustedOrigin != null || hasContentToken());
|
|
80
95
|
const resolvedOrigin = resolveTrustedOrigin(trustedOrigin);
|
|
81
96
|
if (shouldEmbed && resolvedOrigin != null) {
|
|
82
|
-
|
|
97
|
+
// Embedded: the SFA/CRM host that framed us decides the environment.
|
|
98
|
+
return (_jsx(EmbedFlow, { trustedOrigin: resolvedOrigin, apiBase: apiBase !== null && apiBase !== void 0 ? apiBase : resolveApiBase(resolvedOrigin), children: children }));
|
|
83
99
|
}
|
|
84
|
-
|
|
100
|
+
// Standalone: there is no host, so the page's own origin decides.
|
|
101
|
+
return (_jsx(StandaloneFlow, { apiBase: apiBase !== null && apiBase !== void 0 ? apiBase : resolveApiBase(resolvePageOrigin()), defaultOrganizationId: defaultOrganizationId, objectKey: objectKey, level: level, children: children }));
|
|
85
102
|
}
|
|
86
103
|
function EmbedFlow({ trustedOrigin, apiBase, children, }) {
|
|
87
104
|
const [phase, setPhase] = useState({ step: 'waiting_for_code' });
|
|
@@ -201,8 +218,19 @@ function EmbedStatus({ title, message, showReload, }) {
|
|
|
201
218
|
// Standalone flow (login form + DevToolbar)
|
|
202
219
|
// ---------------------------------------------------------------------------
|
|
203
220
|
function StandaloneFlow({ apiBase, defaultOrganizationId, objectKey, level, children, }) {
|
|
204
|
-
const
|
|
205
|
-
const [
|
|
221
|
+
const normalizedBase = useMemo(() => apiBase.replace(/\/+$/, ''), [apiBase]);
|
|
222
|
+
const [session, setSession] = useState(() => {
|
|
223
|
+
const saved = loadSession();
|
|
224
|
+
// Discard a saved session whose apiBase does not match the current
|
|
225
|
+
// environment. Without this check a staging session could be reused
|
|
226
|
+
// when the page is opened from a production host (or vice-versa).
|
|
227
|
+
if ((saved === null || saved === void 0 ? void 0 : saved.apiBase) != null && saved.apiBase !== normalizedBase) {
|
|
228
|
+
clearSession();
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
return saved;
|
|
232
|
+
});
|
|
233
|
+
const [activeBase, setActiveBase] = useState(() => { var _a; return (_a = session === null || session === void 0 ? void 0 : session.apiBase) !== null && _a !== void 0 ? _a : normalizedBase; });
|
|
206
234
|
const [status, setStatus] = useState('idle');
|
|
207
235
|
const [error, setError] = useState(null);
|
|
208
236
|
// NOTE: level を渡さない既存ページのために、objectKey の有無から補う。
|
|
@@ -308,7 +336,7 @@ function LoginForm({ defaultApiBase, defaultOrganizationId, onSubmit, status, er
|
|
|
308
336
|
: undefined;
|
|
309
337
|
onSubmit(email.trim(), password, apiBase.trim(), Number.isFinite(orgId) ? orgId : undefined);
|
|
310
338
|
};
|
|
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:
|
|
339
|
+
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: KEYSTONE_PRODUCTION_API_BASE, 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
340
|
}
|
|
313
341
|
// ---------------------------------------------------------------------------
|
|
314
342
|
// Geniee logo mark (inline SVG)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const KEYSTONE_PRODUCTION_API_BASE = "https://keystone-api-production.chikyu.mobi";
|
|
2
|
+
export declare const KEYSTONE_STAGING_API_BASE = "https://keystone-api-staging.chikyu.mobi";
|
|
3
|
+
/**
|
|
4
|
+
* @description Resolves the Keystone API base URL from an origin.
|
|
5
|
+
*
|
|
6
|
+
* Every non-production chikyu environment — `stg.chikyu.mobi`, the dev tier,
|
|
7
|
+
* local development, and Amplify PR previews — uses the staging Keystone API.
|
|
8
|
+
* Everything else — `app.chikyu.net`, an origin this table does not know, or
|
|
9
|
+
* no origin at all — uses production.
|
|
10
|
+
*
|
|
11
|
+
* Production is the fallback on purpose: a page that reaches a real customer
|
|
12
|
+
* must never silently talk to staging. A developer on an unrecognised host
|
|
13
|
+
* can still pass `apiBase` explicitly or type it into the standalone login form.
|
|
14
|
+
*
|
|
15
|
+
* Callers decide which origin to pass: the embedding host's origin when the
|
|
16
|
+
* page runs inside the SFA/CRM iframe, and the page's own origin when it runs
|
|
17
|
+
* standalone. A malformed origin is treated the same as no origin.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveApiBase(origin: string | undefined): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const KEYSTONE_PRODUCTION_API_BASE = 'https://keystone-api-production.chikyu.mobi';
|
|
2
|
+
export const KEYSTONE_STAGING_API_BASE = 'https://keystone-api-staging.chikyu.mobi';
|
|
3
|
+
/** Dev-tier chikyu (legacy frontend) hosts: dev01-dev09, devdc, deva0, and the like. */
|
|
4
|
+
const DEV_HOST_PATTERN = /^dev\w+\.chikyu\.mobi$/;
|
|
5
|
+
/**
|
|
6
|
+
* Amplify-hosted new frontend. Covers PR previews
|
|
7
|
+
* (`pr-{id}.amplify.axiom.chikyu.mobi`, see amplify.yml) AND the dev / stg
|
|
8
|
+
* record-detail hosts that frame sfa-kit pages
|
|
9
|
+
* (`dev01.waf.amplify.axiom.chikyu.mobi`, `stg.waf.amplify.axiom.chikyu.mobi`).
|
|
10
|
+
* Keep this a suffix match: narrowing it to `pr-` would send every dev
|
|
11
|
+
* environment to production.
|
|
12
|
+
*/
|
|
13
|
+
const AMPLIFY_PATTERN = /\.amplify\.axiom\.chikyu\.mobi$/;
|
|
14
|
+
/** Hostnames that are always a developer's own machine. */
|
|
15
|
+
const LOCAL_HOSTNAMES = new Set([
|
|
16
|
+
'localhost',
|
|
17
|
+
'127.0.0.1',
|
|
18
|
+
'[::1]',
|
|
19
|
+
'dev.chikyu.local',
|
|
20
|
+
'dev-docker.chikyu.mobi',
|
|
21
|
+
]);
|
|
22
|
+
/** Shared pre-production chikyu environments. */
|
|
23
|
+
const STAGING_HOSTNAMES = new Set(['stg.chikyu.mobi']);
|
|
24
|
+
/**
|
|
25
|
+
* @description Resolves the Keystone API base URL from an origin.
|
|
26
|
+
*
|
|
27
|
+
* Every non-production chikyu environment — `stg.chikyu.mobi`, the dev tier,
|
|
28
|
+
* local development, and Amplify PR previews — uses the staging Keystone API.
|
|
29
|
+
* Everything else — `app.chikyu.net`, an origin this table does not know, or
|
|
30
|
+
* no origin at all — uses production.
|
|
31
|
+
*
|
|
32
|
+
* Production is the fallback on purpose: a page that reaches a real customer
|
|
33
|
+
* must never silently talk to staging. A developer on an unrecognised host
|
|
34
|
+
* can still pass `apiBase` explicitly or type it into the standalone login form.
|
|
35
|
+
*
|
|
36
|
+
* Callers decide which origin to pass: the embedding host's origin when the
|
|
37
|
+
* page runs inside the SFA/CRM iframe, and the page's own origin when it runs
|
|
38
|
+
* standalone. A malformed origin is treated the same as no origin.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveApiBase(origin) {
|
|
41
|
+
const hostname = hostnameOf(origin);
|
|
42
|
+
if (hostname == null)
|
|
43
|
+
return KEYSTONE_PRODUCTION_API_BASE;
|
|
44
|
+
const isNonProduction = STAGING_HOSTNAMES.has(hostname) ||
|
|
45
|
+
LOCAL_HOSTNAMES.has(hostname) ||
|
|
46
|
+
DEV_HOST_PATTERN.test(hostname) ||
|
|
47
|
+
AMPLIFY_PATTERN.test(hostname);
|
|
48
|
+
return isNonProduction
|
|
49
|
+
? KEYSTONE_STAGING_API_BASE
|
|
50
|
+
: KEYSTONE_PRODUCTION_API_BASE;
|
|
51
|
+
}
|
|
52
|
+
/** The hostname of an origin, or undefined when there is no usable origin. */
|
|
53
|
+
function hostnameOf(origin) {
|
|
54
|
+
if (origin == null)
|
|
55
|
+
return undefined;
|
|
56
|
+
try {
|
|
57
|
+
// WHATWG URL preserves a trailing dot on FQDN hostnames (e.g.
|
|
58
|
+
// "stg.chikyu.mobi." stays as-is). Strip it so the allowlist and
|
|
59
|
+
// regex checks match regardless of whether the dot is present.
|
|
60
|
+
return new URL(origin).hostname.replace(/\.$/, '');
|
|
61
|
+
}
|
|
62
|
+
catch (_a) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SFA_KIT_VERSION = "1.
|
|
1
|
+
export declare const SFA_KIT_VERSION = "1.14.0";
|
package/src/lib/version.js
CHANGED