@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 +4 -4
- package/package.json +6 -1
- package/src/lib/hooks/useRecordDetailUrl.d.ts +2 -2
- package/src/lib/hooks/useRecordDetailUrl.js +3 -3
- package/src/lib/navigation/record-detail-url.d.ts +4 -5
- package/src/lib/signing/compute-content-hash.d.ts +13 -0
- package/src/lib/signing/compute-content-hash.js +47 -0
- package/src/lib/signing/compute-signature.d.ts +12 -0
- package/src/lib/signing/compute-signature.js +19 -0
- package/src/lib/signing/create-manifest-from-directory.d.ts +16 -0
- package/src/lib/signing/create-manifest-from-directory.js +46 -0
- package/src/lib/signing/create-manifest.d.ts +13 -0
- package/src/lib/signing/create-manifest.js +22 -0
- package/src/lib/signing/index.d.ts +5 -0
- package/src/lib/signing/index.js +4 -0
- package/src/lib/signing/types.d.ts +21 -0
- package/src/lib/signing/types.js +1 -0
- package/src/lib/version.d.ts +1 -1
- package/src/lib/version.js +3 -1
- package/src/signing.d.ts +18 -0
- package/src/signing.js +1 -0
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.
|
|
21
|
+
// pkg.version -> '1.8.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.
|
|
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: '
|
|
603
|
+
Returns `{ url: string, target: '_blank' } | null`.
|
|
604
604
|
|
|
605
|
-
- `target` is `'
|
|
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.
|
|
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 `'
|
|
11
|
-
*
|
|
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 `'
|
|
13
|
-
*
|
|
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: '
|
|
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 `'
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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: '
|
|
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,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 {};
|
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.8.0";
|
package/src/lib/version.js
CHANGED
package/src/signing.d.ts
ADDED
|
@@ -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';
|