@geniee-inc/sfa-kit 1.7.1 → 1.9.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 +2 -2
- package/package.json +6 -1
- package/src/lib/provider/SfaClient.d.ts +13 -0
- package/src/lib/provider/SfaClient.js +87 -4
- 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 +1 -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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geniee-inc/sfa-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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,
|
|
@@ -12,11 +12,18 @@ export declare class SfaClient implements SfaClientInterface {
|
|
|
12
12
|
private readonly organizationId;
|
|
13
13
|
private readonly onAuthError;
|
|
14
14
|
readonly mode: SfaApiMode;
|
|
15
|
+
/** In-memory cache for describe results, keyed by objectKey. Stores Promises
|
|
16
|
+
* so concurrent callers share a single in-flight request. */
|
|
17
|
+
private readonly describeCache;
|
|
15
18
|
constructor(config: SfaClientConfig);
|
|
16
19
|
private resolveBase;
|
|
17
20
|
get(collection: string, recordId: string): Promise<SfaRecord>;
|
|
18
21
|
list(collection: string, params?: SfaListParams): Promise<SfaListResponse>;
|
|
19
22
|
describe(objectKey: string): Promise<SfaDescribeResult>;
|
|
23
|
+
/** Cached describe fetch. Returns a shared Promise so concurrent callers
|
|
24
|
+
* for the same objectKey deduplicate into one HTTP request. On failure the
|
|
25
|
+
* entry is evicted so the next caller retries. */
|
|
26
|
+
private describeObject;
|
|
20
27
|
listObjects(): Promise<SfaObjectCatalog>;
|
|
21
28
|
listFieldTypes(): Promise<SfaFieldTypeCatalog>;
|
|
22
29
|
private assertDirectMode;
|
|
@@ -33,6 +40,12 @@ export declare class SfaClient implements SfaClientInterface {
|
|
|
33
40
|
* another write landed first.
|
|
34
41
|
*/
|
|
35
42
|
update(collection: string, recordId: string, fields: Record<string, unknown>, version: string): Promise<SfaRecord>;
|
|
43
|
+
/**
|
|
44
|
+
* @description Fetches describe metadata (with cache) and normalizes
|
|
45
|
+
* cardinality:"many" reference fields to arrays. Falls back to no-op if
|
|
46
|
+
* describe fails, so a broken metadata endpoint never blocks writes.
|
|
47
|
+
*/
|
|
48
|
+
private applySchemaAwareNormalization;
|
|
36
49
|
/**
|
|
37
50
|
* @description Deletes a record with optimistic locking. Returns void on
|
|
38
51
|
* success (204). Throws SfaConflictError on 412 if the record was modified
|
|
@@ -21,6 +21,9 @@ function timeoutSignal() {
|
|
|
21
21
|
*/
|
|
22
22
|
export class SfaClient {
|
|
23
23
|
constructor(config) {
|
|
24
|
+
/** In-memory cache for describe results, keyed by objectKey. Stores Promises
|
|
25
|
+
* so concurrent callers share a single in-flight request. */
|
|
26
|
+
this.describeCache = new Map();
|
|
24
27
|
this.apiBase = this.resolveBase(config.apiBase.replace(/\/+$/, ''), config.mode);
|
|
25
28
|
this.auth = config.auth;
|
|
26
29
|
this.mode = config.mode;
|
|
@@ -101,10 +104,26 @@ export class SfaClient {
|
|
|
101
104
|
}
|
|
102
105
|
describe(objectKey) {
|
|
103
106
|
return __awaiter(this, void 0, void 0, function* () {
|
|
104
|
-
const
|
|
105
|
-
return { object
|
|
107
|
+
const object = yield this.describeObject(objectKey);
|
|
108
|
+
return { object };
|
|
106
109
|
});
|
|
107
110
|
}
|
|
111
|
+
/** Cached describe fetch. Returns a shared Promise so concurrent callers
|
|
112
|
+
* for the same objectKey deduplicate into one HTTP request. On failure the
|
|
113
|
+
* entry is evicted so the next caller retries. */
|
|
114
|
+
describeObject(objectKey) {
|
|
115
|
+
const cached = this.describeCache.get(objectKey);
|
|
116
|
+
if (cached)
|
|
117
|
+
return cached;
|
|
118
|
+
const promise = this.fetchGet(this.objectsPath(`/${enc(objectKey)}`))
|
|
119
|
+
.then(raw => normalizeObjectDetail(raw))
|
|
120
|
+
.catch((err) => {
|
|
121
|
+
this.describeCache.delete(objectKey);
|
|
122
|
+
throw err;
|
|
123
|
+
});
|
|
124
|
+
this.describeCache.set(objectKey, promise);
|
|
125
|
+
return promise;
|
|
126
|
+
}
|
|
108
127
|
listObjects() {
|
|
109
128
|
return __awaiter(this, void 0, void 0, function* () {
|
|
110
129
|
var _a;
|
|
@@ -142,12 +161,13 @@ export class SfaClient {
|
|
|
142
161
|
create(collection, fields, options) {
|
|
143
162
|
return __awaiter(this, void 0, void 0, function* () {
|
|
144
163
|
this.assertDirectMode();
|
|
164
|
+
const normalized = yield this.applySchemaAwareNormalization(collection, fields);
|
|
145
165
|
const path = `/v1/objects/${enc(collection)}/records`;
|
|
146
166
|
const extra = {};
|
|
147
167
|
if (options === null || options === void 0 ? void 0 : options.idempotencyKey) {
|
|
148
168
|
extra['Idempotency-Key'] = options.idempotencyKey;
|
|
149
169
|
}
|
|
150
|
-
const response = yield this.fetchWithBody('POST', path, { fields: normalizeWriteFields(
|
|
170
|
+
const response = yield this.fetchWithBody('POST', path, { fields: normalizeWriteFields(normalized) }, extra);
|
|
151
171
|
const raw = yield response.json();
|
|
152
172
|
return normalizeKeystoneRecord(raw);
|
|
153
173
|
});
|
|
@@ -161,12 +181,32 @@ export class SfaClient {
|
|
|
161
181
|
return __awaiter(this, void 0, void 0, function* () {
|
|
162
182
|
this.assertDirectMode();
|
|
163
183
|
this.assertRecordId(recordId);
|
|
184
|
+
const normalized = yield this.applySchemaAwareNormalization(collection, fields);
|
|
164
185
|
const path = `/v1/objects/${enc(collection)}/records/${enc(recordId)}`;
|
|
165
|
-
const response = yield this.fetchWithBody('PATCH', path, { fields: normalizeWriteFields(
|
|
186
|
+
const response = yield this.fetchWithBody('PATCH', path, { fields: normalizeWriteFields(normalized) }, { 'If-Match': version });
|
|
166
187
|
const raw = yield response.json();
|
|
167
188
|
return normalizeKeystoneRecord(raw);
|
|
168
189
|
});
|
|
169
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* @description Fetches describe metadata (with cache) and normalizes
|
|
193
|
+
* cardinality:"many" reference fields to arrays. Falls back to no-op if
|
|
194
|
+
* describe fails, so a broken metadata endpoint never blocks writes.
|
|
195
|
+
*/
|
|
196
|
+
applySchemaAwareNormalization(collection, fields) {
|
|
197
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
198
|
+
try {
|
|
199
|
+
const detail = yield this.describeObject(collection);
|
|
200
|
+
return normalizeCardinalityFields(fields, detail.fields);
|
|
201
|
+
}
|
|
202
|
+
catch (_a) {
|
|
203
|
+
// Describe failed -- proceed without cardinality normalization.
|
|
204
|
+
// The write will either succeed (no many-cardinality fields in payload)
|
|
205
|
+
// or fail with the same 422 it would have before this feature.
|
|
206
|
+
return fields;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
170
210
|
/**
|
|
171
211
|
* @description Deletes a record with optimistic locking. Returns void on
|
|
172
212
|
* success (204). Throws SfaConflictError on 412 if the record was modified
|
|
@@ -293,6 +333,49 @@ function enc(segment) {
|
|
|
293
333
|
return encodeURIComponent(segment);
|
|
294
334
|
}
|
|
295
335
|
// ── Keystone record normalization (direct mode) ──
|
|
336
|
+
// ── Write-path normalization ──
|
|
337
|
+
/** Field kinds whose values must be arrays when cardinality is 'many'. */
|
|
338
|
+
const ARRAY_WRAPPABLE_KINDS = new Set([
|
|
339
|
+
'reference',
|
|
340
|
+
'user_reference',
|
|
341
|
+
]);
|
|
342
|
+
/**
|
|
343
|
+
* @description Wraps scalar values in arrays for fields whose schema declares
|
|
344
|
+
* cardinality:"many". Already-array values, null, and undefined are passed
|
|
345
|
+
* through unchanged. Runs *before* normalizeWriteFields so SfaFieldValue
|
|
346
|
+
* wrappers are still intact; when a wrapper is detected its inner `.value`
|
|
347
|
+
* is wrapped while the wrapper structure is preserved.
|
|
348
|
+
*/
|
|
349
|
+
function normalizeCardinalityFields(fields, descriptors) {
|
|
350
|
+
const manyFields = new Set();
|
|
351
|
+
for (const desc of descriptors) {
|
|
352
|
+
if (desc.cardinality === 'many' && ARRAY_WRAPPABLE_KINDS.has(desc.kind)) {
|
|
353
|
+
manyFields.add(desc.key);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (manyFields.size === 0)
|
|
357
|
+
return fields;
|
|
358
|
+
const result = {};
|
|
359
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
360
|
+
if (manyFields.has(key) && value != null && !Array.isArray(value)) {
|
|
361
|
+
if (isSfaFieldValue(value)) {
|
|
362
|
+
// Wrap the inner value; preserve the SfaFieldValue wrapper so the
|
|
363
|
+
// subsequent normalizeWriteFields step can still unwrap it.
|
|
364
|
+
const inner = value.value;
|
|
365
|
+
result[key] =
|
|
366
|
+
inner != null && !Array.isArray(inner)
|
|
367
|
+
? Object.assign(Object.assign({}, value), { value: [inner] }) : value;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
result[key] = [value];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
result[key] = value;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
296
379
|
/**
|
|
297
380
|
* @description Converts a caller-supplied fields map into the raw values
|
|
298
381
|
* expected by Keystone's RecordWrite body. If a value is already a raw value
|
|
@@ -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.9.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';
|