@geniee-inc/sfa-kit 1.7.0 → 1.8.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,10 +18,10 @@ 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.6.0'
21
+ // pkg.version -> '1.8.0'
22
22
  ```
23
23
 
24
- No other subpath resolves: `exports` deliberately exposes only the root entry and `./package.json`, so internal modules cannot be imported directly.
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.
25
25
 
26
26
  ## Architecture
27
27
 
@@ -600,9 +600,9 @@ function CompanyRow({ record }: { record: SfaRecord }) {
600
600
  }
601
601
  ```
602
602
 
603
- Returns `{ url: string, target: '_top' | '_blank' } | null`.
603
+ Returns `{ url: string, target: '_blank' } | null`.
604
604
 
605
- - `target` is `'_top'` when running inside an SFA iframe (navigates the parent frame to the record).
605
+ - `target` is always `'_blank'` so the record detail page opens in a new browser tab, preserving the user's current Studio page.
606
606
  - Returns `null` in standalone preview mode (no host origin to build a URL from), when no `organizationId` is in context, or when the collection does not have a standard record page (e.g. `schedules`).
607
607
 
608
608
  ### buildRecordDetailUrl (advanced)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geniee-inc/sfa-kit",
3
- "version": "1.7.0",
3
+ "version": "1.8.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",
@@ -12,6 +12,11 @@
12
12
  "import": "./src/index.js",
13
13
  "default": "./src/index.js"
14
14
  },
15
+ "./signing": {
16
+ "types": "./src/signing.d.ts",
17
+ "import": "./src/signing.js",
18
+ "default": "./src/signing.js"
19
+ },
15
20
  "./package.json": "./package.json"
16
21
  },
17
22
  "sideEffects": false,
@@ -7,8 +7,8 @@ import type { RecordDetailLink } from '../navigation/record-detail-url';
7
7
  * Reads the organization ID from `SfaContext` and the host origin from
8
8
  * `document.referrer` (the SFA parent frame). No configuration needed.
9
9
  *
10
- * The returned `target` is always `'_top'` (navigates the SFA parent frame),
11
- * because the hook only returns a link when running inside an iframe.
10
+ * The returned `target` is always `'_blank'` so the record opens in a new
11
+ * browser tab, preserving the user's current Studio page context.
12
12
  *
13
13
  * @example
14
14
  * ```tsx
@@ -9,8 +9,8 @@ import { useSfaContext } from '../provider/useSfaClient';
9
9
  * Reads the organization ID from `SfaContext` and the host origin from
10
10
  * `document.referrer` (the SFA parent frame). No configuration needed.
11
11
  *
12
- * The returned `target` is always `'_top'` (navigates the SFA parent frame),
13
- * because the hook only returns a link when running inside an iframe.
12
+ * The returned `target` is always `'_blank'` so the record opens in a new
13
+ * browser tab, preserving the user's current Studio page context.
14
14
  *
15
15
  * @example
16
16
  * ```tsx
@@ -37,6 +37,6 @@ export function useRecordDetailUrl(collectionName, recordId) {
37
37
  if (url == null) {
38
38
  return null;
39
39
  }
40
- return { url, target: '_top' };
40
+ return { url, target: '_blank' };
41
41
  }, [organizationId, collectionName, recordId]);
42
42
  }
@@ -24,12 +24,11 @@ export interface RecordDetailLink {
24
24
  /** The full URL to the record detail page. */
25
25
  readonly url: string;
26
26
  /**
27
- * Suggested link target. Always `'_top'` because the hook only returns a
28
- * link when running inside an iframe, and `_top` navigates the SFA parent
29
- * frame to the record detail page. Callers may override with `'_blank'`
30
- * in their `<a>` tag if the user prefers a new tab.
27
+ * Suggested link target. Always `'_blank'` so the record detail page opens
28
+ * in a new browser tab, preserving the user's current Studio page context.
29
+ * The SFA iframe sandbox includes `allow-popups` to permit this.
31
30
  */
32
- readonly target: '_top';
31
+ readonly target: '_blank';
33
32
  }
34
33
  /**
35
34
  * @description Builds the full URL for a record's detail page in SFA/CRM.
@@ -0,0 +1,13 @@
1
+ import type { FileEntry } from './types';
2
+ /**
3
+ * Computes the SHA-256 content hash matching Keystone's
4
+ * {@link https://github.com/chikyuinc/chikyu/blob/branch_main/keystone/src/keystone/features/pages/domain/page_bundle_integrity.py compute_content_hash}.
5
+ *
6
+ * The file at path "manifest.json" (exact match, root only) is excluded.
7
+ * Remaining files are sorted lexicographically by path. For each file, the
8
+ * hash input is: 4-byte big-endian path length, UTF-8 path bytes, 4-byte
9
+ * big-endian content length, content bytes.
10
+ *
11
+ * @returns Hex-encoded SHA-256 digest.
12
+ */
13
+ export declare function computeContentHash(files: ReadonlyArray<FileEntry>): string;
@@ -0,0 +1,47 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * Computes the SHA-256 content hash matching Keystone's
4
+ * {@link https://github.com/chikyuinc/chikyu/blob/branch_main/keystone/src/keystone/features/pages/domain/page_bundle_integrity.py compute_content_hash}.
5
+ *
6
+ * The file at path "manifest.json" (exact match, root only) is excluded.
7
+ * Remaining files are sorted lexicographically by path. For each file, the
8
+ * hash input is: 4-byte big-endian path length, UTF-8 path bytes, 4-byte
9
+ * big-endian content length, content bytes.
10
+ *
11
+ * @returns Hex-encoded SHA-256 digest.
12
+ */
13
+ export function computeContentHash(files) {
14
+ const hash = createHash('sha256');
15
+ const sorted = [...files]
16
+ .filter(f => f.path !== 'manifest.json')
17
+ .sort((a, b) => compareByCodePoints(a.path, b.path));
18
+ for (const entry of sorted) {
19
+ const pathBytes = Buffer.from(entry.path, 'utf-8');
20
+ hash.update(uint32BE(pathBytes.length));
21
+ hash.update(pathBytes);
22
+ hash.update(uint32BE(entry.content.length));
23
+ hash.update(entry.content);
24
+ }
25
+ return hash.digest('hex');
26
+ }
27
+ /** Encode a number as a 4-byte big-endian buffer. */
28
+ function uint32BE(n) {
29
+ const buf = Buffer.alloc(4);
30
+ buf.writeUInt32BE(n);
31
+ return buf;
32
+ }
33
+ /** Compare two strings by Unicode code point, matching Python's `sorted()`. */
34
+ function compareByCodePoints(a, b) {
35
+ let i = 0;
36
+ let j = 0;
37
+ while (i < a.length && j < b.length) {
38
+ const ac = a.codePointAt(i);
39
+ const bc = b.codePointAt(j);
40
+ if (ac !== bc) {
41
+ return ac - bc;
42
+ }
43
+ i += ac > 0xffff ? 2 : 1;
44
+ j += bc > 0xffff ? 2 : 1;
45
+ }
46
+ return a.length - b.length;
47
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Computes the HMAC-SHA-256 bundle signature matching Keystone's
3
+ * BundleSignatureVerifier.
4
+ *
5
+ * Signs the message `"{studioVersion}:{contentHash}"` with the given key.
6
+ *
7
+ * @param studioVersion - Studio version stored as page metadata.
8
+ * @param contentHash - Hex-encoded SHA-256 from {@link computeContentHash}.
9
+ * @param signingKey - Signing key as a UTF-8 string or raw bytes.
10
+ * @returns Hex-encoded HMAC-SHA-256 digest.
11
+ */
12
+ export declare function computeSignature(studioVersion: string, contentHash: string, signingKey: string | Uint8Array): string;
@@ -0,0 +1,19 @@
1
+ import { createHmac } from 'node:crypto';
2
+ /**
3
+ * Computes the HMAC-SHA-256 bundle signature matching Keystone's
4
+ * BundleSignatureVerifier.
5
+ *
6
+ * Signs the message `"{studioVersion}:{contentHash}"` with the given key.
7
+ *
8
+ * @param studioVersion - Studio version stored as page metadata.
9
+ * @param contentHash - Hex-encoded SHA-256 from {@link computeContentHash}.
10
+ * @param signingKey - Signing key as a UTF-8 string or raw bytes.
11
+ * @returns Hex-encoded HMAC-SHA-256 digest.
12
+ */
13
+ export function computeSignature(studioVersion, contentHash, signingKey) {
14
+ if (signingKey.length === 0) {
15
+ throw new Error('signingKey must not be empty');
16
+ }
17
+ const message = `${studioVersion}:${contentHash}`;
18
+ return createHmac('sha256', signingKey).update(message).digest('hex');
19
+ }
@@ -0,0 +1,16 @@
1
+ import type { BundleManifest } from './types';
2
+ /**
3
+ * Reads a dist directory and creates a signed {@link BundleManifest}.
4
+ *
5
+ * Walks the directory tree, constructs {@link FileEntry} objects with
6
+ * forward-slash relative paths, and delegates to {@link createManifest}.
7
+ *
8
+ * This function is **Node.js-only** and must not be used in browser
9
+ * environments. It does NOT write the manifest file to disk; the caller
10
+ * is responsible for serializing and writing the returned object.
11
+ *
12
+ * @param distDir - Absolute or relative path to the dist directory.
13
+ * @param studioVersion - Studio version stored as page metadata.
14
+ * @param signingKey - Signing key as a UTF-8 string or raw bytes.
15
+ */
16
+ export declare function createManifestFromDirectory(distDir: string, studioVersion: string, signingKey: string | Uint8Array): Promise<BundleManifest>;
@@ -0,0 +1,46 @@
1
+ import { __awaiter } from "tslib";
2
+ import { readdir, readFile } from 'node:fs/promises';
3
+ import { join, relative, posix } from 'node:path';
4
+ import { createManifest } from './create-manifest';
5
+ /**
6
+ * Reads a dist directory and creates a signed {@link BundleManifest}.
7
+ *
8
+ * Walks the directory tree, constructs {@link FileEntry} objects with
9
+ * forward-slash relative paths, and delegates to {@link createManifest}.
10
+ *
11
+ * This function is **Node.js-only** and must not be used in browser
12
+ * environments. It does NOT write the manifest file to disk; the caller
13
+ * is responsible for serializing and writing the returned object.
14
+ *
15
+ * @param distDir - Absolute or relative path to the dist directory.
16
+ * @param studioVersion - Studio version stored as page metadata.
17
+ * @param signingKey - Signing key as a UTF-8 string or raw bytes.
18
+ */
19
+ export function createManifestFromDirectory(distDir, studioVersion, signingKey) {
20
+ return __awaiter(this, void 0, void 0, function* () {
21
+ const files = yield collectFiles(distDir);
22
+ return createManifest(studioVersion, files, signingKey);
23
+ });
24
+ }
25
+ /**
26
+ * Recursively collects all files under a directory as {@link FileEntry} objects.
27
+ * Paths are normalized to POSIX forward slashes.
28
+ */
29
+ function collectFiles(dir) {
30
+ return __awaiter(this, void 0, void 0, function* () {
31
+ var _a;
32
+ const entries = yield readdir(dir, { withFileTypes: true, recursive: true });
33
+ const files = [];
34
+ for (const entry of entries) {
35
+ if (!entry.isFile())
36
+ continue;
37
+ const parentPath = (_a = entry.parentPath) !== null && _a !== void 0 ? _a : entry.path;
38
+ const absolutePath = join(parentPath, entry.name);
39
+ const relativePath = relative(dir, absolutePath);
40
+ const posixPath = relativePath.split(/[\\/]/).join(posix.sep);
41
+ const content = yield readFile(absolutePath);
42
+ files.push({ path: posixPath, content });
43
+ }
44
+ return files;
45
+ });
46
+ }
@@ -0,0 +1,13 @@
1
+ import type { BundleManifest, FileEntry } from './types';
2
+ /**
3
+ * Creates a signed {@link BundleManifest} from a list of bundle files.
4
+ *
5
+ * Returns the three fields Keystone's BundleSignatureVerifier requires.
6
+ * Callers can spread decorative fields (e.g. `sfaKitVersion` for the npm
7
+ * package version, `name`, `displayName`) on top before writing to disk.
8
+ *
9
+ * @param studioVersion - Studio version stored as page metadata.
10
+ * @param files - All files in the bundle (manifest.json is automatically excluded).
11
+ * @param signingKey - Signing key as a UTF-8 string or raw bytes.
12
+ */
13
+ export declare function createManifest(studioVersion: string, files: ReadonlyArray<FileEntry>, signingKey: string | Uint8Array): BundleManifest;
@@ -0,0 +1,22 @@
1
+ import { computeContentHash } from './compute-content-hash';
2
+ import { computeSignature } from './compute-signature';
3
+ /**
4
+ * Creates a signed {@link BundleManifest} from a list of bundle files.
5
+ *
6
+ * Returns the three fields Keystone's BundleSignatureVerifier requires.
7
+ * Callers can spread decorative fields (e.g. `sfaKitVersion` for the npm
8
+ * package version, `name`, `displayName`) on top before writing to disk.
9
+ *
10
+ * @param studioVersion - Studio version stored as page metadata.
11
+ * @param files - All files in the bundle (manifest.json is automatically excluded).
12
+ * @param signingKey - Signing key as a UTF-8 string or raw bytes.
13
+ */
14
+ export function createManifest(studioVersion, files, signingKey) {
15
+ const contentHash = computeContentHash(files);
16
+ const signature = computeSignature(studioVersion, contentHash, signingKey);
17
+ return {
18
+ studio_version: studioVersion,
19
+ content_hash: contentHash,
20
+ signature,
21
+ };
22
+ }
@@ -0,0 +1,5 @@
1
+ export type { BundleManifest, FileEntry } from './types';
2
+ export { computeContentHash } from './compute-content-hash';
3
+ export { computeSignature } from './compute-signature';
4
+ export { createManifest } from './create-manifest';
5
+ export { createManifestFromDirectory } from './create-manifest-from-directory';
@@ -0,0 +1,4 @@
1
+ export { computeContentHash } from './compute-content-hash';
2
+ export { computeSignature } from './compute-signature';
3
+ export { createManifest } from './create-manifest';
4
+ export { createManifestFromDirectory } from './create-manifest-from-directory';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The three fields Keystone's BundleSignatureVerifier reads from
3
+ * manifest.json. Callers can spread decorative fields on top before
4
+ * writing to disk (e.g. `sfaKitVersion` for the npm package version,
5
+ * `name`, `displayName`, `entry`).
6
+ */
7
+ export interface BundleManifest {
8
+ /** Studio version string (the build version, not the sfa-kit npm version). */
9
+ studio_version: string;
10
+ /** SHA-256 hex digest of all non-manifest file contents. */
11
+ content_hash: string;
12
+ /** HMAC-SHA-256 hex of `"{studio_version}:{content_hash}"`. */
13
+ signature: string;
14
+ }
15
+ /** A single file in the bundle, used for content hash computation. */
16
+ export interface FileEntry {
17
+ /** Relative path using forward slashes (e.g. "index.html", "assets/bundle.js"). */
18
+ path: string;
19
+ /** Raw file content as bytes. */
20
+ content: Uint8Array;
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1 +1 @@
1
- export declare const SFA_KIT_VERSION = "1.7.0";
1
+ export declare const SFA_KIT_VERSION = "1.8.0";
@@ -1 +1,3 @@
1
- export const SFA_KIT_VERSION = "1.7.0";
1
+ // このファイルは libs/sfa-kit/scripts/sync-version.cjs が生成する。
2
+ // 手で編集しないこと。バージョンを上げるときは libs/sfa-kit/package.json を変更する。
3
+ export const SFA_KIT_VERSION = '1.8.0';
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Bundle signing utilities for Keystone's BundleSignatureVerifier.
3
+ *
4
+ * This subpath export (`@geniee-inc/sfa-kit/signing`) is Node.js-only
5
+ * and should not be imported in browser environments. It uses `node:crypto`
6
+ * for SHA-256 and HMAC-SHA-256 operations.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { createManifestFromDirectory } from '@geniee-inc/sfa-kit/signing';
11
+ *
12
+ * const manifest = await createManifestFromDirectory('./dist', '0.5.0', signingKey);
13
+ * ```
14
+ *
15
+ * @packageDocumentation
16
+ */
17
+ export type { BundleManifest, FileEntry } from './lib/signing';
18
+ export { computeContentHash, computeSignature, createManifest, createManifestFromDirectory, } from './lib/signing';
package/src/signing.js ADDED
@@ -0,0 +1 @@
1
+ export { computeContentHash, computeSignature, createManifest, createManifestFromDirectory, } from './lib/signing';