space-data-module-sdk 0.8.15 → 0.8.16
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/docs/_shell/consumer-assets.html +5 -0
- package/docs/browser-wasmedge-isomorphic.html +261 -0
- package/docs/byo-wasm-quickstart.html +226 -0
- package/docs/byo-wasm-quickstart.md +204 -0
- package/docs/conformance.html +156 -0
- package/docs/emception-shared-memory-verdict.md +209 -0
- package/docs/events-abi.html +313 -0
- package/docs/events-abi.md +530 -0
- package/docs/families/analytics.html +178 -0
- package/docs/families/analytics.md +104 -0
- package/docs/families/attitude.html +157 -0
- package/docs/families/attitude.md +36 -0
- package/docs/families/behavior.html +157 -0
- package/docs/families/behavior.md +36 -0
- package/docs/families/breakup.html +157 -0
- package/docs/families/breakup.md +36 -0
- package/docs/families/conjunction.html +186 -0
- package/docs/families/conjunction.md +121 -0
- package/docs/families/data-source.html +318 -0
- package/docs/families/effects.html +157 -0
- package/docs/families/effects.md +36 -0
- package/docs/families/environment.html +170 -0
- package/docs/families/environment.md +73 -0
- package/docs/families/estimation.html +190 -0
- package/docs/families/estimation.md +128 -0
- package/docs/families/gnc.html +157 -0
- package/docs/families/gnc.md +36 -0
- package/docs/families/maneuver.html +209 -0
- package/docs/families/maneuver.md +206 -0
- package/docs/families/obstruction.html +191 -0
- package/docs/families/obstruction.md +96 -0
- package/docs/families/propagator.html +333 -0
- package/docs/families/propulsion.html +157 -0
- package/docs/families/propulsion.md +36 -0
- package/docs/families/reentry.html +157 -0
- package/docs/families/reentry.md +36 -0
- package/docs/families/rf.html +193 -0
- package/docs/families/rf.md +136 -0
- package/docs/families/scheduler.html +157 -0
- package/docs/families/scheduler.md +36 -0
- package/docs/families/sensor.html +157 -0
- package/docs/families/sensor.md +36 -0
- package/docs/families/signature.html +157 -0
- package/docs/families/signature.md +36 -0
- package/docs/flatsql-host-contract.html +176 -0
- package/docs/flatsql-streaming-standard.html +375 -0
- package/docs/gpu-module-abi.html +233 -0
- package/docs/harness-family-matrix.md +135 -0
- package/docs/index.html +216 -85
- package/docs/isomorphic-pthreads.html +228 -0
- package/docs/isomorphic-pthreads.md +84 -0
- package/docs/isomorphic-sdn-runtime-plan.html +274 -0
- package/docs/language-runtime-matrix.html +182 -0
- package/docs/llms.txt +86 -0
- package/docs/module-bundle-runtime-plan.html +146 -0
- package/docs/module-publication-standard.html +503 -0
- package/docs/node-red-default-node-parity.html +201 -0
- package/docs/propagator-abi.md +45 -12
- package/docs/protect-and-sign.html +181 -0
- package/docs/protect-and-sign.md +120 -0
- package/docs/protocol-installation.html +296 -0
- package/docs/publication-submission.html +158 -0
- package/docs/publication-submission.md +78 -0
- package/docs/secrets-capability.html +206 -0
- package/docs/styles.css +860 -167
- package/docs/testing-harness.html +408 -0
- package/docs/tri-runtime-parity-gate.html +182 -0
- package/docs/tri-runtime-parity.html +203 -0
- package/include/orbpro/orbpro_event_runner.h +951 -0
- package/include/orbpro/orbpro_events_abi.h +565 -0
- package/include/orbpro/orbpro_propagator_abi.h +17 -0
- package/package.json +11 -4
- package/schemas/orbpro/Events.fbs +501 -0
- package/schemas/orbpro/Propagator.fbs +72 -9
- package/schemas/orbpro/reference-frame-crosswalk.json +108 -0
- package/schemas/orbpro/reference-frame.lock.json +98 -0
- package/src/browser.js +13 -0
- package/src/generated/orbpro/events-abi.js +206 -0
- package/src/generated/orbpro/events-abi.ts +319 -0
- package/src/generated/orbpro/propagator/reference-frame.js +17 -0
- package/src/generated/orbpro/propagator/reference-frame.ts +91 -1
- package/src/generated/orbpro/propagator-abi.js +17 -0
- package/src/generated/orbpro/propagator-abi.ts +17 -0
- package/src/generated/spacedatastandards/plg/CCT.d.ts +94 -0
- package/src/generated/spacedatastandards/plg/CCT.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/CCT.js +208 -0
- package/src/generated/spacedatastandards/plg/CCT.ts +281 -0
- package/src/generated/spacedatastandards/plg/CCTCategory.d.ts +98 -0
- package/src/generated/spacedatastandards/plg/CCTCategory.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/CCTCategory.js +177 -0
- package/src/generated/spacedatastandards/plg/CCTCategory.ts +269 -0
- package/src/generated/spacedatastandards/plg/CCTCategoryRollup.d.ts +54 -0
- package/src/generated/spacedatastandards/plg/CCTCategoryRollup.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/CCTCategoryRollup.js +106 -0
- package/src/generated/spacedatastandards/plg/CCTCategoryRollup.ts +144 -0
- package/src/generated/spacedatastandards/plg/PLG.d.ts +46 -2
- package/src/generated/spacedatastandards/plg/PLG.d.ts.map +1 -1
- package/src/generated/spacedatastandards/plg/PLG.js +78 -5
- package/src/generated/spacedatastandards/plg/PLG.ts +90 -5
- package/src/generated/spacedatastandards/plg/capabilityClass.d.ts +181 -0
- package/src/generated/spacedatastandards/plg/capabilityClass.d.ts.map +1 -0
- package/src/generated/spacedatastandards/plg/capabilityClass.js +183 -0
- package/src/generated/spacedatastandards/plg/capabilityClass.ts +208 -0
- package/src/generated/spacedatastandards/plg/main.d.ts +4 -0
- package/src/generated/spacedatastandards/plg/main.d.ts.map +1 -1
- package/src/generated/spacedatastandards/plg/main.js +4 -0
- package/src/generated/spacedatastandards/plg/main.ts +4 -0
- package/src/generated/spacedatastandards/plg/pluginCategory.d.ts +40 -2
- package/src/generated/spacedatastandards/plg/pluginCategory.d.ts.map +1 -1
- package/src/generated/spacedatastandards/plg/pluginCategory.js +40 -2
- package/src/generated/spacedatastandards/plg/pluginCategory.ts +40 -2
- package/src/host/browserModuleHarness.js +8 -0
- package/src/host/wasiThreadHost.js +189 -14
- package/src/index.d.ts +18 -1
- package/src/transport/index.js +16 -0
- package/src/transport/records.js +1015 -3
package/src/transport/records.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import * as flatbuffers from "flatbuffers/mjs/flatbuffers.js";
|
|
2
2
|
|
|
3
|
+
import { BPF, BPFT } from "spacedatastandards.org/lib/js/BPF/BPF.js";
|
|
4
|
+
import { BPFAttestationT } from "spacedatastandards.org/lib/js/BPF/BPFAttestation.js";
|
|
5
|
+
import { BPFModuleT } from "spacedatastandards.org/lib/js/BPF/BPFModule.js";
|
|
6
|
+
import { BPFPartT } from "spacedatastandards.org/lib/js/BPF/BPFPart.js";
|
|
7
|
+
import { BPFRuntimeLockT } from "spacedatastandards.org/lib/js/BPF/BPFRuntimeLock.js";
|
|
8
|
+
import { bpfLicenseMode } from "spacedatastandards.org/lib/js/BPF/bpfLicenseMode.js";
|
|
9
|
+
import { bpfPartKind } from "spacedatastandards.org/lib/js/BPF/bpfPartKind.js";
|
|
10
|
+
import { bpfProtectionTier } from "spacedatastandards.org/lib/js/BPF/bpfProtectionTier.js";
|
|
3
11
|
import {
|
|
4
12
|
ENC,
|
|
5
13
|
ENCT,
|
|
@@ -22,9 +30,15 @@ import {
|
|
|
22
30
|
base64ToBytes,
|
|
23
31
|
bytesToBase64,
|
|
24
32
|
bytesToHex,
|
|
33
|
+
hexToBytes,
|
|
25
34
|
toUint8Array,
|
|
26
35
|
} from "../utils/encoding.js";
|
|
27
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
ed25519PublicKey,
|
|
38
|
+
ed25519Sign,
|
|
39
|
+
ed25519Verify,
|
|
40
|
+
sha256Bytes,
|
|
41
|
+
} from "../utils/wasmCrypto.js";
|
|
28
42
|
|
|
29
43
|
const TRAILER_MAGIC_TEXT = "$REC";
|
|
30
44
|
const TRAILER_MAGIC_BYTES = new TextEncoder().encode(TRAILER_MAGIC_TEXT);
|
|
@@ -660,8 +674,8 @@ export function decodePublicationRecordCollection(bytes) {
|
|
|
660
674
|
const expectedStandard =
|
|
661
675
|
STANDARD_BY_RECORD_TYPE[recordType] ?? null;
|
|
662
676
|
if (standard && expectedStandard && standard !== expectedStandard) {
|
|
663
|
-
|
|
664
|
-
`REC trailer record ${index} standard/type
|
|
677
|
+
console.warn(
|
|
678
|
+
`REC trailer record ${index} standard/type drift (${standard} from Record.standard vs ${expectedStandard} from local RecordType ${recordType}); decoding by Record.standard.`,
|
|
665
679
|
);
|
|
666
680
|
}
|
|
667
681
|
const valueMeta = assertUnionTableField(
|
|
@@ -875,3 +889,1001 @@ export function decodeProtectedBlobBase64(base64) {
|
|
|
875
889
|
}
|
|
876
890
|
|
|
877
891
|
export { TRAILER_MAGIC_TEXT, TRAILER_FOOTER_LENGTH };
|
|
892
|
+
|
|
893
|
+
/* ------------------------------------------------------------------------ *
|
|
894
|
+
* $BPF — Build Profile (SDS ordinal 227, published in
|
|
895
|
+
* spacedatastandards.org 1.200.0).
|
|
896
|
+
*
|
|
897
|
+
* A build profile is the authoring-time CONFIGURATION of one composed build.
|
|
898
|
+
* It names no licensee, carries no key material, and confers no rights — it
|
|
899
|
+
* is not a grant ($LGR), not a license key ($PLK), and not a module manifest
|
|
900
|
+
* ($PLG). Per-module references here are the identity triple MODULE_ID /
|
|
901
|
+
* MODULE_VERSION / CONTENT_HASH; a descriptor is resolved through the
|
|
902
|
+
* module's own published manifest (see `extractGrantModuleDescriptor` and
|
|
903
|
+
* `decodePlgManifest`), never restated inside a profile.
|
|
904
|
+
*
|
|
905
|
+
* This route deliberately does NOT join `RECORD_TYPE_BY_STANDARD` above:
|
|
906
|
+
* that trio is the $REC publication trailer appended to a protected module
|
|
907
|
+
* artifact, which is a different transport than a portable profile export.
|
|
908
|
+
* Profile containment is `encodeBuildProfileRecordCollection` /
|
|
909
|
+
* `decodeBuildProfileRecordCollection`.
|
|
910
|
+
*
|
|
911
|
+
* SIGNING (owner dual-format signing law 2026-08-22; Themis 2026-08-30). A
|
|
912
|
+
* signed profile carries BOTH ratified signatures in one `BPFAttestation`,
|
|
913
|
+
* each verifying INDEPENDENTLY against the same literal Ed25519
|
|
914
|
+
* `SIGNING_PUBLIC_KEY`:
|
|
915
|
+
* - `SIGNATURE` — over the size-prefixed $BPF FlatBuffer
|
|
916
|
+
* with BOTH 64-byte signature payloads
|
|
917
|
+
* zeroed, vectors and offsets preserved.
|
|
918
|
+
* - `CANONICAL_JSON_SIGNATURE` — over canonical JSON in IDL field order and
|
|
919
|
+
* capitalization, no insignificant
|
|
920
|
+
* whitespace, with ONLY the two signature
|
|
921
|
+
* fields omitted (`SIGNING_PUBLIC_KEY` and
|
|
922
|
+
* `SIGNED_AT` are covered).
|
|
923
|
+
* Presence of the attestation table IS the statement that a profile is
|
|
924
|
+
* signed; there is no `SIGNED` boolean, because a self-asserted flag is
|
|
925
|
+
* attacker-controlled. Verification FAILS CLOSED: a profile whose
|
|
926
|
+
* attestation is incomplete, or either of whose signatures fails, is
|
|
927
|
+
* REJECTED and is never downgraded to "unsigned and therefore acceptable" —
|
|
928
|
+
* that downgrade is the laundering path.
|
|
929
|
+
* ------------------------------------------------------------------------ */
|
|
930
|
+
|
|
931
|
+
const BUILD_PROFILE_FILE_IDENTIFIER = "$BPF";
|
|
932
|
+
const BUILD_PROFILE_SIGNATURE_LENGTH = 64;
|
|
933
|
+
const DEFAULT_RUNTIME_LOCK_TTL_DAYS = 180;
|
|
934
|
+
const MIN_RUNTIME_LOCK_TTL_DAYS = 1;
|
|
935
|
+
const MAX_RUNTIME_LOCK_TTL_DAYS = 365;
|
|
936
|
+
const UINT64_MAX = 18446744073709551615n;
|
|
937
|
+
const UINT32_MAX = 4294967295;
|
|
938
|
+
const LOWERCASE_SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
939
|
+
const RFC3339_FIXED_MILLISECONDS =
|
|
940
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
941
|
+
const DECIMAL_UINT = /^(?:0|[1-9][0-9]*)$/;
|
|
942
|
+
|
|
943
|
+
function buildEnumMaps(enumObject) {
|
|
944
|
+
const byName = {};
|
|
945
|
+
const byValue = {};
|
|
946
|
+
for (const [name, value] of Object.entries(enumObject)) {
|
|
947
|
+
if (typeof value !== "number") continue;
|
|
948
|
+
byName[name] = value;
|
|
949
|
+
byValue[value] = name;
|
|
950
|
+
}
|
|
951
|
+
return Object.freeze({
|
|
952
|
+
byName: Object.freeze(byName),
|
|
953
|
+
byValue: Object.freeze(byValue),
|
|
954
|
+
names: Object.freeze(Object.keys(byName)),
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const BPF_PART_KIND = buildEnumMaps(bpfPartKind);
|
|
959
|
+
const BPF_PROTECTION_TIER = buildEnumMaps(bpfProtectionTier);
|
|
960
|
+
const BPF_LICENSE_MODE = buildEnumMaps(bpfLicenseMode);
|
|
961
|
+
|
|
962
|
+
function normalizeEnumName(value, maps, label, fallbackName) {
|
|
963
|
+
if (value === undefined || value === null || value === "") {
|
|
964
|
+
return fallbackName;
|
|
965
|
+
}
|
|
966
|
+
if (typeof value === "number") {
|
|
967
|
+
const name = maps.byValue[value];
|
|
968
|
+
if (name === undefined) {
|
|
969
|
+
throw new TypeError(
|
|
970
|
+
`${label} must be one of ${maps.names.join(", ")} (received ${value}).`,
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
return name;
|
|
974
|
+
}
|
|
975
|
+
const name = String(value).trim();
|
|
976
|
+
if (!Object.hasOwn(maps.byName, name)) {
|
|
977
|
+
throw new TypeError(
|
|
978
|
+
`${label} must be one of ${maps.names.join(", ")} (received ${JSON.stringify(String(value))}).`,
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
return name;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function requireNonEmptyString(value, label) {
|
|
985
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
986
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
987
|
+
}
|
|
988
|
+
return value.trim();
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function optionalTrimmedString(value, label) {
|
|
992
|
+
if (value === undefined || value === null || value === "") {
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
if (typeof value !== "string") {
|
|
996
|
+
throw new TypeError(`${label} must be a string when present.`);
|
|
997
|
+
}
|
|
998
|
+
const trimmed = value.trim();
|
|
999
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function requireLowercaseSha256(value, label) {
|
|
1003
|
+
if (typeof value !== "string" || !LOWERCASE_SHA256_HEX.test(value)) {
|
|
1004
|
+
throw new TypeError(`${label} must be 64 lowercase hexadecimal characters.`);
|
|
1005
|
+
}
|
|
1006
|
+
return value;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function optionalLowercaseSha256(value, label) {
|
|
1010
|
+
if (value === undefined || value === null || value === "") {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
return requireLowercaseSha256(value, label);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function optionalFixedMillisecondTimestamp(value, label) {
|
|
1017
|
+
if (value === undefined || value === null || value === "") {
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
if (typeof value !== "string" || !RFC3339_FIXED_MILLISECONDS.test(value)) {
|
|
1021
|
+
throw new TypeError(
|
|
1022
|
+
`${label} must be an RFC 3339 UTC timestamp with fixed milliseconds.`,
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
return value;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function requireBoolean(value, label) {
|
|
1029
|
+
if (value === undefined || value === null) {
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
if (typeof value !== "boolean") {
|
|
1033
|
+
throw new TypeError(`${label} must be a boolean.`);
|
|
1034
|
+
}
|
|
1035
|
+
return value;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function normalizeUint64(value, label) {
|
|
1039
|
+
if (value === undefined || value === null || value === "") {
|
|
1040
|
+
return 0n;
|
|
1041
|
+
}
|
|
1042
|
+
let normalized;
|
|
1043
|
+
if (typeof value === "bigint") {
|
|
1044
|
+
normalized = value;
|
|
1045
|
+
} else if (typeof value === "number") {
|
|
1046
|
+
if (!Number.isSafeInteger(value)) {
|
|
1047
|
+
throw new TypeError(
|
|
1048
|
+
`${label} must be a safe integer, a bigint, or a decimal string.`,
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
normalized = BigInt(value);
|
|
1052
|
+
} else if (typeof value === "string" && DECIMAL_UINT.test(value)) {
|
|
1053
|
+
normalized = BigInt(value);
|
|
1054
|
+
} else {
|
|
1055
|
+
throw new TypeError(
|
|
1056
|
+
`${label} must be a safe integer, a bigint, or a decimal string.`,
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
if (normalized < 0n || normalized > UINT64_MAX) {
|
|
1060
|
+
throw new RangeError(`${label} must fit in uint64.`);
|
|
1061
|
+
}
|
|
1062
|
+
return normalized;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function normalizeStringVector(value, label, { requireLeadingDot = false } = {}) {
|
|
1066
|
+
if (value === undefined || value === null) {
|
|
1067
|
+
return [];
|
|
1068
|
+
}
|
|
1069
|
+
if (!Array.isArray(value)) {
|
|
1070
|
+
throw new TypeError(`${label} must be an array of strings.`);
|
|
1071
|
+
}
|
|
1072
|
+
const seen = new Set();
|
|
1073
|
+
const out = [];
|
|
1074
|
+
for (const [index, entry] of value.entries()) {
|
|
1075
|
+
const normalized = requireNonEmptyString(entry, `${label}[${index}]`);
|
|
1076
|
+
if (requireLeadingDot && !normalized.startsWith(".")) {
|
|
1077
|
+
throw new TypeError(
|
|
1078
|
+
`${label}[${index}] must be a dot-anchored suffix rule written with its leading dot.`,
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
if (seen.has(normalized)) {
|
|
1082
|
+
throw new Error(`${label} contains the duplicate entry ${JSON.stringify(normalized)}.`);
|
|
1083
|
+
}
|
|
1084
|
+
seen.add(normalized);
|
|
1085
|
+
out.push(normalized);
|
|
1086
|
+
}
|
|
1087
|
+
return out;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function normalizeSignaturePayload(value, label) {
|
|
1091
|
+
const bytes = toUint8Array(value ?? new Uint8Array(0));
|
|
1092
|
+
if (bytes.length !== BUILD_PROFILE_SIGNATURE_LENGTH) {
|
|
1093
|
+
throw new TypeError(
|
|
1094
|
+
`${label} must be exactly ${BUILD_PROFILE_SIGNATURE_LENGTH} bytes.`,
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
return new Uint8Array(bytes);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function normalizeBuildProfilePart(part, index, seen) {
|
|
1101
|
+
if (part === null || typeof part !== "object" || Array.isArray(part)) {
|
|
1102
|
+
throw new TypeError(`BPF profile parts[${index}] must be an object.`);
|
|
1103
|
+
}
|
|
1104
|
+
const label = `BPF profile parts[${index}]`;
|
|
1105
|
+
const partId = requireNonEmptyString(part.partId, `${label}.partId`);
|
|
1106
|
+
if (seen.has(partId)) {
|
|
1107
|
+
throw new Error(
|
|
1108
|
+
`BPF profile contains the duplicate part id ${JSON.stringify(partId)}.`,
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
seen.add(partId);
|
|
1112
|
+
return {
|
|
1113
|
+
partId,
|
|
1114
|
+
kind: normalizeEnumName(part.kind, BPF_PART_KIND, `${label}.kind`, "UNSPECIFIED"),
|
|
1115
|
+
included: requireBoolean(part.included, `${label}.included`),
|
|
1116
|
+
contentSha256: optionalLowercaseSha256(part.contentSha256, `${label}.contentSha256`),
|
|
1117
|
+
byteLength: normalizeUint64(part.byteLength, `${label}.byteLength`),
|
|
1118
|
+
description: optionalTrimmedString(part.description, `${label}.description`),
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function normalizeBuildProfileModule(module, index, seen) {
|
|
1123
|
+
if (module === null || typeof module !== "object" || Array.isArray(module)) {
|
|
1124
|
+
throw new TypeError(`BPF profile modules[${index}] must be an object.`);
|
|
1125
|
+
}
|
|
1126
|
+
const label = `BPF profile modules[${index}]`;
|
|
1127
|
+
const moduleId = requireNonEmptyString(module.moduleId, `${label}.moduleId`);
|
|
1128
|
+
if (seen.has(moduleId)) {
|
|
1129
|
+
throw new Error(
|
|
1130
|
+
`BPF profile contains the duplicate module id ${JSON.stringify(moduleId)}.`,
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
seen.add(moduleId);
|
|
1134
|
+
return {
|
|
1135
|
+
moduleId,
|
|
1136
|
+
moduleVersion: optionalTrimmedString(module.moduleVersion, `${label}.moduleVersion`),
|
|
1137
|
+
included: requireBoolean(module.included, `${label}.included`),
|
|
1138
|
+
protection: normalizeEnumName(
|
|
1139
|
+
module.protection,
|
|
1140
|
+
BPF_PROTECTION_TIER,
|
|
1141
|
+
`${label}.protection`,
|
|
1142
|
+
"UNSPECIFIED",
|
|
1143
|
+
),
|
|
1144
|
+
contentHash: optionalLowercaseSha256(module.contentHash, `${label}.contentHash`),
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function normalizeBuildProfileRuntimeLock(lock) {
|
|
1149
|
+
if (lock === undefined || lock === null) {
|
|
1150
|
+
throw new TypeError(
|
|
1151
|
+
"BPF profile runtimeLock is required: an absent lock is never read as an unlocked one.",
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
if (typeof lock !== "object" || Array.isArray(lock)) {
|
|
1155
|
+
throw new TypeError("BPF profile runtimeLock must be an object.");
|
|
1156
|
+
}
|
|
1157
|
+
const ttlDays =
|
|
1158
|
+
lock.ttlDays === undefined || lock.ttlDays === null || lock.ttlDays === ""
|
|
1159
|
+
? DEFAULT_RUNTIME_LOCK_TTL_DAYS
|
|
1160
|
+
: lock.ttlDays;
|
|
1161
|
+
if (
|
|
1162
|
+
!Number.isSafeInteger(ttlDays) ||
|
|
1163
|
+
ttlDays < MIN_RUNTIME_LOCK_TTL_DAYS ||
|
|
1164
|
+
ttlDays > MAX_RUNTIME_LOCK_TTL_DAYS ||
|
|
1165
|
+
ttlDays > UINT32_MAX
|
|
1166
|
+
) {
|
|
1167
|
+
throw new RangeError(
|
|
1168
|
+
`BPF profile runtimeLock.ttlDays must be an integer ${MIN_RUNTIME_LOCK_TTL_DAYS} through ${MAX_RUNTIME_LOCK_TTL_DAYS} inclusive; an out-of-range lifetime is rejected, never clamped.`,
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
return {
|
|
1172
|
+
allowedDomains: normalizeStringVector(
|
|
1173
|
+
lock.allowedDomains,
|
|
1174
|
+
"BPF profile runtimeLock.allowedDomains",
|
|
1175
|
+
),
|
|
1176
|
+
allowedTlds: normalizeStringVector(
|
|
1177
|
+
lock.allowedTlds,
|
|
1178
|
+
"BPF profile runtimeLock.allowedTlds",
|
|
1179
|
+
{ requireLeadingDot: true },
|
|
1180
|
+
),
|
|
1181
|
+
devDomains: normalizeStringVector(
|
|
1182
|
+
lock.devDomains,
|
|
1183
|
+
"BPF profile runtimeLock.devDomains",
|
|
1184
|
+
),
|
|
1185
|
+
ttlDays,
|
|
1186
|
+
compiledAtMs: normalizeUint64(
|
|
1187
|
+
lock.compiledAtMs,
|
|
1188
|
+
"BPF profile runtimeLock.compiledAtMs",
|
|
1189
|
+
),
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function normalizeBuildProfileAttestation(attestation) {
|
|
1194
|
+
if (attestation === undefined || attestation === null) {
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
if (typeof attestation !== "object" || Array.isArray(attestation)) {
|
|
1198
|
+
throw new TypeError("BPF profile attestation must be an object when present.");
|
|
1199
|
+
}
|
|
1200
|
+
return {
|
|
1201
|
+
signingPublicKey: requireLowercaseSha256(
|
|
1202
|
+
attestation.signingPublicKey,
|
|
1203
|
+
"BPF profile attestation.signingPublicKey",
|
|
1204
|
+
),
|
|
1205
|
+
signedAt: optionalFixedMillisecondTimestamp(
|
|
1206
|
+
attestation.signedAt,
|
|
1207
|
+
"BPF profile attestation.signedAt",
|
|
1208
|
+
),
|
|
1209
|
+
signature: normalizeSignaturePayload(
|
|
1210
|
+
attestation.signature,
|
|
1211
|
+
"BPF profile attestation.signature",
|
|
1212
|
+
),
|
|
1213
|
+
canonicalJsonSignature: normalizeSignaturePayload(
|
|
1214
|
+
attestation.canonicalJsonSignature,
|
|
1215
|
+
"BPF profile attestation.canonicalJsonSignature",
|
|
1216
|
+
),
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* Validate a build profile and return its NORMALIZED canonical JS shape:
|
|
1222
|
+
* every default applied, every uint64 a BigInt, absent optional strings
|
|
1223
|
+
* `null`, and the keyed `PARTS`/`MODULES` vectors sorted by their IDL key so
|
|
1224
|
+
* that one field set always encodes to one byte sequence.
|
|
1225
|
+
*/
|
|
1226
|
+
export function validateBuildProfile(profile) {
|
|
1227
|
+
if (profile === null || typeof profile !== "object" || Array.isArray(profile)) {
|
|
1228
|
+
throw new TypeError("BPF profile must be an object.");
|
|
1229
|
+
}
|
|
1230
|
+
const parts = profile.parts ?? [];
|
|
1231
|
+
const modules = profile.modules ?? [];
|
|
1232
|
+
if (!Array.isArray(parts)) {
|
|
1233
|
+
throw new TypeError("BPF profile parts must be an array.");
|
|
1234
|
+
}
|
|
1235
|
+
if (!Array.isArray(modules)) {
|
|
1236
|
+
throw new TypeError("BPF profile modules must be an array.");
|
|
1237
|
+
}
|
|
1238
|
+
const partIds = new Set();
|
|
1239
|
+
const moduleIds = new Set();
|
|
1240
|
+
return {
|
|
1241
|
+
profileId: requireNonEmptyString(profile.profileId, "BPF profile profileId"),
|
|
1242
|
+
name: requireNonEmptyString(profile.name, "BPF profile name"),
|
|
1243
|
+
description: optionalTrimmedString(profile.description, "BPF profile description"),
|
|
1244
|
+
createdAt: optionalFixedMillisecondTimestamp(
|
|
1245
|
+
profile.createdAt,
|
|
1246
|
+
"BPF profile createdAt",
|
|
1247
|
+
),
|
|
1248
|
+
updatedAt: optionalFixedMillisecondTimestamp(
|
|
1249
|
+
profile.updatedAt,
|
|
1250
|
+
"BPF profile updatedAt",
|
|
1251
|
+
),
|
|
1252
|
+
templateSha256: requireLowercaseSha256(
|
|
1253
|
+
profile.templateSha256,
|
|
1254
|
+
"BPF profile templateSha256",
|
|
1255
|
+
),
|
|
1256
|
+
parts: parts
|
|
1257
|
+
.map((part, index) => normalizeBuildProfilePart(part, index, partIds))
|
|
1258
|
+
.sort((left, right) => (left.partId < right.partId ? -1 : left.partId > right.partId ? 1 : 0)),
|
|
1259
|
+
modules: modules
|
|
1260
|
+
.map((module, index) => normalizeBuildProfileModule(module, index, moduleIds))
|
|
1261
|
+
.sort((left, right) =>
|
|
1262
|
+
left.moduleId < right.moduleId ? -1 : left.moduleId > right.moduleId ? 1 : 0,
|
|
1263
|
+
),
|
|
1264
|
+
runtimeLock: normalizeBuildProfileRuntimeLock(profile.runtimeLock),
|
|
1265
|
+
licenseMode: normalizeEnumName(
|
|
1266
|
+
profile.licenseMode,
|
|
1267
|
+
BPF_LICENSE_MODE,
|
|
1268
|
+
"BPF profile licenseMode",
|
|
1269
|
+
"UNSPECIFIED",
|
|
1270
|
+
),
|
|
1271
|
+
attestation: normalizeBuildProfileAttestation(profile.attestation),
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function buildProfileTableFromObject(profile) {
|
|
1276
|
+
return new BPFT(
|
|
1277
|
+
profile.profileId,
|
|
1278
|
+
profile.name,
|
|
1279
|
+
profile.description,
|
|
1280
|
+
profile.createdAt,
|
|
1281
|
+
profile.updatedAt,
|
|
1282
|
+
profile.templateSha256,
|
|
1283
|
+
profile.parts.map(
|
|
1284
|
+
(part) =>
|
|
1285
|
+
new BPFPartT(
|
|
1286
|
+
part.partId,
|
|
1287
|
+
BPF_PART_KIND.byName[part.kind],
|
|
1288
|
+
part.included,
|
|
1289
|
+
part.contentSha256,
|
|
1290
|
+
part.byteLength,
|
|
1291
|
+
part.description,
|
|
1292
|
+
),
|
|
1293
|
+
),
|
|
1294
|
+
profile.modules.map(
|
|
1295
|
+
(module) =>
|
|
1296
|
+
new BPFModuleT(
|
|
1297
|
+
module.moduleId,
|
|
1298
|
+
module.moduleVersion,
|
|
1299
|
+
module.included,
|
|
1300
|
+
BPF_PROTECTION_TIER.byName[module.protection],
|
|
1301
|
+
module.contentHash,
|
|
1302
|
+
// MODULE_DESCRIPTOR is never written: a per-module reference is the
|
|
1303
|
+
// identity triple, and a profile never restates a module manifest.
|
|
1304
|
+
null,
|
|
1305
|
+
),
|
|
1306
|
+
),
|
|
1307
|
+
new BPFRuntimeLockT(
|
|
1308
|
+
profile.runtimeLock.allowedDomains,
|
|
1309
|
+
profile.runtimeLock.allowedTlds,
|
|
1310
|
+
profile.runtimeLock.devDomains,
|
|
1311
|
+
profile.runtimeLock.ttlDays,
|
|
1312
|
+
profile.runtimeLock.compiledAtMs,
|
|
1313
|
+
),
|
|
1314
|
+
BPF_LICENSE_MODE.byName[profile.licenseMode],
|
|
1315
|
+
profile.attestation
|
|
1316
|
+
? new BPFAttestationT(
|
|
1317
|
+
profile.attestation.signingPublicKey,
|
|
1318
|
+
profile.attestation.signedAt,
|
|
1319
|
+
Array.from(profile.attestation.signature),
|
|
1320
|
+
Array.from(profile.attestation.canonicalJsonSignature),
|
|
1321
|
+
)
|
|
1322
|
+
: null,
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/** Encode a validated build profile as canonical size-prefixed SDS `$BPF` bytes. */
|
|
1327
|
+
export function encodeBuildProfile(profile) {
|
|
1328
|
+
const normalized = validateBuildProfile(profile);
|
|
1329
|
+
const builder = new flatbuffers.Builder(1024);
|
|
1330
|
+
const root = buildProfileTableFromObject(normalized).pack(builder);
|
|
1331
|
+
BPF.finishSizePrefixedBPFBuffer(builder, root);
|
|
1332
|
+
return new Uint8Array(builder.asUint8Array());
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function decodeBuildProfilePart(table, index) {
|
|
1336
|
+
return {
|
|
1337
|
+
partId: table.PART_ID(),
|
|
1338
|
+
kind:
|
|
1339
|
+
BPF_PART_KIND.byValue[table.KIND()] ??
|
|
1340
|
+
(() => {
|
|
1341
|
+
throw new Error(`BPF profile parts[${index}] KIND ${table.KIND()} is unknown.`);
|
|
1342
|
+
})(),
|
|
1343
|
+
included: table.INCLUDED(),
|
|
1344
|
+
contentSha256: optionalLowercaseSha256(
|
|
1345
|
+
table.CONTENT_SHA256(),
|
|
1346
|
+
`BPF profile parts[${index}].contentSha256`,
|
|
1347
|
+
),
|
|
1348
|
+
byteLength: normalizeUint64(
|
|
1349
|
+
table.BYTE_LENGTH(),
|
|
1350
|
+
`BPF profile parts[${index}].byteLength`,
|
|
1351
|
+
),
|
|
1352
|
+
description: optionalTrimmedString(
|
|
1353
|
+
table.DESCRIPTION(),
|
|
1354
|
+
`BPF profile parts[${index}].description`,
|
|
1355
|
+
),
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function decodeBuildProfileModule(table, index) {
|
|
1360
|
+
if (table.MODULE_DESCRIPTOR()) {
|
|
1361
|
+
throw new Error(
|
|
1362
|
+
`BPF profile modules[${index}] carries an embedded MODULE_DESCRIPTOR. This route models a ` +
|
|
1363
|
+
"module reference as the identity triple MODULE_ID / MODULE_VERSION / CONTENT_HASH and " +
|
|
1364
|
+
"cannot canonically project a descriptor it does not model, so it refuses the record " +
|
|
1365
|
+
"rather than silently dropping signed bytes. Resolve the descriptor from the module's " +
|
|
1366
|
+
"own published manifest.",
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
return {
|
|
1370
|
+
moduleId: table.MODULE_ID(),
|
|
1371
|
+
moduleVersion: optionalTrimmedString(
|
|
1372
|
+
table.MODULE_VERSION(),
|
|
1373
|
+
`BPF profile modules[${index}].moduleVersion`,
|
|
1374
|
+
),
|
|
1375
|
+
included: table.INCLUDED(),
|
|
1376
|
+
protection:
|
|
1377
|
+
BPF_PROTECTION_TIER.byValue[table.PROTECTION()] ??
|
|
1378
|
+
(() => {
|
|
1379
|
+
throw new Error(
|
|
1380
|
+
`BPF profile modules[${index}] PROTECTION ${table.PROTECTION()} is unknown.`,
|
|
1381
|
+
);
|
|
1382
|
+
})(),
|
|
1383
|
+
contentHash: optionalLowercaseSha256(
|
|
1384
|
+
table.CONTENT_HASH(),
|
|
1385
|
+
`BPF profile modules[${index}].contentHash`,
|
|
1386
|
+
),
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function decodeBuildProfileRuntimeLock(table) {
|
|
1391
|
+
if (!table) {
|
|
1392
|
+
throw new Error(
|
|
1393
|
+
"BPF record is missing RUNTIME_LOCK: an absent lock is never read as an unlocked one.",
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
const readVector = (length, read) => {
|
|
1397
|
+
const out = [];
|
|
1398
|
+
for (let index = 0; index < length; index += 1) {
|
|
1399
|
+
out.push(read(index));
|
|
1400
|
+
}
|
|
1401
|
+
return out;
|
|
1402
|
+
};
|
|
1403
|
+
return {
|
|
1404
|
+
allowedDomains: readVector(table.allowedDomainsLength(), (index) =>
|
|
1405
|
+
table.ALLOWED_DOMAINS(index),
|
|
1406
|
+
),
|
|
1407
|
+
allowedTlds: readVector(table.allowedTldsLength(), (index) => table.ALLOWED_TLDS(index)),
|
|
1408
|
+
devDomains: readVector(table.devDomainsLength(), (index) => table.DEV_DOMAINS(index)),
|
|
1409
|
+
ttlDays: table.TTL_DAYS(),
|
|
1410
|
+
compiledAtMs: normalizeUint64(
|
|
1411
|
+
table.COMPILED_AT_MS(),
|
|
1412
|
+
"BPF profile runtimeLock.compiledAtMs",
|
|
1413
|
+
),
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function decodeBuildProfileAttestation(table) {
|
|
1418
|
+
if (!table) {
|
|
1419
|
+
// PRESENCE IS THE STATEMENT: an absent attestation table means unsigned.
|
|
1420
|
+
// There is no `SIGNED` boolean to consult and none is synthesized.
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
// FAIL CLOSED on an incomplete attestation. The lengths are read BEFORE the
|
|
1424
|
+
// array views are taken: an absent vector reports offset 0, and asking the
|
|
1425
|
+
// generated accessor for a view at that offset would read wherever the
|
|
1426
|
+
// vtable pointer happens to aim rather than refusing the record.
|
|
1427
|
+
for (const [length, field] of [
|
|
1428
|
+
[table.signatureLength(), "signature"],
|
|
1429
|
+
[table.canonicalJsonSignatureLength(), "canonicalJsonSignature"],
|
|
1430
|
+
]) {
|
|
1431
|
+
if (length !== BUILD_PROFILE_SIGNATURE_LENGTH) {
|
|
1432
|
+
throw new TypeError(
|
|
1433
|
+
`BPF profile attestation.${field} must be exactly ${BUILD_PROFILE_SIGNATURE_LENGTH} bytes; an attestation missing either payload is REJECTED, never read as unsigned.`,
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
return {
|
|
1438
|
+
signingPublicKey: requireLowercaseSha256(
|
|
1439
|
+
table.SIGNING_PUBLIC_KEY(),
|
|
1440
|
+
"BPF profile attestation.signingPublicKey",
|
|
1441
|
+
),
|
|
1442
|
+
signedAt: optionalFixedMillisecondTimestamp(
|
|
1443
|
+
table.SIGNED_AT(),
|
|
1444
|
+
"BPF profile attestation.signedAt",
|
|
1445
|
+
),
|
|
1446
|
+
signature: normalizeSignaturePayload(
|
|
1447
|
+
table.signatureArray(),
|
|
1448
|
+
"BPF profile attestation.signature",
|
|
1449
|
+
),
|
|
1450
|
+
canonicalJsonSignature: normalizeSignaturePayload(
|
|
1451
|
+
table.canonicalJsonSignatureArray(),
|
|
1452
|
+
"BPF profile attestation.canonicalJsonSignature",
|
|
1453
|
+
),
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function decodeBuildProfileTable(table) {
|
|
1458
|
+
const parts = [];
|
|
1459
|
+
for (let index = 0; index < table.partsLength(); index += 1) {
|
|
1460
|
+
parts.push(decodeBuildProfilePart(table.PARTS(index), index));
|
|
1461
|
+
}
|
|
1462
|
+
const modules = [];
|
|
1463
|
+
for (let index = 0; index < table.modulesLength(); index += 1) {
|
|
1464
|
+
modules.push(decodeBuildProfileModule(table.MODULES(index), index));
|
|
1465
|
+
}
|
|
1466
|
+
return validateBuildProfile({
|
|
1467
|
+
profileId: table.PROFILE_ID(),
|
|
1468
|
+
name: table.NAME(),
|
|
1469
|
+
description: table.DESCRIPTION(),
|
|
1470
|
+
createdAt: table.CREATED_AT(),
|
|
1471
|
+
updatedAt: table.UPDATED_AT(),
|
|
1472
|
+
templateSha256: table.TEMPLATE_SHA256(),
|
|
1473
|
+
parts,
|
|
1474
|
+
modules,
|
|
1475
|
+
runtimeLock: decodeBuildProfileRuntimeLock(table.RUNTIME_LOCK()),
|
|
1476
|
+
licenseMode: normalizeEnumName(
|
|
1477
|
+
table.LICENSE_MODE(),
|
|
1478
|
+
BPF_LICENSE_MODE,
|
|
1479
|
+
"BPF profile licenseMode",
|
|
1480
|
+
"UNSPECIFIED",
|
|
1481
|
+
),
|
|
1482
|
+
attestation: decodeBuildProfileAttestation(table.ATTESTATION()),
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
function assertSizePrefixedBuildProfileBuffer(bytes) {
|
|
1487
|
+
const buffer = toUint8Array(bytes);
|
|
1488
|
+
if (buffer.length < flatbuffers.SIZE_PREFIX_LENGTH + 12) {
|
|
1489
|
+
throw new TypeError("BPF record expects non-truncated size-prefixed FlatBuffer bytes.");
|
|
1490
|
+
}
|
|
1491
|
+
const declaredLength = readUint32LE(buffer, 0, "BPF record size prefix");
|
|
1492
|
+
if (declaredLength !== buffer.length - flatbuffers.SIZE_PREFIX_LENGTH) {
|
|
1493
|
+
throw new Error("BPF record size prefix does not match the buffer length.");
|
|
1494
|
+
}
|
|
1495
|
+
// Past the 4-byte size prefix the layout is an ordinary root FlatBuffer, so
|
|
1496
|
+
// the existing structural hardening applies verbatim to the subarray.
|
|
1497
|
+
assertRootFlatbufferTable(
|
|
1498
|
+
buffer.subarray(flatbuffers.SIZE_PREFIX_LENGTH),
|
|
1499
|
+
BUILD_PROFILE_FILE_IDENTIFIER,
|
|
1500
|
+
"BPF record",
|
|
1501
|
+
);
|
|
1502
|
+
return buffer;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/** Decode and revalidate canonical size-prefixed SDS `$BPF` bytes. */
|
|
1506
|
+
export function decodeBuildProfile(bytes) {
|
|
1507
|
+
const buffer = assertSizePrefixedBuildProfileBuffer(bytes);
|
|
1508
|
+
const table = BPF.getSizePrefixedRootAsBPF(new flatbuffers.ByteBuffer(buffer));
|
|
1509
|
+
return decodeBuildProfileTable(table);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function canonicalJsonObject(entries) {
|
|
1513
|
+
const fields = [];
|
|
1514
|
+
for (const [key, serialized] of entries) {
|
|
1515
|
+
if (serialized === undefined) continue;
|
|
1516
|
+
fields.push(`${JSON.stringify(key)}:${serialized}`);
|
|
1517
|
+
}
|
|
1518
|
+
return `{${fields.join(",")}}`;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function canonicalJsonStringVector(values) {
|
|
1522
|
+
return `[${values.map((value) => JSON.stringify(value)).join(",")}]`;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function canonicalJsonOptionalString(value) {
|
|
1526
|
+
return value === null || value === undefined ? undefined : JSON.stringify(value);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Project a build profile to its canonical JSON signing form: IDL field
|
|
1531
|
+
* order, IDL capitalization, no insignificant whitespace, uint64 fields as
|
|
1532
|
+
* decimal strings, and ONLY the two signature payloads omitted —
|
|
1533
|
+
* `SIGNING_PUBLIC_KEY` and `SIGNED_AT` are covered by the signature.
|
|
1534
|
+
*/
|
|
1535
|
+
export function canonicalBuildProfileJson(profile) {
|
|
1536
|
+
const normalized = validateBuildProfile(profile);
|
|
1537
|
+
return canonicalJsonObject([
|
|
1538
|
+
["PROFILE_ID", JSON.stringify(normalized.profileId)],
|
|
1539
|
+
["NAME", JSON.stringify(normalized.name)],
|
|
1540
|
+
["DESCRIPTION", canonicalJsonOptionalString(normalized.description)],
|
|
1541
|
+
["CREATED_AT", canonicalJsonOptionalString(normalized.createdAt)],
|
|
1542
|
+
["UPDATED_AT", canonicalJsonOptionalString(normalized.updatedAt)],
|
|
1543
|
+
["TEMPLATE_SHA256", JSON.stringify(normalized.templateSha256)],
|
|
1544
|
+
[
|
|
1545
|
+
"PARTS",
|
|
1546
|
+
`[${normalized.parts
|
|
1547
|
+
.map((part) =>
|
|
1548
|
+
canonicalJsonObject([
|
|
1549
|
+
["PART_ID", JSON.stringify(part.partId)],
|
|
1550
|
+
["KIND", JSON.stringify(part.kind)],
|
|
1551
|
+
["INCLUDED", part.included ? "true" : "false"],
|
|
1552
|
+
["CONTENT_SHA256", canonicalJsonOptionalString(part.contentSha256)],
|
|
1553
|
+
["BYTE_LENGTH", JSON.stringify(part.byteLength.toString())],
|
|
1554
|
+
["DESCRIPTION", canonicalJsonOptionalString(part.description)],
|
|
1555
|
+
]),
|
|
1556
|
+
)
|
|
1557
|
+
.join(",")}]`,
|
|
1558
|
+
],
|
|
1559
|
+
[
|
|
1560
|
+
"MODULES",
|
|
1561
|
+
`[${normalized.modules
|
|
1562
|
+
.map((module) =>
|
|
1563
|
+
canonicalJsonObject([
|
|
1564
|
+
["MODULE_ID", JSON.stringify(module.moduleId)],
|
|
1565
|
+
["MODULE_VERSION", canonicalJsonOptionalString(module.moduleVersion)],
|
|
1566
|
+
["INCLUDED", module.included ? "true" : "false"],
|
|
1567
|
+
["PROTECTION", JSON.stringify(module.protection)],
|
|
1568
|
+
["CONTENT_HASH", canonicalJsonOptionalString(module.contentHash)],
|
|
1569
|
+
]),
|
|
1570
|
+
)
|
|
1571
|
+
.join(",")}]`,
|
|
1572
|
+
],
|
|
1573
|
+
[
|
|
1574
|
+
"RUNTIME_LOCK",
|
|
1575
|
+
canonicalJsonObject([
|
|
1576
|
+
["ALLOWED_DOMAINS", canonicalJsonStringVector(normalized.runtimeLock.allowedDomains)],
|
|
1577
|
+
["ALLOWED_TLDS", canonicalJsonStringVector(normalized.runtimeLock.allowedTlds)],
|
|
1578
|
+
["DEV_DOMAINS", canonicalJsonStringVector(normalized.runtimeLock.devDomains)],
|
|
1579
|
+
["TTL_DAYS", String(normalized.runtimeLock.ttlDays)],
|
|
1580
|
+
["COMPILED_AT_MS", JSON.stringify(normalized.runtimeLock.compiledAtMs.toString())],
|
|
1581
|
+
]),
|
|
1582
|
+
],
|
|
1583
|
+
["LICENSE_MODE", JSON.stringify(normalized.licenseMode)],
|
|
1584
|
+
[
|
|
1585
|
+
"ATTESTATION",
|
|
1586
|
+
normalized.attestation === null
|
|
1587
|
+
? undefined
|
|
1588
|
+
: canonicalJsonObject([
|
|
1589
|
+
["SIGNING_PUBLIC_KEY", JSON.stringify(normalized.attestation.signingPublicKey)],
|
|
1590
|
+
["SIGNED_AT", canonicalJsonOptionalString(normalized.attestation.signedAt)],
|
|
1591
|
+
]),
|
|
1592
|
+
],
|
|
1593
|
+
]);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Build the `SIGNATURE` preimage: a COPY of the size-prefixed `$BPF` bytes
|
|
1598
|
+
* with both 64-byte signature payloads zeroed, vectors and offsets preserved.
|
|
1599
|
+
*/
|
|
1600
|
+
export function zeroBuildProfileSignaturePayloads(bytes) {
|
|
1601
|
+
const clone = new Uint8Array(assertSizePrefixedBuildProfileBuffer(bytes));
|
|
1602
|
+
const attestation = BPF.getSizePrefixedRootAsBPF(
|
|
1603
|
+
new flatbuffers.ByteBuffer(clone),
|
|
1604
|
+
).ATTESTATION();
|
|
1605
|
+
if (attestation) {
|
|
1606
|
+
attestation.signatureArray().fill(0);
|
|
1607
|
+
attestation.canonicalJsonSignatureArray().fill(0);
|
|
1608
|
+
}
|
|
1609
|
+
return clone;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/**
|
|
1613
|
+
* Resolve the signer AND the key it publishes together, because publishing a
|
|
1614
|
+
* `SIGNING_PUBLIC_KEY` that does not belong to the signer produces a record
|
|
1615
|
+
* whose signatures can never verify — a silent authoring failure that only
|
|
1616
|
+
* surfaces at the importer. There are exactly two shapes and neither can
|
|
1617
|
+
* guess the other's half:
|
|
1618
|
+
* - `signingSeed` — the SDK signs and derives the published key itself.
|
|
1619
|
+
* - `sign` — an external signer (a wallet, a keyslot); the caller
|
|
1620
|
+
* MUST state the `signingPublicKey` that signer holds.
|
|
1621
|
+
*/
|
|
1622
|
+
async function resolveBuildProfileSigner(options) {
|
|
1623
|
+
const declaredPublicKey = optionalTrimmedString(
|
|
1624
|
+
options.signingPublicKey,
|
|
1625
|
+
"signBuildProfile signingPublicKey",
|
|
1626
|
+
);
|
|
1627
|
+
const hasSeed = options.signingSeed !== undefined && options.signingSeed !== null;
|
|
1628
|
+
const hasSignFunction = typeof options.sign === "function";
|
|
1629
|
+
if (hasSeed && hasSignFunction) {
|
|
1630
|
+
throw new TypeError(
|
|
1631
|
+
"signBuildProfile takes options.signingSeed OR options.sign, never both: the published key must belong to the signer that produced the signatures.",
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
if (hasSignFunction) {
|
|
1635
|
+
if (!declaredPublicKey) {
|
|
1636
|
+
throw new TypeError(
|
|
1637
|
+
"signBuildProfile with options.sign requires options.signingPublicKey: the SDK cannot derive an external signer's public key, and publishing the wrong one yields a record that never verifies.",
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
return {
|
|
1641
|
+
sign: async (message) => toUint8Array(await options.sign(message)),
|
|
1642
|
+
signingPublicKey: requireLowercaseSha256(
|
|
1643
|
+
declaredPublicKey,
|
|
1644
|
+
"signBuildProfile signingPublicKey",
|
|
1645
|
+
),
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
if (!hasSeed) {
|
|
1649
|
+
throw new TypeError(
|
|
1650
|
+
"signBuildProfile requires either options.signingSeed (an Ed25519 seed) or options.sign with options.signingPublicKey.",
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
const seed = toUint8Array(options.signingSeed);
|
|
1654
|
+
const derivedPublicKey = bytesToHex(await ed25519PublicKey(seed));
|
|
1655
|
+
if (declaredPublicKey && declaredPublicKey !== derivedPublicKey) {
|
|
1656
|
+
throw new Error(
|
|
1657
|
+
"signBuildProfile signingPublicKey does not belong to options.signingSeed; the attestation would publish a key that cannot verify its own signatures.",
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
return {
|
|
1661
|
+
sign: async (message) => toUint8Array(await ed25519Sign(message, seed)),
|
|
1662
|
+
signingPublicKey: requireLowercaseSha256(
|
|
1663
|
+
derivedPublicKey,
|
|
1664
|
+
"signBuildProfile signingPublicKey",
|
|
1665
|
+
),
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/**
|
|
1670
|
+
* Sign a build profile in BOTH ratified forms and return the signed profile
|
|
1671
|
+
* plus its canonical size-prefixed `$BPF` bytes.
|
|
1672
|
+
*/
|
|
1673
|
+
export async function signBuildProfile(profile, options = {}) {
|
|
1674
|
+
const { sign, signingPublicKey } = await resolveBuildProfileSigner(options);
|
|
1675
|
+
const signedAt =
|
|
1676
|
+
optionalFixedMillisecondTimestamp(options.signedAt, "signBuildProfile signedAt") ??
|
|
1677
|
+
new Date().toISOString();
|
|
1678
|
+
const zeroSignature = new Uint8Array(BUILD_PROFILE_SIGNATURE_LENGTH);
|
|
1679
|
+
const unsignedProfile = validateBuildProfile({
|
|
1680
|
+
...validateBuildProfile(profile),
|
|
1681
|
+
attestation: {
|
|
1682
|
+
signingPublicKey,
|
|
1683
|
+
signedAt,
|
|
1684
|
+
signature: zeroSignature,
|
|
1685
|
+
canonicalJsonSignature: zeroSignature,
|
|
1686
|
+
},
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
const canonicalJson = canonicalBuildProfileJson(unsignedProfile);
|
|
1690
|
+
const canonicalJsonSignature = normalizeSignaturePayload(
|
|
1691
|
+
await sign(textEncoder.encode(canonicalJson)),
|
|
1692
|
+
"signBuildProfile canonical JSON signature",
|
|
1693
|
+
);
|
|
1694
|
+
|
|
1695
|
+
const bytes = encodeBuildProfile(unsignedProfile);
|
|
1696
|
+
const signature = normalizeSignaturePayload(
|
|
1697
|
+
await sign(zeroBuildProfileSignaturePayloads(bytes)),
|
|
1698
|
+
"signBuildProfile FlatBuffer signature",
|
|
1699
|
+
);
|
|
1700
|
+
|
|
1701
|
+
// Write both payloads into the existing 64-byte vectors: the layout the
|
|
1702
|
+
// signature was taken over is preserved byte for byte.
|
|
1703
|
+
const attestation = BPF.getSizePrefixedRootAsBPF(
|
|
1704
|
+
new flatbuffers.ByteBuffer(bytes),
|
|
1705
|
+
).ATTESTATION();
|
|
1706
|
+
attestation.signatureArray().set(signature);
|
|
1707
|
+
attestation.canonicalJsonSignatureArray().set(canonicalJsonSignature);
|
|
1708
|
+
|
|
1709
|
+
return { profile: decodeBuildProfile(bytes), bytes, canonicalJson };
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
function resolveBuildProfileVerifier(options) {
|
|
1713
|
+
if (typeof options.verify === "function") {
|
|
1714
|
+
return options.verify;
|
|
1715
|
+
}
|
|
1716
|
+
return (message, signature, publicKey) => ed25519Verify(message, signature, publicKey);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* Verify the FlatBuffer form on its own: the size-prefixed `$BPF` bytes with
|
|
1721
|
+
* both signature payloads zeroed, against the record's literal
|
|
1722
|
+
* `SIGNING_PUBLIC_KEY`.
|
|
1723
|
+
*/
|
|
1724
|
+
export async function verifyBuildProfileFlatbufferSignature(bytes, options = {}) {
|
|
1725
|
+
const profile = decodeBuildProfile(bytes);
|
|
1726
|
+
if (!profile.attestation) {
|
|
1727
|
+
return false;
|
|
1728
|
+
}
|
|
1729
|
+
const verify = resolveBuildProfileVerifier(options);
|
|
1730
|
+
return (
|
|
1731
|
+
(await verify(
|
|
1732
|
+
zeroBuildProfileSignaturePayloads(bytes),
|
|
1733
|
+
profile.attestation.signature,
|
|
1734
|
+
hexToBytes(profile.attestation.signingPublicKey),
|
|
1735
|
+
)) === true
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
/**
|
|
1740
|
+
* Verify the canonical-JSON form on its own — a holder of the JSON projection
|
|
1741
|
+
* and the signature needs no FlatBuffer to check it.
|
|
1742
|
+
*/
|
|
1743
|
+
export async function verifyBuildProfileCanonicalJsonSignature(profile, options = {}) {
|
|
1744
|
+
const normalized = validateBuildProfile(profile);
|
|
1745
|
+
if (!normalized.attestation) {
|
|
1746
|
+
return false;
|
|
1747
|
+
}
|
|
1748
|
+
const verify = resolveBuildProfileVerifier(options);
|
|
1749
|
+
return (
|
|
1750
|
+
(await verify(
|
|
1751
|
+
textEncoder.encode(canonicalBuildProfileJson(normalized)),
|
|
1752
|
+
normalized.attestation.canonicalJsonSignature,
|
|
1753
|
+
hexToBytes(normalized.attestation.signingPublicKey),
|
|
1754
|
+
)) === true
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
/**
|
|
1759
|
+
* Fail-closed verification of a `$BPF` buffer. An unsigned profile decodes as
|
|
1760
|
+
* `{ signed: false }`; a SIGNED profile whose either signature fails to
|
|
1761
|
+
* verify THROWS — it is never downgraded to unsigned and therefore
|
|
1762
|
+
* acceptable.
|
|
1763
|
+
*/
|
|
1764
|
+
export async function verifyBuildProfile(bytes, options = {}) {
|
|
1765
|
+
const profile = decodeBuildProfile(bytes);
|
|
1766
|
+
if (!profile.attestation) {
|
|
1767
|
+
return { profile, signed: false };
|
|
1768
|
+
}
|
|
1769
|
+
if (!(await verifyBuildProfileFlatbufferSignature(bytes, options))) {
|
|
1770
|
+
throw new Error(
|
|
1771
|
+
"BPF attestation SIGNATURE failed to verify against SIGNING_PUBLIC_KEY; the record is REJECTED, never read as unsigned.",
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
if (!(await verifyBuildProfileCanonicalJsonSignature(profile, options))) {
|
|
1775
|
+
throw new Error(
|
|
1776
|
+
"BPF attestation CANONICAL_JSON_SIGNATURE failed to verify against SIGNING_PUBLIC_KEY; the record is REJECTED, never read as unsigned.",
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
return { profile, signed: true };
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
/**
|
|
1783
|
+
* Encode the portable export unit: a `$REC` collection carrying exactly one
|
|
1784
|
+
* `$BPF` record.
|
|
1785
|
+
*/
|
|
1786
|
+
export function encodeBuildProfileRecordCollection(profile, options = {}) {
|
|
1787
|
+
const normalized = validateBuildProfile(profile);
|
|
1788
|
+
const builder = new flatbuffers.Builder(2048);
|
|
1789
|
+
const root = new RECT(
|
|
1790
|
+
normalizeStringField(options.version) ?? DEFAULT_RECORD_COLLECTION_VERSION,
|
|
1791
|
+
[new RecordT(RecordType.BPF, buildProfileTableFromObject(normalized), "BPF")],
|
|
1792
|
+
).pack(builder);
|
|
1793
|
+
REC.finishRECBuffer(builder, root);
|
|
1794
|
+
return builder.asUint8Array();
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/**
|
|
1798
|
+
* Decode a `$REC` collection carrying a build profile. EXACTLY ONE `$BPF`
|
|
1799
|
+
* record is admitted: zero and two-or-more are both REJECTED. Any other
|
|
1800
|
+
* record travelling with it — an `$APP` launcher manifest, for example — is
|
|
1801
|
+
* advisory metadata carrying no authority, so its presence is recorded and
|
|
1802
|
+
* its absence is not an error.
|
|
1803
|
+
*/
|
|
1804
|
+
export function decodeBuildProfileRecordCollection(bytes) {
|
|
1805
|
+
const buffer = toUint8Array(bytes);
|
|
1806
|
+
assertRootFlatbufferTable(buffer, TRAILER_MAGIC_TEXT, "BPF record collection");
|
|
1807
|
+
const bb = new flatbuffers.ByteBuffer(buffer);
|
|
1808
|
+
const collectionTable = REC.getRootAsREC(bb);
|
|
1809
|
+
const collectionMeta = assertFlatbufferTable(
|
|
1810
|
+
buffer,
|
|
1811
|
+
collectionTable.bb_pos,
|
|
1812
|
+
"BPF record collection",
|
|
1813
|
+
);
|
|
1814
|
+
assertOptionalStringField(buffer, collectionMeta, 4, "BPF record collection version");
|
|
1815
|
+
const recordTables = assertTableVectorField(
|
|
1816
|
+
buffer,
|
|
1817
|
+
collectionMeta,
|
|
1818
|
+
6,
|
|
1819
|
+
"BPF record collection records",
|
|
1820
|
+
);
|
|
1821
|
+
if (recordTables.length === 0) {
|
|
1822
|
+
throw new Error("BPF record collection does not contain any records.");
|
|
1823
|
+
}
|
|
1824
|
+
const records = [];
|
|
1825
|
+
let profile = null;
|
|
1826
|
+
for (let index = 0; index < recordTables.length; index += 1) {
|
|
1827
|
+
const recordTable = collectionTable.RECORDS(index, new Record()) ?? null;
|
|
1828
|
+
if (!recordTable) {
|
|
1829
|
+
throw new Error(`BPF record collection record ${index} could not be loaded.`);
|
|
1830
|
+
}
|
|
1831
|
+
const recordMeta = assertFlatbufferTable(
|
|
1832
|
+
buffer,
|
|
1833
|
+
recordTable.bb_pos,
|
|
1834
|
+
`BPF record collection record ${index}`,
|
|
1835
|
+
);
|
|
1836
|
+
assertOptionalStringField(
|
|
1837
|
+
buffer,
|
|
1838
|
+
recordMeta,
|
|
1839
|
+
8,
|
|
1840
|
+
`BPF record collection record ${index} standard`,
|
|
1841
|
+
);
|
|
1842
|
+
const recordType = getRecordValueType(recordTable);
|
|
1843
|
+
const standard = normalizeStringField(recordTable.standard()) ?? null;
|
|
1844
|
+
if (
|
|
1845
|
+
!assertUnionTableField(
|
|
1846
|
+
buffer,
|
|
1847
|
+
recordMeta,
|
|
1848
|
+
6,
|
|
1849
|
+
`BPF record collection record ${index} value`,
|
|
1850
|
+
)
|
|
1851
|
+
) {
|
|
1852
|
+
throw new Error(`BPF record collection record ${index} is missing a value.`);
|
|
1853
|
+
}
|
|
1854
|
+
if (recordType === RecordType.BPF) {
|
|
1855
|
+
if (profile) {
|
|
1856
|
+
throw new Error(
|
|
1857
|
+
"BPF record collection contains more than one BPF record; exactly one is admitted.",
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
const table = recordTable.value(new BPF());
|
|
1861
|
+
if (!table) {
|
|
1862
|
+
throw new Error(`BPF record collection record ${index} BPF payload is missing.`);
|
|
1863
|
+
}
|
|
1864
|
+
profile = decodeBuildProfileTable(table);
|
|
1865
|
+
}
|
|
1866
|
+
records.push({ standard, recordType });
|
|
1867
|
+
}
|
|
1868
|
+
if (!profile) {
|
|
1869
|
+
throw new Error(
|
|
1870
|
+
"BPF record collection contains no BPF record; exactly one is admitted.",
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
return {
|
|
1874
|
+
version:
|
|
1875
|
+
normalizeStringField(collectionTable.version()) ?? DEFAULT_RECORD_COLLECTION_VERSION,
|
|
1876
|
+
profile,
|
|
1877
|
+
profileBytes: encodeBuildProfile(profile),
|
|
1878
|
+
records,
|
|
1879
|
+
recordCollectionBytes: buffer,
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
export {
|
|
1884
|
+
BUILD_PROFILE_FILE_IDENTIFIER,
|
|
1885
|
+
BUILD_PROFILE_SIGNATURE_LENGTH,
|
|
1886
|
+
DEFAULT_RUNTIME_LOCK_TTL_DAYS,
|
|
1887
|
+
MAX_RUNTIME_LOCK_TTL_DAYS,
|
|
1888
|
+
MIN_RUNTIME_LOCK_TTL_DAYS,
|
|
1889
|
+
};
|