@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.10.0'
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 default API endpoint (`https://keystone-api-staging.chikyu.mobi`).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geniee-inc/sfa-kit",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "React hooks and components for building pages that connect to GENIEE SFA/CRM data.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -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 = DEFAULT_API_BASE, trustedOrigin, defaultOrganizationId, objectKey, level, children, }) {
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
- return (_jsx(EmbedFlow, { trustedOrigin: resolvedOrigin, apiBase: apiBase, children: children }));
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
- return (_jsx(StandaloneFlow, { apiBase: apiBase, defaultOrganizationId: defaultOrganizationId, objectKey: objectKey, level: level, children: children }));
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 [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(/\/+$/, ''); });
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: "https://keystone-api-staging.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] })] }) }));
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
+ }
@@ -1 +1 @@
1
- export declare const SFA_KIT_VERSION = "1.13.0";
1
+ export declare const SFA_KIT_VERSION = "1.14.0";
@@ -1,3 +1,3 @@
1
1
  // このファイルは libs/sfa-kit/scripts/sync-version.cjs が生成する。
2
2
  // 手で編集しないこと。バージョンを上げるときは libs/sfa-kit/package.json を変更する。
3
- export const SFA_KIT_VERSION = '1.13.0';
3
+ export const SFA_KIT_VERSION = '1.14.0';