@blamejs/core 0.14.10 → 0.14.12

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.
@@ -38,13 +38,21 @@
38
38
 
39
39
  var validateOpts = require("./validate-opts");
40
40
  var lazyRequire = require("./lazy-require");
41
+ var C = require("./constants");
41
42
  var { ComplianceError } = require("./framework-error");
42
43
 
43
44
  var prohibited = require("./compliance-ai-act-prohibited");
44
45
  var risk = require("./compliance-ai-act-risk");
45
46
  var transparency = require("./compliance-ai-act-transparency");
46
47
  var logging = require("./compliance-ai-act-logging");
48
+ var safeJson = require("./safe-json");
47
49
  var audit = lazyRequire(function () { return require("./audit"); });
50
+ // modelManifest carries the CycloneDX 1.6 ML-BOM build/sign/verify
51
+ // envelope. lazyRequire'd to dodge the framework's documented
52
+ // circular-load chain (index.js → compliance-* → audit → db →
53
+ // framework-error → constants → package.json), the same reason
54
+ // ai-model-manifest reads package.json behind a call.
55
+ var modelManifest = lazyRequire(function () { return require("./ai-model-manifest"); });
48
56
 
49
57
  // ---- Article 113 deadline calendar ----
50
58
  //
@@ -795,6 +803,441 @@ function crossWalkIso23894() {
795
803
  });
796
804
  }
797
805
 
806
+ // ---- GPAI Code-of-Practice adherence declaration (Art. 53/55) ----
807
+ //
808
+ // The General-Purpose AI Code of Practice (published 10 July 2025 by
809
+ // the AI Office) is the voluntary instrument by which a GPAI provider
810
+ // demonstrates compliance with Reg (EU) 2024/1689 Art. 53 (and Art. 55
811
+ // for systemic-risk models). Signing the Code is a public adherence
812
+ // declaration; this primitive emits a cryptographically-bound,
813
+ // tamper-evident version of that declaration so the obligation-set it
814
+ // covers cannot be silently downgraded and the per-commitment evidence
815
+ // cannot be replaced with a hollow claim.
816
+ //
817
+ // COP_VERSION_RE is shape-only — it pins the documented `YYYY-MM`
818
+ // release-label form of the Code (e.g. "2025-07") with a real month
819
+ // group; it is NOT a semantic validity check (the AI Office is the
820
+ // authority on which labels exist).
821
+ var COP_VERSION_RE = /^\d{4}-(0[1-9]|1[0-2])$/; // allow:regex-no-length-cap — fixed 7-char YYYY-MM Code-of-Practice release label, fully anchored
822
+ // SHA3-512 hex digest shape, matching b.crypto.sha3Hash output (64
823
+ // bytes → 128 lowercase-hex chars). An evidenceHash that does not match
824
+ // this shape is a hollow attestation — the compliance-theater shape
825
+ // this primitive exists to refuse.
826
+ var EVIDENCE_HASH_RE = /^[0-9a-f]{128}$/; // allow:regex-no-length-cap — fixed-length SHA3-512 hex (128 chars), fully anchored
827
+
828
+ // Default validity window for a phase-in adherence declaration. The
829
+ // Art. 53 obligations become applicable 2026-08-02 (DEADLINES
830
+ // .generalPurposeAI); a declaration that predates a material model
831
+ // change should not be relied on indefinitely. 90 days is the auditor-
832
+ // review default; operators override via opts.validityMs.
833
+ var DEFAULT_VALIDITY_MS = C.TIME.days(90);
834
+
835
+ var DECLARE_ALLOWED_KEYS = [
836
+ "modelId", "modelVersion", "provider", "isSystemicRisk", "trainingFlops",
837
+ "designatedSystemicRisk", "modalities", "copVersion", "commitments",
838
+ "trainingDataSummary", "generatedAt", "validityMs", "privateKeyPem",
839
+ "serialNumber", "audit",
840
+ ];
841
+
842
+ // Derive the in-scope obligation set from the regulation, never from an
843
+ // operator-asserted list. declareAdherence is GPAI-specific, so kind is
844
+ // injected as "gpai" before gpaiClassify runs — otherwise a caller that
845
+ // omits kind/generalPurpose would classify as non-GPAI (isGpai:false →
846
+ // obligations:[]) and the scope-downgrade refusal below could never
847
+ // fire.
848
+ function _deriveGpaiObligations(opts) {
849
+ var probe = {
850
+ kind: "gpai",
851
+ trainingFlops: opts.trainingFlops,
852
+ designatedSystemicRisk: opts.designatedSystemicRisk === true ||
853
+ opts.isSystemicRisk === true ? true : undefined,
854
+ };
855
+ var verdict = gpaiClassify(probe);
856
+ return {
857
+ isSystemicRisk: verdict.isSystemicRisk,
858
+ obligations: verdict.obligations, // [{ article, title, description }, ...]
859
+ };
860
+ }
861
+
862
+ /**
863
+ * @primitive b.compliance.aiAct.gpai.adherenceForm
864
+ * @signature b.compliance.aiAct.gpai.adherenceForm(opts)
865
+ * @since 0.14.11
866
+ * @status stable
867
+ * @compliance eu-ai-act-art-11
868
+ * @related b.compliance.aiAct.gpai.declareAdherence, b.ai.modelManifest.build
869
+ *
870
+ * Build the unsigned GPAI Code-of-Practice adherence document for a
871
+ * general-purpose AI model. The obligation set is DERIVED from the
872
+ * regulation via the GPAI classifier (Reg (EU) 2024/1689 Art. 53(1)(a-d)
873
+ * always; Art. 55 when the model is a systemic-risk model under
874
+ * Art. 51(2)), never taken from an operator-supplied list. Each
875
+ * obligation is paired with the operator's commitment + evidence hash;
876
+ * the evidence hash is validated against the SHA3-512 hex shape so a
877
+ * hollow attestation (junk "hash") is refused at build time (CWE-345
878
+ * insufficient verification of data authenticity).
879
+ *
880
+ * This is the document `declareAdherence` signs; most operators call
881
+ * `declareAdherence` directly. The form is exposed for operators who
882
+ * want to inspect or persist the derived obligation set before signing.
883
+ *
884
+ * @opts
885
+ * modelId: string, // required — provider's model identifier
886
+ * modelVersion: string, // required — model version
887
+ * provider: object, // { name, address, contact }
888
+ * trainingFlops: number, // cumulative training compute (Art. 51(2) presumption at >= 1e25)
889
+ * isSystemicRisk: boolean, // operator-asserted systemic-risk designation (Art. 51(1)(b))
890
+ * designatedSystemicRisk: boolean, // AI-Office-designated systemic risk
891
+ * copVersion: string, // GPAI Code of Practice release label, "YYYY-MM" (default "2025-07")
892
+ * commitments: object[], // [{ article, statement, evidenceHash }] — evidenceHash is SHA3-512 hex
893
+ * trainingDataSummary: object,// Art. 53(1)(d) public-summary pointer (b.compliance.aiAct.gpai.trainingDataSummary)
894
+ * generatedAt: string, // ISO 8601 UTC; defaults to now
895
+ * validityMs: number, // declaration validity window; default 90 days
896
+ *
897
+ * @example
898
+ * var hash = b.crypto.sha3Hash("eval-report-2026.pdf");
899
+ * var form = b.compliance.aiAct.gpai.adherenceForm({
900
+ * modelId: "acme-llm-7b",
901
+ * modelVersion: "1.0",
902
+ * commitments: [{ article: "Art. 53(1)(a)", statement: "Annex XI docs maintained", evidenceHash: hash }],
903
+ * });
904
+ * form.commitments.length; // 4 — the four Art. 53 obligations (no systemic-risk chapter)
905
+ * form.commitments[0].evidenced; // true (Art. 53(1)(a) has a bound commitment)
906
+ * form.commitments[1].evidenced; // false (no commitment supplied yet)
907
+ */
908
+ function adherenceForm(opts) {
909
+ validateOpts.requireObject(opts, "compliance.aiAct.gpai.adherenceForm",
910
+ ComplianceError, "compliance-ai-act/bad-input");
911
+ validateOpts(opts, DECLARE_ALLOWED_KEYS, "compliance.aiAct.gpai.adherenceForm");
912
+ validateOpts.requireNonEmptyString(opts.modelId, "adherenceForm: modelId",
913
+ ComplianceError, "compliance-ai-act/no-model-id");
914
+ validateOpts.requireNonEmptyString(opts.modelVersion, "adherenceForm: modelVersion",
915
+ ComplianceError, "compliance-ai-act/no-model-version");
916
+
917
+ var copVersion = opts.copVersion || "2025-07";
918
+ if (typeof copVersion !== "string" || copVersion.length > 10 || !COP_VERSION_RE.test(copVersion)) {
919
+ throw new ComplianceError("compliance-ai-act/cop-version-bad",
920
+ "adherenceForm: copVersion must be a YYYY-MM Code-of-Practice release label — got " +
921
+ JSON.stringify(copVersion));
922
+ }
923
+
924
+ var derived = _deriveGpaiObligations(opts);
925
+ if (derived.obligations.length === 0) {
926
+ // Defensive: kind is injected "gpai" so this is unreachable on the
927
+ // happy path, but a future classifier change must not silently emit
928
+ // an empty-obligation declaration that asserts nothing.
929
+ throw new ComplianceError("compliance-ai-act/cop-no-obligations",
930
+ "adherenceForm: derived GPAI obligation set is empty — refusing to sign a declaration that covers no Art. 53 obligation");
931
+ }
932
+
933
+ var requiredArticles = derived.obligations.map(function (o) { return o.article; });
934
+
935
+ // Index operator commitments by article + validate each evidence hash.
936
+ var byArticle = Object.create(null);
937
+ var commitments = Array.isArray(opts.commitments) ? opts.commitments : [];
938
+ for (var i = 0; i < commitments.length; i += 1) {
939
+ var c = commitments[i];
940
+ if (!c || typeof c !== "object" || typeof c.article !== "string") {
941
+ throw new ComplianceError("compliance-ai-act/cop-commitment-shape",
942
+ "adherenceForm: commitments[" + i + "] must be { article, statement, evidenceHash }");
943
+ }
944
+ if (typeof c.evidenceHash !== "string" || c.evidenceHash.length !== 128 || !EVIDENCE_HASH_RE.test(c.evidenceHash)) {
945
+ throw new ComplianceError("compliance-ai-act/cop-evidence-bad-hash",
946
+ "adherenceForm: commitments[" + i + "].evidenceHash must be a SHA3-512 hex digest " +
947
+ "(128 lowercase-hex chars, b.crypto.sha3Hash output) — a 1-char junk hash is a hollow attestation");
948
+ }
949
+ byArticle[c.article] = {
950
+ article: c.article,
951
+ statement: typeof c.statement === "string" ? c.statement : null,
952
+ evidenceHash: c.evidenceHash,
953
+ };
954
+ }
955
+
956
+ // Build the per-obligation declaration. Every DERIVED obligation gets
957
+ // an entry; if the operator supplied a matching commitment it binds,
958
+ // otherwise the obligation is recorded as not-yet-evidenced (the
959
+ // verify path surfaces it, the auditor decides).
960
+ var declaredCommitments = derived.obligations.map(function (o) {
961
+ var bound = byArticle[o.article];
962
+ return {
963
+ article: o.article,
964
+ title: o.title,
965
+ description: o.description,
966
+ statement: bound ? bound.statement : null,
967
+ evidenceHash: bound ? bound.evidenceHash : null,
968
+ evidenced: !!bound,
969
+ };
970
+ });
971
+
972
+ var generatedAt = opts.generatedAt || new Date().toISOString();
973
+ var validityMs = opts.validityMs === undefined ? DEFAULT_VALIDITY_MS
974
+ : validateOpts.optionalPositiveFinite(opts.validityMs, "adherenceForm: validityMs",
975
+ ComplianceError, "compliance-ai-act/bad-validity");
976
+
977
+ return {
978
+ "$schema": "https://blamejs.com/schema/ai-act-gpai-cop-adherence-v1.json",
979
+ regulation: "EU Regulation 2024/1689 — AI Act",
980
+ articles: derived.isSystemicRisk ? ["Art. 53", "Art. 55"] : ["Art. 53"],
981
+ instrument: "GPAI Code of Practice (10 July 2025)",
982
+ copVersion: copVersion,
983
+ modelId: opts.modelId,
984
+ modelVersion: opts.modelVersion,
985
+ provider: opts.provider || { name: null, address: null, contact: null },
986
+ isSystemicRisk: derived.isSystemicRisk,
987
+ requiredArticles: requiredArticles,
988
+ commitments: declaredCommitments,
989
+ trainingDataSummary: opts.trainingDataSummary || null,
990
+ // Art. 113 phase-in deadlines are bound INTO the signed payload so a
991
+ // verifier can confirm the declaration was made against the correct
992
+ // applicability calendar (not back-dated to a different regime).
993
+ deadlines: DEADLINES,
994
+ generatedAt: generatedAt,
995
+ validityMs: validityMs,
996
+ note: "Adherence to the GPAI Code of Practice per Reg (EU) 2024/1689 Art. 53" +
997
+ (derived.isSystemicRisk ? " + Art. 55 (systemic risk)." : "."),
998
+ };
999
+ }
1000
+
1001
+ /**
1002
+ * @primitive b.compliance.aiAct.gpai.declareAdherence
1003
+ * @signature b.compliance.aiAct.gpai.declareAdherence(opts)
1004
+ * @since 0.14.11
1005
+ * @status stable
1006
+ * @compliance eu-ai-act-art-11
1007
+ * @related b.ai.modelManifest.sign, b.compliance.aiAct.gpai.adherenceForm
1008
+ *
1009
+ * Emit a SIGNED, tamper-evident GPAI Code-of-Practice adherence
1010
+ * declaration (Reg (EU) 2024/1689 Art. 53(1)(a-d); Art. 55 when the
1011
+ * model is a systemic-risk model under Art. 51(2)). The Code of
1012
+ * Practice (10 July 2025) is the AI Office's voluntary compliance
1013
+ * instrument; this primitive binds the adherence to the model + the
1014
+ * derived obligation set + the per-commitment evidence hashes inside a
1015
+ * CycloneDX 1.6 ML-BOM signed with ML-DSA-87 (FIPS 204) via
1016
+ * `b.ai.modelManifest.build` + `sign`. There is no unsigned return
1017
+ * path on the happy path: the declaration always ships inside the
1018
+ * signature envelope.
1019
+ *
1020
+ * Two compliance-theater shapes are refused structurally rather than
1021
+ * trusted:
1022
+ *
1023
+ * - Obligation-set downgrade: the in-scope obligations are DERIVED
1024
+ * from the classifier (kind injected as "gpai"), never accepted
1025
+ * from the operator. A 10^25-FLOP+ model that omits the Art. 55
1026
+ * systemic-risk chapter is refused — the classifier puts Art. 55
1027
+ * in scope, the declaration must cover it.
1028
+ * - Hollow attestation: every commitment's `evidenceHash` is checked
1029
+ * against the SHA3-512 hex shape (128 hex chars, `b.crypto.sha3Hash`
1030
+ * output). A junk "hash" cannot bind — CWE-345 (insufficient
1031
+ * verification of data authenticity) / CWE-347 (improper
1032
+ * verification of cryptographic signature).
1033
+ *
1034
+ * The signed envelope is verified with
1035
+ * `b.compliance.aiAct.gpai.verifyAdherence(envelope, publicKeyPem)`,
1036
+ * which re-canonicalizes before trusting any field (never trusts an
1037
+ * embedded signed-bytes value — the xml-crypto signature-substitution
1038
+ * class, CVE-2025-29774 / CVE-2025-29775) and rejects an expired
1039
+ * declaration (`generatedAt + validityMs < now`) so a stale adherence
1040
+ * cannot be replayed past its window.
1041
+ *
1042
+ * @opts
1043
+ * modelId: string, // required
1044
+ * modelVersion: string, // required
1045
+ * provider: object, // { name, address, contact }
1046
+ * trainingFlops: number, // cumulative training compute; Art. 51(2) presumption at >= 1e25 FLOP
1047
+ * isSystemicRisk: boolean, // operator-asserted systemic-risk designation (Art. 51(1)(b))
1048
+ * designatedSystemicRisk: boolean, // AI-Office-designated systemic risk
1049
+ * copVersion: string, // Code of Practice release label "YYYY-MM" (default "2025-07")
1050
+ * commitments: object[], // [{ article, statement, evidenceHash }]; evidenceHash is b.crypto.sha3Hash output
1051
+ * trainingDataSummary: object,// Art. 53(1)(d) public-summary pointer (b.compliance.aiAct.gpai.trainingDataSummary)
1052
+ * validityMs: number, // validity window; default 90 days
1053
+ * privateKeyPem: string, // required — ML-DSA-87 signing key (b.crypto.generateSigningKeyPair)
1054
+ * serialNumber: string, // urn:uuid:...; defaults to a fresh UUIDv4
1055
+ * audit: boolean, // emit compliance.aiact.gpai.declareadherence audit event; default true
1056
+ *
1057
+ * @example
1058
+ * var pair = b.crypto.generateSigningKeyPair("ml-dsa-87");
1059
+ * var hash = b.crypto.sha3Hash("annex-xi-technical-documentation-v1");
1060
+ * var env = b.compliance.aiAct.gpai.declareAdherence({
1061
+ * modelId: "acme-llm-7b",
1062
+ * modelVersion: "1.0",
1063
+ * commitments: [{ article: "Art. 53(1)(a)", statement: "Annex XI docs maintained", evidenceHash: hash }],
1064
+ * privateKeyPem: pair.privateKey,
1065
+ * });
1066
+ * typeof env.signature; // "string"
1067
+ */
1068
+ function declareAdherence(opts) {
1069
+ validateOpts.requireObject(opts, "compliance.aiAct.gpai.declareAdherence",
1070
+ ComplianceError, "compliance-ai-act/bad-input");
1071
+ validateOpts(opts, DECLARE_ALLOWED_KEYS, "compliance.aiAct.gpai.declareAdherence");
1072
+ validateOpts.requireNonEmptyString(opts.privateKeyPem,
1073
+ "declareAdherence: privateKeyPem", ComplianceError, "compliance-ai-act/no-signing-key");
1074
+
1075
+ var form = adherenceForm(opts);
1076
+
1077
+ // Scope-downgrade refusal: a SIGNED declaration must cover EVERY
1078
+ // derived obligation with bound evidence. The obligations are derived
1079
+ // from the classifier, not the operator, so a systemic-risk model
1080
+ // (>= 1e25 FLOP, Art. 51(2)) puts Art. 55 in scope — signing a
1081
+ // declaration that omits the Art. 55 chapter would be a silent scope
1082
+ // downgrade. adherenceForm() stays an inspection tool that surfaces
1083
+ // `evidenced: false`; the signing path refuses an unevidenced
1084
+ // obligation outright (CWE-345 insufficient verification).
1085
+ var unevidenced = form.commitments
1086
+ .filter(function (c) { return !c.evidenced; })
1087
+ .map(function (c) { return c.article; });
1088
+ if (unevidenced.length > 0) {
1089
+ throw new ComplianceError("compliance-ai-act/cop-obligation-unevidenced",
1090
+ "declareAdherence: cannot sign — required obligation(s) [" + unevidenced.join(", ") +
1091
+ "] have no bound commitment with a valid evidenceHash. A systemic-risk model must cover " +
1092
+ "the Art. 55 chapter; supply a commitment for every required article (" +
1093
+ form.requiredArticles.join(", ") + ").");
1094
+ }
1095
+
1096
+ // Compose the AIBOM substrate: the adherence form rides as a
1097
+ // property-bag + Art. 53(1)(d) training-data summary on the model
1098
+ // component, signed as one canonical-JSON-1785 byte stream. We do NOT
1099
+ // hand-roll an envelope or call canonicalJson.stringify + crypto.sign
1100
+ // directly — modelManifest.build/sign already provide CycloneDX 1.6
1101
+ // conformance + the signature-substitution defense in verify.
1102
+ var bom = modelManifest().build({
1103
+ model: {
1104
+ name: form.modelId,
1105
+ version: form.modelVersion,
1106
+ modelCard: {
1107
+ properties: [
1108
+ { name: "ai-act:gpai-cop-adherence", value: JSON.stringify(form) },
1109
+ ],
1110
+ },
1111
+ },
1112
+ serialNumber: opts.serialNumber,
1113
+ tool: { name: "@blamejs/core:compliance.aiAct.gpai.declareAdherence" },
1114
+ });
1115
+
1116
+ var envelope = modelManifest().sign(bom, {
1117
+ privateKeyPem: opts.privateKeyPem,
1118
+ audit: false, // emit our own domain-specific event below
1119
+ });
1120
+
1121
+ // Surface the adherence form alongside the signed envelope so callers
1122
+ // don't have to re-parse the BOM property to read what was declared.
1123
+ var out = {
1124
+ bom: envelope.bom,
1125
+ signature: envelope.signature,
1126
+ adherence: form,
1127
+ };
1128
+
1129
+ if (opts.audit !== false) {
1130
+ // Hot-path audit sink — drop-silent by design (rule 5). A throw
1131
+ // here would crash the caller that just produced a valid signed
1132
+ // declaration.
1133
+ try {
1134
+ audit().safeEmit({
1135
+ action: "compliance.aiact.gpai.declareadherence",
1136
+ outcome: "success",
1137
+ metadata: {
1138
+ modelId: form.modelId,
1139
+ modelVersion: form.modelVersion,
1140
+ isSystemicRisk: form.isSystemicRisk,
1141
+ articles: form.articles,
1142
+ serialNumber: envelope.bom.serialNumber,
1143
+ },
1144
+ });
1145
+ } catch (_e) { /* drop-silent — by design */ }
1146
+ }
1147
+
1148
+ return Object.freeze(out);
1149
+ }
1150
+
1151
+ /**
1152
+ * @primitive b.compliance.aiAct.gpai.verifyAdherence
1153
+ * @signature b.compliance.aiAct.gpai.verifyAdherence(envelope, publicKeyPem, opts?)
1154
+ * @since 0.14.11
1155
+ * @status stable
1156
+ * @compliance eu-ai-act-art-11
1157
+ * @related b.compliance.aiAct.gpai.declareAdherence, b.ai.modelManifest.verify
1158
+ *
1159
+ * Verify a signed GPAI Code-of-Practice adherence declaration produced
1160
+ * by `declareAdherence`. Delegates the cryptographic check to
1161
+ * `b.ai.modelManifest.verify`, which re-canonicalizes the BOM with
1162
+ * canonical-JSON-1785 before trusting any field and NEVER trusts an
1163
+ * embedded signed-bytes value (the xml-crypto signature-substitution
1164
+ * class, CVE-2025-29774 / CVE-2025-29775). On a valid signature it
1165
+ * additionally enforces the validity window: a declaration whose
1166
+ * `generatedAt + validityMs` is in the past is rejected with
1167
+ * `reason: "expired"` so a stale adherence cannot be replayed past its
1168
+ * auditor-review window. Returns `{ valid, adherence, reason }`; never
1169
+ * throws (the documented contract mirrors b.ai.modelManifest.verify).
1170
+ *
1171
+ * @opts
1172
+ * now: number, // override the comparison clock (ms epoch); default Date.now()
1173
+ * audit: boolean, // emit compliance.aiact.gpai.verifyadherence audit event; default true
1174
+ *
1175
+ * @example
1176
+ * var result = b.compliance.aiAct.gpai.verifyAdherence(env, pair.publicKey);
1177
+ * if (result.valid) result.adherence.requiredArticles; // ["Art. 53(1)(a)", ...]
1178
+ * else result.reason; // "signature-invalid" | "expired" | ...
1179
+ */
1180
+ function verifyAdherence(envelope, publicKeyPem, opts) {
1181
+ opts = opts || {};
1182
+ var inner = modelManifest().verify(envelope, publicKeyPem, { audit: false });
1183
+ if (!inner.valid) {
1184
+ return { valid: false, adherence: null, reason: inner.reason };
1185
+ }
1186
+
1187
+ // Re-read the adherence form from the (now signature-verified) BOM
1188
+ // property — never from a top-level field a caller could swap.
1189
+ var adherence = _extractAdherenceFromBom(inner.bom);
1190
+ if (!adherence) {
1191
+ return { valid: false, adherence: null, reason: "adherence-property-missing" };
1192
+ }
1193
+
1194
+ // Anti-replay: reject an expired declaration.
1195
+ if (typeof adherence.generatedAt === "string" &&
1196
+ typeof adherence.validityMs === "number" && isFinite(adherence.validityMs)) {
1197
+ var issuedMs = Date.parse(adherence.generatedAt);
1198
+ if (isFinite(issuedMs)) {
1199
+ var now = typeof opts.now === "number" && isFinite(opts.now) ? opts.now : Date.now();
1200
+ if (issuedMs + adherence.validityMs < now) {
1201
+ return { valid: false, adherence: null, reason: "expired" };
1202
+ }
1203
+ }
1204
+ }
1205
+
1206
+ if (opts.audit !== false) {
1207
+ try {
1208
+ audit().safeEmit({
1209
+ action: "compliance.aiact.gpai.verifyadherence",
1210
+ outcome: "success",
1211
+ metadata: {
1212
+ modelId: adherence.modelId,
1213
+ modelVersion: adherence.modelVersion,
1214
+ isSystemicRisk: adherence.isSystemicRisk,
1215
+ serialNumber: inner.bom && inner.bom.serialNumber,
1216
+ },
1217
+ });
1218
+ } catch (_e) { /* drop-silent — by design */ }
1219
+ }
1220
+
1221
+ return { valid: true, adherence: adherence, reason: null };
1222
+ }
1223
+
1224
+ // Pull the adherence form out of the signed BOM's model-card property
1225
+ // bag. Returns null on any shape mismatch — the caller maps that to a
1226
+ // structured verify reason rather than throwing.
1227
+ function _extractAdherenceFromBom(bom) {
1228
+ try {
1229
+ var card = bom && bom.metadata && bom.metadata.component && bom.metadata.component.modelCard;
1230
+ var props = card && card.properties;
1231
+ if (!Array.isArray(props)) return null;
1232
+ for (var i = 0; i < props.length; i += 1) {
1233
+ if (props[i] && props[i].name === "ai-act:gpai-cop-adherence") {
1234
+ return safeJson.parse(props[i].value, { maxBytes: C.BYTES.mib(1) });
1235
+ }
1236
+ }
1237
+ } catch (_e) { return null; }
1238
+ return null;
1239
+ }
1240
+
798
1241
  module.exports = {
799
1242
  classify: classify,
800
1243
  deployerChecklist: deployerChecklist,
@@ -806,6 +1249,9 @@ module.exports = {
806
1249
  classify: gpaiClassify,
807
1250
  listObligations: listGpaiObligations,
808
1251
  trainingDataSummary: trainingDataSummary,
1252
+ adherenceForm: adherenceForm,
1253
+ declareAdherence: declareAdherence,
1254
+ verifyAdherence: verifyAdherence,
809
1255
  OBLIGATIONS: GPAI_OBLIGATIONS,
810
1256
  },
811
1257
  articleObligations: articleObligations,