@geniee-inc/sfa-kit 1.14.1 → 1.17.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 (43) hide show
  1. package/GUIDE.md +131 -12
  2. package/package.json +19 -2
  3. package/src/bin/sfa-kit-bundle.d.ts +13 -0
  4. package/src/bin/sfa-kit-bundle.js +142 -0
  5. package/src/bundle.d.ts +26 -0
  6. package/src/bundle.js +1 -0
  7. package/src/lib/auth/ObjectKeyGuard.d.ts +12 -0
  8. package/src/lib/auth/ObjectKeyGuard.js +61 -0
  9. package/src/lib/auth/SfaLoginGate.js +5 -4
  10. package/src/lib/bundle/build-static-bundle.d.ts +45 -0
  11. package/src/lib/bundle/build-static-bundle.js +263 -0
  12. package/src/lib/bundle/esbuild-loader.d.ts +25 -0
  13. package/src/lib/bundle/esbuild-loader.js +27 -0
  14. package/src/lib/bundle/index.d.ts +2 -0
  15. package/src/lib/bundle/index.js +1 -0
  16. package/src/lib/bundle/request-signature.d.ts +22 -0
  17. package/src/lib/bundle/request-signature.js +81 -0
  18. package/src/lib/hooks/splitBulkRows.d.ts +8 -4
  19. package/src/lib/hooks/splitBulkRows.js +17 -4
  20. package/src/lib/hooks/useBulkCreateRecords.d.ts +8 -3
  21. package/src/lib/hooks/useBulkCreateRecords.js +5 -2
  22. package/src/lib/hooks/useBulkDeleteRecords.d.ts +14 -1
  23. package/src/lib/hooks/useBulkDeleteRecords.js +4 -0
  24. package/src/lib/hooks/useBulkUpdateRecords.d.ts +8 -2
  25. package/src/lib/hooks/useBulkUpdateRecords.js +3 -1
  26. package/src/lib/provider/SfaClient.d.ts +1 -0
  27. package/src/lib/provider/SfaClient.js +18 -5
  28. package/src/lib/provider/types.d.ts +19 -3
  29. package/src/lib/signing/compute-content-hash.d.ts +1 -1
  30. package/src/lib/signing/compute-signature.d.ts +17 -0
  31. package/src/lib/signing/compute-signature.js +17 -0
  32. package/src/lib/signing/create-manifest-from-directory.d.ts +13 -3
  33. package/src/lib/signing/create-manifest-from-directory.js +15 -28
  34. package/src/lib/signing/create-manifest.d.ts +7 -1
  35. package/src/lib/signing/create-manifest.js +8 -2
  36. package/src/lib/signing/index.d.ts +6 -5
  37. package/src/lib/signing/index.js +5 -4
  38. package/src/lib/signing/read-bundle-files.d.ts +19 -0
  39. package/src/lib/signing/read-bundle-files.js +41 -0
  40. package/src/lib/version.d.ts +1 -1
  41. package/src/lib/version.js +1 -1
  42. package/src/signing.d.ts +9 -4
  43. package/src/signing.js +1 -1
package/GUIDE.md CHANGED
@@ -21,7 +21,97 @@ import pkg from '@geniee-inc/sfa-kit/package.json';
21
21
  // pkg.version -> '1.14.0'
22
22
  ```
23
23
 
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.
24
+ Three additional subpaths are available: `@geniee-inc/sfa-kit/signing` exposes bundle-manifest utilities, `@geniee-inc/sfa-kit/bundle` builds and signs a static bundle in one call (both Node.js-only), and `./package.json` exposes the manifest. Internal modules cannot be imported directly.
25
+
26
+ ### Static bundle manifest (custom-tab uploads)
27
+
28
+ A page uploaded through GENIEE's SFA/CRM settings UI as a custom tab is a framework-free static bundle — `index.html` + `manifest.json` + `assets/` — not a Next.js app. Its `manifest.json` carries the three fields Keystone's `BundleSignatureVerifier` reads: `studio_version`, `content_hash`, `signature`.
29
+
30
+ **The signing key lives only in Keystone, so a page build never computes the signature itself.** It hashes the bundle locally and asks Keystone to sign:
31
+
32
+ ```ts
33
+ import {
34
+ readBundleFiles,
35
+ computeContentHash,
36
+ } from '@geniee-inc/sfa-kit/signing';
37
+ import type { BundleManifest } from '@geniee-inc/sfa-kit/signing';
38
+
39
+ const files = await readBundleFiles('dist');
40
+ const contentHash = computeContentHash(files);
41
+
42
+ // Keystone holds the signing key, so ask it for the signature. The Bearer token
43
+ // is the same one the rest of the API is called with.
44
+ const response = await fetch(`${apiBase}/v1/pages/sign`, {
45
+ method: 'POST',
46
+ headers: {
47
+ 'Content-Type': 'application/json',
48
+ Authorization: `Bearer ${accessToken}`,
49
+ },
50
+ body: JSON.stringify({
51
+ studio_version: studioVersion,
52
+ content_hash: contentHash,
53
+ }),
54
+ });
55
+ if (!response.ok) throw new Error(`Signing failed: ${response.status}`);
56
+ const { signature } = (await response.json()) as { signature: string };
57
+
58
+ const manifest: BundleManifest = {
59
+ studio_version: studioVersion, // the Studio build version, NOT the sfa-kit npm version
60
+ content_hash: contentHash,
61
+ signature,
62
+ };
63
+ ```
64
+
65
+ ### Building the bundle
66
+
67
+ `@geniee-inc/sfa-kit/bundle` does the whole build in one call: it bundles the entry module with esbuild, writes `index.html`, hashes the result, gets the signature from Keystone and writes `manifest.json`.
68
+
69
+ ```ts
70
+ import { buildStaticBundle } from '@geniee-inc/sfa-kit/bundle';
71
+
72
+ await buildStaticBundle({
73
+ entry: 'entry.tsx', // renders <SfaLoginGate>…</SfaLoginGate> into #root
74
+ outDir: 'dist',
75
+ studioVersion: '0.5.0', // the Studio build version, NOT the sfa-kit npm version
76
+ apiBase: 'https://keystone-api-staging.chikyu.mobi',
77
+ accessToken: token,
78
+ });
79
+ ```
80
+
81
+ Or from the command line, which is the shorter path when a build script drives it:
82
+
83
+ ```bash
84
+ SFA_KIT_ACCESS_TOKEN=<token> npx sfa-kit-bundle \
85
+ --entry entry.tsx \
86
+ --out dist \
87
+ --studio-version 0.5.0 \
88
+ --api-base https://keystone-api-staging.chikyu.mobi
89
+ ```
90
+
91
+ The token comes from the environment rather than a flag, so it stays out of shell history and the process list.
92
+
93
+ Then zip the **contents** of `dist/` — the bundle's files have to sit at the zip root, not inside a wrapper folder. Zipping is left to the caller: Node ships no zip writer, and every caller already has one.
94
+
95
+ `esbuild` is an optional peer dependency; install it alongside the kit. Its options are fixed: JS, JSX, TS and CSS bundle, but there is no loader for images or fonts, so a page importing a `.png`, `.svg` or a font — including through `url()` in CSS — fails to build. Inline such assets as data URIs, or reference them by absolute URL.
96
+ Nothing is written when signing fails, so a failed build never leaves an unsigned bundle behind to be uploaded by mistake.
97
+
98
+ `studioVersion` has no default on purpose. The signature covers `"{studio_version}:{content_hash}"` and Keystone reads the field back separately, so a guessed value produces a valid signature over the wrong version.
99
+
100
+ ### Signing exports
101
+
102
+ The lower-level pieces `buildStaticBundle` uses, for a caller that builds its own way:
103
+
104
+ | Export | Needs the signing key |
105
+ | ----------------------------- | --------------------- |
106
+ | `readBundleFiles` | No |
107
+ | `computeContentHash` | No |
108
+ | `computeSignature` | **Yes** |
109
+ | `createManifest` | **Yes** |
110
+ | `createManifestFromDirectory` | **Yes** |
111
+
112
+ The three key-taking exports exist for the service that holds the key. Calling them from a page build means inventing a key, which Keystone will not accept — and the mistake is invisible at first, because bundle verification ships in `disabled` mode: the upload succeeds, and only starts failing once the mode is switched to `enforced`.
113
+
114
+ `content_hash` must come from `computeContentHash`, never from hashing files back to back. The algorithm length-prefixes each path and each file's contents before feeding them into one running SHA-256; without those prefixes `"ab" + "c"` and `"a" + "bc"` hash identically, and the result never matches Keystone's own computation.
25
115
 
26
116
  ## Architecture
27
117
 
@@ -286,9 +376,11 @@ that did land, and those are not recoverable anywhere else.
286
376
  whole picture leaves it looking neither written nor retryable. Re-read the
287
377
  records to find out what landed
288
378
  - `failed` — the rows that did not land **and can be sent again**: `conflict`,
289
- `invalid` and `not_attempted`. Retry exactly these. What makes that safe is
290
- that they did not apply; there is no server-side de-duplication to fall back
291
- on
379
+ `invalid`, `not_attempted`, `not_found` and `not_permitted`. Retry exactly
380
+ these. What makes that safe is that they did not apply; there is no
381
+ server-side de-duplication to fall back on. Read each row's `status` before
382
+ offering a retry: `not_found` and `not_permitted` are the server's settled
383
+ answer, and resending alone will not change it
292
384
  - `unresolved` — the rows that may already be written but cannot be reported
293
385
  either way. They keep whatever `id` arrived. **Re-read these; never retry
294
386
  them**: a create that already landed would be written twice
@@ -375,7 +467,9 @@ return (
375
467
  <ErrorSnackbar
376
468
  error={null}
377
469
  message={`${results.length} 件中 ${updated.length} 件を保存しました。${failed.length} 件は保存されていません。`}
378
- onRetry={() => void bulkUpdate(rowsFor(failed)).catch(() => undefined)}
470
+ onRetry={() =>
471
+ void bulkUpdate(rowsFor(resendable(failed))).catch(() => undefined)
472
+ }
379
473
  onClose={reset}
380
474
  />
381
475
  )}
@@ -385,17 +479,42 @@ return (
385
479
 
386
480
  Counts only: which row failed and why belongs in the grid, where the reader
387
481
  acts on it — `results` carries each row's `index` and its per-field errors.
388
- Offer `onRetry` for `failed` rows; `unresolved` rows are re-read instead, so
389
- label the action accordingly (`retryLabel`).
482
+ `unresolved` rows are re-read instead, so label the action accordingly
483
+ (`retryLabel`).
484
+
485
+ Offer `onRetry` for the `failed` rows a resend can still change — that is what
486
+ `resendable` stands for above:
487
+
488
+ ```tsx
489
+ const SETTLED = new Set(['not_found', 'not_permitted']);
490
+
491
+ const resendable = (rows: readonly SfaBulkRowResult[]) =>
492
+ rows.filter(row => !SETTLED.has(row.status));
493
+ ```
494
+
495
+ Every row in `failed` is safe to send again, but a `not_found` or
496
+ `not_permitted` one comes back refused however often it is sent. Showing those
497
+ under a retry button gives the reader an action that cannot succeed; report
498
+ what they say instead.
390
499
 
391
500
  A row counts as written only when it carries what a write produces: the record
392
501
  for a create or an update, the id for a delete. Anything short of that is
393
502
  `failed` or `unresolved`, and which one is decided by what the answer says,
394
- not by elimination. Three statuses mean the row did not apply, and only those
395
- may be sent again: `conflict`, `invalid`, and `not_attempted` — the last for a
396
- row the call ran out of budget before starting, which is resent exactly as it
397
- was. A `conflict` carries the server's current record: retry with the
398
- `version` it reports, or re-read first if none arrived.
503
+ not by elimination. Five statuses mean the row did not apply, and only those
504
+ may be sent again:
505
+
506
+ | Status | What it means | What to do |
507
+ | --------------- | -------------------------------------------------- | -------------------------------------------------------------------------- |
508
+ | `conflict` | Another write got there first | Retry with the `version` the row reports, or re-read first if none arrived |
509
+ | `invalid` | The row failed validation | Correct the row from its per-field errors, then resend |
510
+ | `not_attempted` | The call ran out of budget before starting the row | Resend exactly as it was |
511
+ | `not_found` | The record the row named does not exist | Tell the reader; a resend answers the same |
512
+ | `not_permitted` | The caller may not write that record | Tell the reader; a resend answers the same |
513
+
514
+ `not_found` and `not_permitted` are refusals of a row the call did reach, so no
515
+ write happened and resending is safe — it is simply futile until the record or
516
+ the permission changes. Their `error.code` may be an empty string: keystone has
517
+ not minted one for `not_permitted`, so branch on `status` rather than `code`.
399
518
 
400
519
  Everything else is `unresolved`: `indeterminate`, the server saying it cannot
401
520
  tell whether the row applied; a claimed success missing what it should carry —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geniee-inc/sfa-kit",
3
- "version": "1.14.1",
3
+ "version": "1.17.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",
@@ -17,8 +17,19 @@
17
17
  "import": "./src/signing.js",
18
18
  "default": "./src/signing.js"
19
19
  },
20
+ "./bundle": {
21
+ "types": "./src/bundle.d.ts",
22
+ "import": "./src/bundle.js",
23
+ "default": "./src/bundle.js"
24
+ },
20
25
  "./package.json": "./package.json"
21
26
  },
27
+ "bin": {
28
+ "sfa-kit-bundle": "./src/bin/sfa-kit-bundle.js"
29
+ },
30
+ "engines": {
31
+ "node": ">=20.1.0"
32
+ },
22
33
  "sideEffects": false,
23
34
  "files": [
24
35
  "src",
@@ -33,9 +44,15 @@
33
44
  "license": "MIT",
34
45
  "peerDependencies": {
35
46
  "react": "^18.0.0 || ^19.0.0",
36
- "react-dom": "^18.0.0 || ^19.0.0"
47
+ "react-dom": "^18.0.0 || ^19.0.0",
48
+ "esbuild": ">=0.18.0"
37
49
  },
38
50
  "dependencies": {
39
51
  "tslib": "^2.3.0"
52
+ },
53
+ "peerDependenciesMeta": {
54
+ "esbuild": {
55
+ "optional": true
56
+ }
40
57
  }
41
58
  }
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @description Runs the bundler from the command line.
4
+ *
5
+ * Exported so the test suite drives the same entry point the `bin` does, and
6
+ * so a caller embedding it can pass argv explicitly.
7
+ *
8
+ * @returns The process exit code. 0 on success, 1 on any failure.
9
+ */
10
+ export declare function runBundleCli(argv: readonly string[], env: Readonly<Record<string, string | undefined>>, out: {
11
+ log(message: string): void;
12
+ error(message: string): void;
13
+ }): Promise<number>;
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ import { __awaiter } from "tslib";
3
+ import { basename } from 'node:path';
4
+ import { parseArgs } from 'node:util';
5
+ import { buildStaticBundle } from '../lib/bundle/index.js';
6
+ /** The access token is read from here, never from an argument. */
7
+ const TOKEN_ENV = 'SFA_KIT_ACCESS_TOKEN';
8
+ const USAGE = `sfa-kit-bundle — build and sign a static bundle for an SFA/CRM custom tab
9
+
10
+ Usage:
11
+ ${TOKEN_ENV}=<token> npx sfa-kit-bundle \\
12
+ --entry entry.tsx \\
13
+ --out dist \\
14
+ --studio-version 0.5.0 \\
15
+ --api-base https://keystone-api-staging.chikyu.mobi
16
+
17
+ Options:
18
+ --entry Entry module that mounts the page into #root. Required.
19
+ --out Output directory. Required.
20
+ --studio-version The Studio build version this bundle targets, not the
21
+ sfa-kit npm version. Required.
22
+ --api-base Keystone API base, without a trailing slash. Required.
23
+ --title <title> of the generated index.html. Default: SFA/CRM.
24
+ --help Show this message.
25
+
26
+ Environment:
27
+ ${TOKEN_ENV} Bearer token for Keystone, the same one the rest of the
28
+ API is called with. Required, and read from the environment rather than an
29
+ argument so it does not land in shell history or in the process list.
30
+
31
+ Writes index.html, assets/bundle.js and a signed manifest.json. Zip the
32
+ contents of the output directory — the files have to sit at the zip root.
33
+ Needs esbuild installed alongside the kit.`;
34
+ const OPTIONS = {
35
+ entry: { type: 'string' },
36
+ out: { type: 'string' },
37
+ 'studio-version': { type: 'string' },
38
+ 'api-base': { type: 'string' },
39
+ title: { type: 'string' },
40
+ help: { type: 'boolean' },
41
+ };
42
+ /**
43
+ * @description Reads the flags, reporting a malformed command line as data.
44
+ *
45
+ * NOTE: `parseArgs` throws on a misspelled flag, a flag with no value, and a
46
+ * stray positional. Left to propagate, those would reject the promise the bin
47
+ * only attaches `.then()` to, and the user would get a stack trace where the
48
+ * usage text belongs.
49
+ */
50
+ function parseCliOptions(argv) {
51
+ try {
52
+ const { values } = parseArgs({
53
+ args: [...argv],
54
+ options: OPTIONS,
55
+ allowPositionals: false,
56
+ });
57
+ return { values };
58
+ }
59
+ catch (thrown) {
60
+ return { error: thrown instanceof Error ? thrown.message : String(thrown) };
61
+ }
62
+ }
63
+ /**
64
+ * @description Runs the bundler from the command line.
65
+ *
66
+ * Exported so the test suite drives the same entry point the `bin` does, and
67
+ * so a caller embedding it can pass argv explicitly.
68
+ *
69
+ * @returns The process exit code. 0 on success, 1 on any failure.
70
+ */
71
+ export function runBundleCli(argv, env, out) {
72
+ return __awaiter(this, void 0, void 0, function* () {
73
+ var _a, _b, _c, _d, _e, _f;
74
+ const parsed = parseCliOptions(argv);
75
+ if (parsed.error != null) {
76
+ out.error(`${parsed.error}\n\n${USAGE}`);
77
+ return 1;
78
+ }
79
+ const values = parsed.values;
80
+ if (values.help) {
81
+ out.log(USAGE);
82
+ return 0;
83
+ }
84
+ const missing = ['entry', 'out', 'studio-version', 'api-base']
85
+ .filter(name => { var _a; return ((_a = values[name]) !== null && _a !== void 0 ? _a : '') === ''; })
86
+ .map(name => `--${name}`);
87
+ const accessToken = (_a = env[TOKEN_ENV]) !== null && _a !== void 0 ? _a : '';
88
+ if (accessToken === '')
89
+ missing.push(TOKEN_ENV);
90
+ if (missing.length > 0) {
91
+ out.error(`Missing required: ${missing.join(', ')}\n\n${USAGE}`);
92
+ return 1;
93
+ }
94
+ try {
95
+ const manifest = yield buildStaticBundle(Object.assign({ entry: (_b = values.entry) !== null && _b !== void 0 ? _b : '', outDir: (_c = values.out) !== null && _c !== void 0 ? _c : '', studioVersion: (_d = values['studio-version']) !== null && _d !== void 0 ? _d : '', apiBase: (_e = values['api-base']) !== null && _e !== void 0 ? _e : '', accessToken }, (((_f = values.title) !== null && _f !== void 0 ? _f : '') === '' ? {} : { title: values.title })));
96
+ out.log(`Built and signed ${values.out} ` +
97
+ `(studio_version ${manifest.studio_version}, ` +
98
+ `content_hash ${manifest.content_hash}).`);
99
+ return 0;
100
+ }
101
+ catch (thrown) {
102
+ // NOTE: buildStaticBundle removes what it wrote, so there is normally
103
+ // nothing left to upload. The one case where output survives — cleanup
104
+ // itself failing — says so in its own message.
105
+ out.error(`${thrown instanceof Error ? thrown.message : String(thrown)}\n` +
106
+ 'Nothing was signed, so nothing is ready to upload.');
107
+ return 1;
108
+ }
109
+ });
110
+ }
111
+ /** True when this module is what node was asked to run, not an import of it. */
112
+ function isEntryPoint() {
113
+ // NOTE: the file name, not a substring of the path. `includes()` would also
114
+ // fire for a consumer script under, say, ~/work/sfa-kit-bundle-demo/, which
115
+ // would start the CLI merely because that script imported runBundleCli.
116
+ //
117
+ // `import.meta.url` would be the exact comparison, but Jest transforms this
118
+ // file to CommonJS, where that syntax does not parse and the whole suite
119
+ // fails to load.
120
+ const invoked = process.argv[1];
121
+ if (invoked == null)
122
+ return false;
123
+ const name = basename(invoked);
124
+ return name === 'sfa-kit-bundle' || name === 'sfa-kit-bundle.js';
125
+ }
126
+ /* istanbul ignore next -- the process wiring is exercised by running the bin */
127
+ if (isEntryPoint()) {
128
+ // NOTE: no top-level await — the package's module target does not allow it.
129
+ void runBundleCli(process.argv.slice(2), process.env, {
130
+ log: message => console.log(message),
131
+ error: message => console.error(message),
132
+ })
133
+ .then(code => {
134
+ process.exitCode = code;
135
+ })
136
+ .catch((thrown) => {
137
+ // Nothing above should reject, but an unhandled rejection would exit 0
138
+ // on some Node versions — reporting a failed build as a success.
139
+ console.error(thrown instanceof Error ? thrown.message : String(thrown));
140
+ process.exitCode = 1;
141
+ });
142
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Static bundle builder for GENIEE SFA/CRM custom-tab uploads.
3
+ *
4
+ * This subpath export (`@geniee-inc/sfa-kit/bundle`) is Node.js-only and needs
5
+ * `esbuild` installed alongside the kit. It produces the framework-free
6
+ * `index.html` + `assets/bundle.js` + signed `manifest.json` a page is uploaded
7
+ * as; the caller zips the result.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { buildStaticBundle } from '@geniee-inc/sfa-kit/bundle';
12
+ *
13
+ * await buildStaticBundle({
14
+ * entry: 'entry.tsx',
15
+ * outDir: 'dist',
16
+ * studioVersion: '0.5.0',
17
+ * apiBase: 'https://keystone-api-staging.chikyu.mobi',
18
+ * accessToken: token,
19
+ * });
20
+ * ```
21
+ *
22
+ * @packageDocumentation
23
+ */
24
+ export type { BundleManifest } from './lib/signing/index.js';
25
+ export type { BuildStaticBundleOptions } from './lib/bundle/index.js';
26
+ export { buildStaticBundle } from './lib/bundle/index.js';
package/src/bundle.js ADDED
@@ -0,0 +1 @@
1
+ export { buildStaticBundle } from './lib/bundle/index.js';
@@ -0,0 +1,12 @@
1
+ import { type ReactNode } from 'react';
2
+ interface ObjectKeyGuardProps {
3
+ readonly objectKey: string;
4
+ readonly children: ReactNode;
5
+ }
6
+ /**
7
+ * objectKey が組織のオブジェクトカタログに存在することを検証する。
8
+ * 不一致の場合はページ本体の代わりにエラーバナーを表示し、
9
+ * singular / typo のキーがリフレッシュ時に 404 で壊れる前に検出する。
10
+ */
11
+ export declare function ObjectKeyGuard({ objectKey, children }: ObjectKeyGuardProps): JSX.Element;
12
+ export {};
@@ -0,0 +1,61 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useObjectCatalog } from '../hooks/useObjectCatalog';
3
+ import { font, space } from '../theme';
4
+ /**
5
+ * objectKey が組織のオブジェクトカタログに存在することを検証する。
6
+ * 不一致の場合はページ本体の代わりにエラーバナーを表示し、
7
+ * singular / typo のキーがリフレッシュ時に 404 で壊れる前に検出する。
8
+ */
9
+ export function ObjectKeyGuard({ objectKey, children }) {
10
+ const { objects, isLoading, error } = useObjectCatalog();
11
+ // カタログ取得中やエラー時はブロックせずそのまま表示する。
12
+ // ガードが止めるのは「カタログに無い」と確定した場合のみ。
13
+ if (isLoading || error || objects.length === 0)
14
+ return _jsx(_Fragment, { children: children });
15
+ const knownKeys = objects.map(o => o.key);
16
+ if (knownKeys.includes(objectKey))
17
+ return _jsx(_Fragment, { children: children });
18
+ // 提案のみ大文字小文字を無視する。一致判定は API がキーを厳密比較するため厳密のまま
19
+ const target = objectKey.toLowerCase();
20
+ const suggestion = knownKeys.find(knownKey => {
21
+ const key = knownKey.toLowerCase();
22
+ return key.startsWith(target) || target.startsWith(key);
23
+ });
24
+ return (_jsx("div", { style: {
25
+ padding: space.lg,
26
+ fontFamily: font.family,
27
+ maxWidth: 560,
28
+ margin: '80px auto',
29
+ }, children: _jsxs("div", { style: {
30
+ background: '#FEF2F2',
31
+ border: '1px solid #FECACA',
32
+ borderRadius: 8,
33
+ padding: space.lg,
34
+ }, children: [_jsx("div", { style: {
35
+ fontWeight: 700,
36
+ fontSize: 15,
37
+ color: '#991B1B',
38
+ marginBottom: 8,
39
+ }, children: "Invalid object key" }), _jsxs("p", { style: {
40
+ margin: 0,
41
+ fontSize: 14,
42
+ color: '#7F1D1D',
43
+ lineHeight: 1.5,
44
+ }, children: [_jsx("code", { style: {
45
+ background: '#FEE2E2',
46
+ padding: '2px 6px',
47
+ borderRadius: 4,
48
+ fontSize: 13,
49
+ }, children: objectKey }), ' ', "does not match any object in this organization.", suggestion && (_jsxs(_Fragment, { children: [' ', "Did you mean", ' ', _jsx("code", { style: {
50
+ background: '#DCFCE7',
51
+ padding: '2px 6px',
52
+ borderRadius: 4,
53
+ fontSize: 13,
54
+ color: '#166534',
55
+ }, children: suggestion }), "?"] }))] }), _jsx("p", { style: {
56
+ margin: '12px 0 0',
57
+ fontSize: 12,
58
+ color: '#9CA3AF',
59
+ lineHeight: 1.4,
60
+ }, children: "Object keys are the plural collection name from the object catalogue (e.g. \"business_discussions\", not \"business_discussion\"). Check the objectKey prop on SfaLoginGate and the manifest." })] }) }));
61
+ }
@@ -6,6 +6,7 @@ import { color, font, radius, space, baseContainerStyle, baseButtonStyle, } from
6
6
  import { SFA_KIT_VERSION } from '../version';
7
7
  import { DevToolbar } from './DevToolbar';
8
8
  import { authenticate } from './keystone-auth';
9
+ import { ObjectKeyGuard } from './ObjectKeyGuard';
9
10
  import { KEYSTONE_PRODUCTION_API_BASE, resolveApiBase, } from './resolve-api-base';
10
11
  import { loadSession, saveSession, clearSession, loadLastOrgId, } from './session-store';
11
12
  import { StandalonePreviewCtx } from './StandalonePreviewContext';
@@ -102,7 +103,7 @@ export function SfaLoginGate({ apiBase, trustedOrigin, defaultOrganizationId, ob
102
103
  return (_jsx(EmbedStatus, { title: "\u63A5\u7D9A\u3067\u304D\u307E\u305B\u3093", message: "SFA/CRM \u306E\u30DB\u30B9\u30C8\u30AA\u30EA\u30B8\u30F3\u3092\u78BA\u8A8D\u3067\u304D\u306A\u3044\u305F\u3081\u3001\u3053\u306E\u30DA\u30FC\u30B8\u3092\u8868\u793A\u3067\u304D\u307E\u305B\u3093\u3002\u57CB\u3081\u8FBC\u307F\u8A2D\u5B9A\u3092\u7BA1\u7406\u8005\u306B\u3054\u78BA\u8A8D\u304F\u3060\u3055\u3044\u3002" }));
103
104
  }
104
105
  // Embedded: the SFA/CRM host that framed us decides the environment.
105
- return (_jsx(EmbedFlow, { trustedOrigin: resolvedOrigin, apiBase: apiBase !== null && apiBase !== void 0 ? apiBase : resolveApiBase(resolvedOrigin), children: children }));
106
+ return (_jsx(EmbedFlow, { objectKey: objectKey, trustedOrigin: resolvedOrigin, apiBase: apiBase !== null && apiBase !== void 0 ? apiBase : resolveApiBase(resolvedOrigin), children: children }));
106
107
  }
107
108
  // Standalone: there is no host, so the page's own origin decides.
108
109
  return (_jsx(StandaloneFlow, { apiBase: apiBase !== null && apiBase !== void 0 ? apiBase : resolveApiBase(resolvePageOrigin()), defaultOrganizationId: defaultOrganizationId, objectKey: objectKey, level: level, children: children }));
@@ -110,7 +111,7 @@ export function SfaLoginGate({ apiBase, trustedOrigin, defaultOrganizationId, ob
110
111
  // ---------------------------------------------------------------------------
111
112
  // Embed flow (iframe + postMessage)
112
113
  // ---------------------------------------------------------------------------
113
- function EmbedFlow({ trustedOrigin, apiBase, children, }) {
114
+ function EmbedFlow({ objectKey, trustedOrigin, apiBase, children, }) {
114
115
  const normalizedBase = useMemo(() => apiBase.replace(/\/+$/, ''), [apiBase]);
115
116
  const { phase, handleAuthError } = useEmbedSession({
116
117
  apiBase: normalizedBase,
@@ -135,7 +136,7 @@ function EmbedFlow({ trustedOrigin, apiBase, children, }) {
135
136
  }
136
137
  if (!auth)
137
138
  return null;
138
- return (_jsx(EmbedContextValue.Provider, { value: phase.context, children: _jsx(SfaProvider, { apiBase: normalizedBase, auth: auth, mode: "direct", organizationId: phase.session.organizationId, onAuthError: handleAuthError, children: children }) }));
139
+ return (_jsx(EmbedContextValue.Provider, { value: phase.context, children: _jsx(SfaProvider, { apiBase: normalizedBase, auth: auth, mode: "direct", organizationId: phase.session.organizationId, onAuthError: handleAuthError, children: objectKey ? (_jsx(ObjectKeyGuard, { objectKey: objectKey, children: children })) : (children) }) }));
139
140
  }
140
141
  function EmbedStatus({ title, message, showReload, }) {
141
142
  const handleReload = useCallback(() => {
@@ -241,7 +242,7 @@ function StandaloneFlow({ apiBase, defaultOrganizationId, objectKey, level, chil
241
242
  if (!session || !auth) {
242
243
  return (_jsx(LoginForm, { defaultApiBase: apiBase, defaultOrganizationId: defaultOrganizationId, onSubmit: handleLogin, status: status, error: error }));
243
244
  }
244
- 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] }) }));
245
+ 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 }), objectKey ? (_jsx(ObjectKeyGuard, { objectKey: objectKey, children: children })) : (children)] }) }));
245
246
  }
246
247
  // ---------------------------------------------------------------------------
247
248
  // GENIEE SFA/CRM-branded login form
@@ -0,0 +1,45 @@
1
+ import type { BundleManifest } from '../signing/types.js';
2
+ export interface BuildStaticBundleOptions {
3
+ /** Entry module that mounts the page, e.g. `entry.tsx`. */
4
+ readonly entry: string;
5
+ /** Directory the bundle is written to. Created if absent. */
6
+ readonly outDir: string;
7
+ /**
8
+ * The Studio build version this bundle targets — not the sfa-kit npm version.
9
+ *
10
+ * NOTE: required on purpose. The signature covers
11
+ * `"{studio_version}:{content_hash}"` and Keystone reads the field back
12
+ * separately, so a default would sign the wrong version perfectly well.
13
+ */
14
+ readonly studioVersion: string;
15
+ /** Keystone API base, e.g. `https://keystone-api-staging.chikyu.mobi`. */
16
+ readonly apiBase: string;
17
+ /** The same Bearer token the rest of the Keystone API is called with. */
18
+ readonly accessToken: string;
19
+ /** Title of the generated `index.html`. */
20
+ readonly title?: string;
21
+ /**
22
+ * Extra manifest fields, e.g. `name` or `displayName`. Keystone ignores
23
+ * everything but the three signed fields, which cannot be overridden here.
24
+ */
25
+ readonly manifestExtras?: Readonly<Record<string, unknown>>;
26
+ }
27
+ /**
28
+ * @description Builds the framework-free static bundle a page is uploaded as
29
+ * to become a custom tab, and signs it.
30
+ *
31
+ * Produces `index.html`, `assets/bundle.js` and a signed `manifest.json` in
32
+ * `outDir`. The caller zips that directory's contents — the files have to sit
33
+ * at the zip root. Zipping is left out because Node ships no zip writer, and a
34
+ * dependency for it would reach a package whose runtime needs only `tslib`.
35
+ *
36
+ * Signing is not optional, and a failure removes the output: an unsigned bundle
37
+ * uploads successfully while Keystone's verifier runs in `disabled` mode and
38
+ * only starts failing at `enforced`, so a half-built directory left behind is
39
+ * something a caller can still zip and upload by mistake.
40
+ *
41
+ * **Node.js only**, and needs `esbuild` installed alongside the kit.
42
+ *
43
+ * @returns The manifest that was written.
44
+ */
45
+ export declare function buildStaticBundle({ entry, outDir, studioVersion, apiBase, accessToken, title, manifestExtras, }: BuildStaticBundleOptions): Promise<BundleManifest>;