@adcp/sdk 16.0.0-beta.3 → 16.0.0-beta.5

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.
Files changed (185) hide show
  1. package/dist/lib/auth/oauth/diagnose.js +3 -11
  2. package/dist/lib/auth/oauth/diagnose.mjs +4 -12
  3. package/dist/lib/auth/oauth/diagnostics.d.mts +2 -1
  4. package/dist/lib/auth/oauth/diagnostics.d.ts +2 -1
  5. package/dist/lib/auth/oauth/diagnostics.js +8 -10
  6. package/dist/lib/auth/oauth/diagnostics.mjs +7 -10
  7. package/dist/lib/index.d.mts +1 -1
  8. package/dist/lib/index.d.ts +1 -1
  9. package/dist/lib/index.js +2 -0
  10. package/dist/lib/index.mjs +7 -1
  11. package/dist/lib/reporting/consumer-runtime.js +2 -1
  12. package/dist/lib/reporting/consumer-runtime.mjs +2 -1
  13. package/dist/lib/reporting/ledger/handler.js +50 -11
  14. package/dist/lib/reporting/ledger/handler.mjs +50 -12
  15. package/dist/lib/reporting/ledger/index.d.mts +17 -14
  16. package/dist/lib/reporting/ledger/index.d.ts +17 -14
  17. package/dist/lib/reporting/ledger/index.js +32 -20
  18. package/dist/lib/reporting/ledger/index.mjs +21 -10
  19. package/dist/lib/reporting/ledger/offering-validation.d.mts +9 -0
  20. package/dist/lib/reporting/ledger/offering-validation.d.ts +9 -0
  21. package/dist/lib/reporting/{gcs/bucket-policy.js → ledger/offering-validation.js} +9 -8
  22. package/dist/lib/reporting/ledger/offering-validation.mjs +8 -0
  23. package/dist/lib/reporting/ledger/receipts.d.mts +48 -0
  24. package/dist/lib/reporting/ledger/receipts.d.ts +48 -0
  25. package/dist/lib/reporting/ledger/receipts.js +186 -0
  26. package/dist/lib/reporting/ledger/receipts.mjs +161 -0
  27. package/dist/lib/reporting/ledger/status-ingest.js +3 -3
  28. package/dist/lib/reporting/ledger/status-ingest.mjs +4 -5
  29. package/dist/lib/reporting/ledger/types.d.mts +43 -0
  30. package/dist/lib/reporting/ledger/types.d.ts +43 -0
  31. package/dist/lib/reporting/ledger/types.js +31 -2
  32. package/dist/lib/reporting/ledger/types.mjs +26 -1
  33. package/dist/lib/reporting/pagination.d.mts +6 -0
  34. package/dist/lib/reporting/pagination.d.ts +6 -0
  35. package/dist/lib/reporting/{gcs/errors.js → pagination.js} +12 -17
  36. package/dist/lib/reporting/pagination.mjs +12 -0
  37. package/dist/lib/reporting/reconciliation.d.mts +1 -0
  38. package/dist/lib/reporting/reconciliation.d.ts +1 -0
  39. package/dist/lib/reporting/reconciliation.js +3 -2
  40. package/dist/lib/reporting/reconciliation.mjs +3 -2
  41. package/dist/lib/reporting/service/index.d.mts +79 -153
  42. package/dist/lib/reporting/service/index.d.ts +79 -153
  43. package/dist/lib/reporting/service/index.js +34 -491
  44. package/dist/lib/reporting/service/index.mjs +36 -510
  45. package/dist/lib/reporting/service/scheduler-support.d.mts +43 -0
  46. package/dist/lib/reporting/service/scheduler-support.d.ts +43 -0
  47. package/dist/lib/reporting/service/scheduler-support.js +125 -0
  48. package/dist/lib/reporting/service/scheduler-support.mjs +96 -0
  49. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  50. package/dist/lib/signing/canonicalize.d.mts +4 -0
  51. package/dist/lib/signing/canonicalize.d.ts +4 -0
  52. package/dist/lib/signing/canonicalize.js +14 -8
  53. package/dist/lib/signing/canonicalize.mjs +14 -8
  54. package/dist/lib/signing/content-digest.d.mts +8 -0
  55. package/dist/lib/signing/content-digest.d.ts +8 -0
  56. package/dist/lib/signing/content-digest.js +11 -5
  57. package/dist/lib/signing/content-digest.mjs +10 -5
  58. package/dist/lib/signing/signer.js +9 -6
  59. package/dist/lib/signing/signer.mjs +10 -7
  60. package/dist/lib/signing/web-bot-auth.js +103 -37
  61. package/dist/lib/signing/web-bot-auth.mjs +108 -37
  62. package/dist/lib/testing/storyboard/junit.js +16 -3
  63. package/dist/lib/testing/storyboard/junit.mjs +16 -3
  64. package/dist/lib/testing/storyboard/request-signing/builder.js +10 -9
  65. package/dist/lib/testing/storyboard/request-signing/builder.mjs +10 -9
  66. package/dist/lib/testing/storyboard/runner.js +94 -25
  67. package/dist/lib/testing/storyboard/runner.mjs +94 -25
  68. package/dist/lib/testing/storyboard/types.d.mts +6 -0
  69. package/dist/lib/testing/storyboard/types.d.ts +6 -0
  70. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  71. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  72. package/dist/lib/types/schemas.generated.d.ts +907 -60
  73. package/dist/lib/types/schemas.generated.js +7 -7
  74. package/dist/lib/types/schemas.generated.mjs +7 -7
  75. package/dist/lib/version.d.mts +3 -3
  76. package/dist/lib/version.d.ts +3 -3
  77. package/dist/lib/version.js +3 -3
  78. package/dist/lib/version.mjs +3 -3
  79. package/docs/TYPE-SUMMARY.md +23 -5
  80. package/docs/guides/REPORTING-GCS-FENCE.md +3 -3
  81. package/docs/guides/REPORTING-GCS-MANAGED.md +4 -4
  82. package/docs/guides/REPORTING-LEDGER-CUTOVER.md +7 -7
  83. package/docs/guides/REPORTING-LEDGER.md +107 -51
  84. package/docs/guides/REPORTING-OPERATIONS.md +66 -5
  85. package/docs/guides/REPORTING-STABILITY.md +36 -32
  86. package/docs/llms.txt +4 -3
  87. package/docs/migration-14.x-rc-worksheet.md +4 -7
  88. package/docs/migration-reporting-storage-package.md +186 -0
  89. package/examples/README.md +1 -1
  90. package/package.json +13 -78
  91. package/dist/lib/reporting/azure/index.d.mts +0 -31
  92. package/dist/lib/reporting/azure/index.d.ts +0 -31
  93. package/dist/lib/reporting/azure/index.js +0 -266
  94. package/dist/lib/reporting/azure/index.mjs +0 -249
  95. package/dist/lib/reporting/bigquery/index.d.mts +0 -246
  96. package/dist/lib/reporting/bigquery/index.d.ts +0 -246
  97. package/dist/lib/reporting/bigquery/index.js +0 -633
  98. package/dist/lib/reporting/bigquery/index.mjs +0 -594
  99. package/dist/lib/reporting/gcs/adapter.d.mts +0 -38
  100. package/dist/lib/reporting/gcs/adapter.d.ts +0 -38
  101. package/dist/lib/reporting/gcs/adapter.js +0 -342
  102. package/dist/lib/reporting/gcs/adapter.mjs +0 -325
  103. package/dist/lib/reporting/gcs/bucket-policy.d.mts +0 -3
  104. package/dist/lib/reporting/gcs/bucket-policy.d.ts +0 -3
  105. package/dist/lib/reporting/gcs/bucket-policy.mjs +0 -7
  106. package/dist/lib/reporting/gcs/errors.d.mts +0 -11
  107. package/dist/lib/reporting/gcs/errors.d.ts +0 -11
  108. package/dist/lib/reporting/gcs/errors.mjs +0 -16
  109. package/dist/lib/reporting/gcs/fence.d.mts +0 -41
  110. package/dist/lib/reporting/gcs/fence.d.ts +0 -41
  111. package/dist/lib/reporting/gcs/fence.js +0 -250
  112. package/dist/lib/reporting/gcs/fence.mjs +0 -230
  113. package/dist/lib/reporting/gcs/index.d.mts +0 -18
  114. package/dist/lib/reporting/gcs/index.d.ts +0 -18
  115. package/dist/lib/reporting/gcs/index.js +0 -44
  116. package/dist/lib/reporting/gcs/index.mjs +0 -14
  117. package/dist/lib/reporting/gcs/io.d.mts +0 -7
  118. package/dist/lib/reporting/gcs/io.d.ts +0 -7
  119. package/dist/lib/reporting/gcs/io.js +0 -95
  120. package/dist/lib/reporting/gcs/io.mjs +0 -69
  121. package/dist/lib/reporting/gcs/reader.d.mts +0 -67
  122. package/dist/lib/reporting/gcs/reader.d.ts +0 -67
  123. package/dist/lib/reporting/gcs/reader.js +0 -193
  124. package/dist/lib/reporting/gcs/reader.mjs +0 -169
  125. package/dist/lib/reporting/gcs/row-provider.d.mts +0 -39
  126. package/dist/lib/reporting/gcs/row-provider.d.ts +0 -39
  127. package/dist/lib/reporting/gcs/row-provider.js +0 -225
  128. package/dist/lib/reporting/gcs/row-provider.mjs +0 -208
  129. package/dist/lib/reporting/gcs/validation.d.mts +0 -5
  130. package/dist/lib/reporting/gcs/validation.d.ts +0 -5
  131. package/dist/lib/reporting/gcs/validation.js +0 -49
  132. package/dist/lib/reporting/gcs/validation.mjs +0 -25
  133. package/dist/lib/reporting/ledger/change-feed.d.mts +0 -69
  134. package/dist/lib/reporting/ledger/change-feed.d.ts +0 -69
  135. package/dist/lib/reporting/ledger/change-feed.js +0 -107
  136. package/dist/lib/reporting/ledger/change-feed.mjs +0 -80
  137. package/dist/lib/reporting/ledger/managed-postgres.d.mts +0 -293
  138. package/dist/lib/reporting/ledger/managed-postgres.d.ts +0 -293
  139. package/dist/lib/reporting/ledger/managed-postgres.js +0 -2346
  140. package/dist/lib/reporting/ledger/managed-postgres.mjs +0 -2332
  141. package/dist/lib/reporting/ledger/managed-router.d.mts +0 -148
  142. package/dist/lib/reporting/ledger/managed-router.d.ts +0 -148
  143. package/dist/lib/reporting/ledger/managed-router.js +0 -280
  144. package/dist/lib/reporting/ledger/managed-router.mjs +0 -258
  145. package/dist/lib/reporting/ledger/managed.d.mts +0 -535
  146. package/dist/lib/reporting/ledger/managed.d.ts +0 -535
  147. package/dist/lib/reporting/ledger/managed.js +0 -761
  148. package/dist/lib/reporting/ledger/managed.mjs +0 -729
  149. package/dist/lib/reporting/ledger/notification-activity.d.mts +0 -332
  150. package/dist/lib/reporting/ledger/notification-activity.d.ts +0 -332
  151. package/dist/lib/reporting/ledger/notification-activity.js +0 -1366
  152. package/dist/lib/reporting/ledger/notification-activity.mjs +0 -1337
  153. package/dist/lib/reporting/ledger/object-writes.d.mts +0 -128
  154. package/dist/lib/reporting/ledger/object-writes.d.ts +0 -128
  155. package/dist/lib/reporting/ledger/object-writes.js +0 -147
  156. package/dist/lib/reporting/ledger/object-writes.mjs +0 -114
  157. package/dist/lib/reporting/ledger/postgres.d.mts +0 -689
  158. package/dist/lib/reporting/ledger/postgres.d.ts +0 -689
  159. package/dist/lib/reporting/ledger/postgres.js +0 -4462
  160. package/dist/lib/reporting/ledger/postgres.mjs +0 -4462
  161. package/dist/lib/reporting/ledger/row-storage-migration.d.mts +0 -59
  162. package/dist/lib/reporting/ledger/row-storage-migration.d.ts +0 -59
  163. package/dist/lib/reporting/ledger/row-storage-migration.js +0 -365
  164. package/dist/lib/reporting/ledger/row-storage-migration.mjs +0 -339
  165. package/dist/lib/reporting/ledger/row-storage-object.d.mts +0 -203
  166. package/dist/lib/reporting/ledger/row-storage-object.d.ts +0 -203
  167. package/dist/lib/reporting/ledger/row-storage-object.js +0 -379
  168. package/dist/lib/reporting/ledger/row-storage-object.mjs +0 -335
  169. package/dist/lib/reporting/ledger/row-storage-provider-io.d.mts +0 -44
  170. package/dist/lib/reporting/ledger/row-storage-provider-io.d.ts +0 -44
  171. package/dist/lib/reporting/ledger/row-storage-provider-io.js +0 -142
  172. package/dist/lib/reporting/ledger/row-storage-provider-io.mjs +0 -111
  173. package/dist/lib/reporting/ledger/row-storage.d.mts +0 -316
  174. package/dist/lib/reporting/ledger/row-storage.d.ts +0 -316
  175. package/dist/lib/reporting/ledger/row-storage.js +0 -812
  176. package/dist/lib/reporting/ledger/row-storage.mjs +0 -802
  177. package/dist/lib/reporting/s3/index.d.mts +0 -34
  178. package/dist/lib/reporting/s3/index.d.ts +0 -34
  179. package/dist/lib/reporting/s3/index.js +0 -347
  180. package/dist/lib/reporting/s3/index.mjs +0 -340
  181. package/dist/lib/reporting/service/maintenance.d.mts +0 -148
  182. package/dist/lib/reporting/service/maintenance.d.ts +0 -148
  183. package/dist/lib/reporting/service/maintenance.js +0 -185
  184. package/dist/lib/reporting/service/maintenance.mjs +0 -159
  185. package/examples/reliable-reporting-service/README.md +0 -169
@@ -5,7 +5,7 @@ import {
5
5
  buildSignatureBase,
6
6
  formatSignatureParams
7
7
  } from "./canonicalize.mjs";
8
- import { computeContentDigest } from "./content-digest.mjs";
8
+ import { computeContentDigest, serializeByteSequenceMember } from "./content-digest.mjs";
9
9
  import { RequestSignatureError, ResponseSignatureError, WebhookSignatureError } from "./errors.mjs";
10
10
  import {
11
11
  MANDATORY_COMPONENTS,
@@ -59,6 +59,7 @@ function prepareRequestSignature(request, identity, options = {}) {
59
59
  const windowSeconds = Math.min(options.windowSeconds ?? 300, MAX_SIGNATURE_WINDOW_SECONDS);
60
60
  const nonce = options.nonce ?? base64UrlRandom(16);
61
61
  const label = options.label ?? "sig1";
62
+ assertLabelAndClock(label, now);
62
63
  const hasBody = (request.body ?? "").length > 0;
63
64
  const binaryEncoding = options.binaryEncoding ?? "legacy-base64url";
64
65
  const coverDigest = options.coverContentDigest ?? binaryEncoding === "rfc8941-base64";
@@ -89,11 +90,8 @@ function prepareRequestSignature(request, identity, options = {}) {
89
90
  }
90
91
  function finalizeRequestSignature(prepared, signature) {
91
92
  const headers = { ...prepared.headers };
92
- const sigB64 = Buffer.from(signature).toString(
93
- prepared.binaryEncoding === "legacy-base64url" ? "base64url" : "base64"
94
- );
95
93
  headers["Signature-Input"] = `${prepared.label}=${formatSignatureParams(prepared.components, prepared.params)}`;
96
- headers["Signature"] = `${prepared.label}=:${sigB64}:`;
94
+ headers["Signature"] = serializeByteSequenceMember(prepared.label, signature, prepared.binaryEncoding);
97
95
  return { headers, signatureBase: prepared.base, params: prepared.params };
98
96
  }
99
97
  function signRequest(request, key, options = {}) {
@@ -115,6 +113,7 @@ function prepareWebhookSignature(request, identity, options = {}) {
115
113
  const windowSeconds = Math.min(options.windowSeconds ?? 300, MAX_SIGNATURE_WINDOW_SECONDS);
116
114
  const nonce = options.nonce ?? base64UrlRandom(16);
117
115
  const label = options.label ?? "sig1";
116
+ assertLabelAndClock(label, now);
118
117
  const headers = { ...flattenHeaders(request.headers) };
119
118
  headers["Content-Digest"] = computeContentDigest(request.body ?? "", "legacy-base64url");
120
119
  const components = [...WEBHOOK_MANDATORY_COMPONENTS];
@@ -141,6 +140,7 @@ function prepareResponseSignature(response, identity, options = {}) {
141
140
  const windowSeconds = Math.min(options.windowSeconds ?? 300, MAX_SIGNATURE_WINDOW_SECONDS);
142
141
  const nonce = options.nonce ?? base64UrlRandom(16);
143
142
  const label = options.label ?? "sig1";
143
+ assertLabelAndClock(label, now);
144
144
  const hasBody = (response.body ?? "").length > 0;
145
145
  const coverDigest = (options.coverContentDigest ?? true) && hasBody;
146
146
  const headers = { ...flattenHeaders(response.headers) };
@@ -169,9 +169,8 @@ function prepareResponseSignature(response, identity, options = {}) {
169
169
  }
170
170
  function finalizeResponseSignature(prepared, signature) {
171
171
  const headers = { ...prepared.headers };
172
- const sigB64 = Buffer.from(signature).toString("base64url");
173
172
  headers["Signature-Input"] = `${prepared.label}=${formatSignatureParams(prepared.components, prepared.params)}`;
174
- headers["Signature"] = `${prepared.label}=:${sigB64}:`;
173
+ headers["Signature"] = serializeByteSequenceMember(prepared.label, signature, "legacy-base64url");
175
174
  return { status: prepared.status, headers, signatureBase: prepared.base, params: prepared.params };
176
175
  }
177
176
  function signResponse(response, key, options = {}) {
@@ -191,6 +190,10 @@ function flattenHeaders(headers) {
191
190
  }
192
191
  return out;
193
192
  }
193
+ function assertLabelAndClock(label, now) {
194
+ if (!/^[a-z*][a-z0-9_.*-]*$/.test(label)) throw new TypeError(`Signature label "${label}" is not an RFC 8941 key`);
195
+ if (!Number.isSafeInteger(now)) throw new TypeError("Signature clock must return integer epoch seconds");
196
+ }
194
197
  function base64UrlRandom(byteLength) {
195
198
  return randomBytes(byteLength).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
196
199
  }
@@ -51,6 +51,7 @@ module.exports = __toCommonJS(web_bot_auth_exports);
51
51
  var import_version = require('../version.js');
52
52
  var import_crypto = require("crypto");
53
53
  var import_structured_headers = require("structured-headers");
54
+ var import_http_message_sig = require("http-message-sig");
54
55
  var import_net = require('../net/index.js');
55
56
  var import_etld = require('./agent-resolver/etld.js');
56
57
  var import_canonicalize = require('./agent-resolver/canonicalize.js');
@@ -176,11 +177,11 @@ function receivedAgentOrigin(value) {
176
177
  }
177
178
  }
178
179
  function buildWebBotAuthSignatureBase(input, request, response) {
179
- const lines = input.components.map((component) => {
180
- const bare = component.split(";", 1)[0];
180
+ const lines = input.components.map((component2) => {
181
+ const bare = component2.split(";", 1)[0];
181
182
  let identifier;
182
183
  try {
183
- identifier = (0, import_structured_headers.parseItem)(`"${bare}"${component.slice(bare.length)}`);
184
+ identifier = (0, import_structured_headers.parseItem)(`"${bare}"${component2.slice(bare.length)}`);
184
185
  } catch {
185
186
  return bad("component");
186
187
  }
@@ -398,8 +399,8 @@ async function verifyWebBotAuth(request, options) {
398
399
  (c) => c.startsWith("signature-agent;key=") && !verified.some((s) => selected.find((i) => i.label === s.label).components.includes(c))
399
400
  );
400
401
  if (agentComponents.length !== 1) denied("agent_not_covered");
401
- const component = (0, import_structured_headers.parseItem)(`"${agentComponents[0].replace("signature-agent;", 'signature-agent";')}`);
402
- const memberKey = component[1].get("key");
402
+ const component2 = (0, import_structured_headers.parseItem)(`"${agentComponents[0].replace("signature-agent;", 'signature-agent";')}`);
403
+ const memberKey = component2[1].get("key");
403
404
  if (typeof memberKey !== "string" || memberKey !== input.label) bad("agent_member");
404
405
  const member = agents.get(memberKey);
405
406
  if (!member) denied("agent_not_covered");
@@ -667,7 +668,7 @@ class SignatureDirectoryResolver {
667
668
  return this.getVerifiedDirectory(origin)?.proof;
668
669
  }
669
670
  }
670
- async function signBase(base, key) {
671
+ function webBotSigner(key) {
671
672
  const jwk = "privateKey" in key ? key.privateKey : key.publicKey;
672
673
  const alg = algorithm(jwk);
673
674
  if (!["test", "development"].includes(process.env.NODE_ENV ?? "") && TEST_KIDS.has(jwkThumbprint(jwk)))
@@ -675,17 +676,40 @@ async function signBase(base, key) {
675
676
  if ("provider" in key) {
676
677
  if (key.provider.keyid !== jwkThumbprint(jwk) || key.provider.algorithm !== alg)
677
678
  throw new TypeError("Web Bot Auth provider identity does not match its public key");
678
- return key.provider.sign(Buffer.from(base));
679
+ return { algorithm: alg, sign: async (data) => Buffer.from(await key.provider.sign(Buffer.from(data))) };
679
680
  }
680
681
  if (alg === "rsa-pss-sha512") {
681
682
  const privateKey = (0, import_crypto.createPrivateKey)({ key: jwk, format: "jwk" });
682
- return (0, import_crypto.sign)("sha512", Buffer.from(base), {
683
- key: privateKey,
684
- padding: import_crypto.constants.RSA_PKCS1_PSS_PADDING,
685
- saltLength: 64
686
- });
683
+ return {
684
+ algorithm: alg,
685
+ sign: (data) => (0, import_crypto.sign)("sha512", Buffer.from(data), {
686
+ key: privateKey,
687
+ padding: import_crypto.constants.RSA_PKCS1_PSS_PADDING,
688
+ saltLength: 64
689
+ })
690
+ };
687
691
  }
688
- return (0, import_crypto2.signWithPrivateJwkAsync)(alg, jwk, Buffer.from(base));
692
+ return { algorithm: alg, sign: (data) => (0, import_crypto2.signWithPrivateJwkAsync)(alg, jwk, Buffer.from(data)) };
693
+ }
694
+ async function createWebBotSignature(...args) {
695
+ try {
696
+ return await (0, import_http_message_sig.createSignature)(...args);
697
+ } catch (error) {
698
+ if ((0, import_http_message_sig.isSignatureError)(error)) throw new WebBotAuthError(400, "component_value");
699
+ throw error;
700
+ }
701
+ }
702
+ function componentDescriptor(identifier) {
703
+ const bare = identifier.split(";", 1)[0];
704
+ const [, params] = (0, import_structured_headers.parseItem)(`"${bare}"${identifier.slice(bare.length)}`);
705
+ return (0, import_http_message_sig.component)(bare, Object.fromEntries(params));
706
+ }
707
+ function coveredFields(components, headers) {
708
+ const names = new Set(components.filter((c) => !c.name.startsWith("@")).map((c) => c.name));
709
+ return [...names].flatMap((name) => {
710
+ const value = (0, import_canonicalize2.getHeaderValue)(headers, name);
711
+ return value === void 0 ? [] : [{ name, value }];
712
+ });
689
713
  }
690
714
  async function signWebBotAuth(request, key, options) {
691
715
  const origin = (0, import_canonicalize.canonicalAgentOrigin)(options.origin);
@@ -695,6 +719,7 @@ async function signWebBotAuth(request, key, options) {
695
719
  const now = Math.floor((options.now ?? (() => Date.now() / 1e3))());
696
720
  const window = options.windowSeconds ?? 300;
697
721
  const nonce = options.nonce ?? (0, import_crypto.randomBytes)(64).toString("base64url");
722
+ if (!Number.isSafeInteger(now)) throw new TypeError("Invalid Web Bot Auth clock");
698
723
  if (!Number.isInteger(window) || window < 1 || window > 300 || !/^[A-Za-z0-9_+\/-]{22,256}={0,2}$/.test(nonce))
699
724
  throw new TypeError("Invalid Web Bot Auth lifetime or nonce");
700
725
  if (!options.relay && ["signature", "signature-input", "signature-agent"].some(
@@ -707,42 +732,69 @@ async function signWebBotAuth(request, key, options) {
707
732
  for (const [name, value] of Object.entries(request.headers))
708
733
  if (value !== void 0 && !["signature", "signature-input", "signature-agent", "content-digest"].includes(name.toLowerCase()))
709
734
  headers[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
710
- const components = ['"@method"', '"@target-uri"', '"@authority"'];
735
+ const components = [(0, import_http_message_sig.component)("@method"), (0, import_http_message_sig.component)("@target-uri"), (0, import_http_message_sig.component)("@authority")];
711
736
  if (request.body !== void 0 && request.body !== "") {
712
737
  if (!(0, import_canonicalize2.getHeaderValue)(headers, "content-type"))
713
738
  throw new TypeError("Body-bearing Web Bot Auth requests need Content-Type");
714
739
  headers["content-digest"] = (0, import_content_digest.computeContentDigest)(request.body, "rfc8941-base64");
715
- components.push('"content-type"', '"content-digest"');
740
+ components.push((0, import_http_message_sig.component)("content-type"), (0, import_http_message_sig.component)("content-digest"));
716
741
  }
717
742
  if (options.relay && ["signature", "signature-input", "signature-agent"].some((name) => !(0, import_canonicalize2.getHeaderValue)(request.headers, name)))
718
743
  throw new TypeError("Relay requires the existing signature fields");
719
744
  const earlier = options.relay ? inputs(request.headers) : [];
720
- if (earlier.some((input2) => input2.label === label || input2.params.tag !== WEB_BOT_AUTH_PROFILE))
745
+ if (earlier.some((input) => input.label === label || input.params.tag !== WEB_BOT_AUTH_PROFILE))
721
746
  throw new TypeError("Relay requires existing Web Bot Auth signatures and a fresh label");
722
747
  const priorAgent = options.relay ? (0, import_canonicalize2.getHeaderValue)(request.headers, "signature-agent") : void 0;
723
748
  if (priorAgent) dictionary(priorAgent, "Signature-Agent");
724
749
  headers["signature-agent"] = `${priorAgent ? priorAgent + ", " : ""}${label}="${origin}"`;
725
- components.push(`"signature-agent";key="${label}"`);
726
- for (const inner of earlier) {
727
- for (const component of inner.components) {
728
- const bare = component.split(";", 1)[0];
729
- const encoded = `"${bare}"${component.slice(bare.length)}`;
730
- if (!components.includes(encoded)) components.push(encoded);
750
+ if (priorAgent) dictionary(headers["signature-agent"], "Signature-Agent");
751
+ components.push((0, import_http_message_sig.component)("signature-agent", { key: label }));
752
+ const covered = new Set(components.map(import_http_message_sig.componentIdentity));
753
+ const cover = (descriptor) => {
754
+ const identity = (0, import_http_message_sig.componentIdentity)(descriptor);
755
+ if (!covered.has(identity)) {
756
+ covered.add(identity);
757
+ components.push(descriptor);
731
758
  }
732
- components.push(`"signature-input";key="${inner.label}"`, `"signature";key="${inner.label}"`);
759
+ };
760
+ for (const inner of earlier) {
761
+ for (const identifier of inner.components) cover(componentDescriptor(identifier));
762
+ cover((0, import_http_message_sig.component)("signature-input", { key: inner.label }));
763
+ cover((0, import_http_message_sig.component)("signature", { key: inner.label }));
733
764
  }
734
765
  const priorInput = options.relay ? (0, import_canonicalize2.getHeaderValue)(request.headers, "signature-input") : void 0;
735
766
  const priorSignature = options.relay ? (0, import_canonicalize2.getHeaderValue)(request.headers, "signature") : void 0;
736
767
  if (options.relay && (!priorInput || !priorSignature || !priorAgent))
737
768
  throw new TypeError("Relay requires the existing signature fields");
738
- headers["signature-input"] = `${priorInput ? priorInput + ", " : ""}${label}=(${components.join(" ")});created=${now};expires=${now + window};nonce="${nonce}";keyid="${jwkThumbprint(jwk)}";alg="${algorithm(jwk)}";tag="web-bot-auth"`;
739
- const input = (0, import_parser.parseSignatureInput)(headers["signature-input"], label);
769
+ if (priorInput) headers["signature-input"] = priorInput;
740
770
  if (priorSignature) headers.signature = priorSignature;
741
- const bytes = await signBase(
742
- buildWebBotAuthSignatureBase(input, { ...request, url: new URL(request.url).href, headers }),
743
- key
771
+ for (const inner of earlier)
772
+ buildWebBotAuthSignatureBase(inner, { ...request, url: new URL(request.url).href, headers });
773
+ const targetUri = new URL(request.url);
774
+ targetUri.hash = "";
775
+ const signed = await createWebBotSignature(
776
+ {
777
+ kind: "request",
778
+ method: request.method.toUpperCase(),
779
+ targetUri: targetUri.href,
780
+ fields: coveredFields(components, headers)
781
+ },
782
+ {
783
+ label,
784
+ components,
785
+ parameters: {
786
+ created: now,
787
+ expires: now + window,
788
+ nonce,
789
+ keyid: jwkThumbprint(jwk),
790
+ alg: algorithm(jwk),
791
+ tag: WEB_BOT_AUTH_PROFILE
792
+ },
793
+ signer: webBotSigner(key)
794
+ }
744
795
  );
745
- headers.signature = `${priorSignature ? priorSignature + ", " : ""}${label}=:${Buffer.from(bytes).toString("base64")}:`;
796
+ headers["signature-input"] = `${priorInput ? priorInput + ", " : ""}${signed.signatureInput}`;
797
+ headers.signature = `${priorSignature ? priorSignature + ", " : ""}${signed.signature}`;
746
798
  return headers;
747
799
  }
748
800
  async function createSignedSignatureDirectory(originUrl, signingKeys, options = {}) {
@@ -777,15 +829,29 @@ async function createSignedSignatureDirectory(originUrl, signingKeys, options =
777
829
  };
778
830
  const signatureInputs = [], signatures = [];
779
831
  for (let i = 0; i < keys.length; i++) {
780
- const label = `key${i + 1}`;
781
- const value = `${label}=("@authority";req "content-digest");created=${now - DIRECTORY_CLOCK_SKEW_SECONDS};expires=${now + lifetime};keyid="${keys[i].kid}";alg="${algorithm(keys[i])}";tag="${DIRECTORY_TAG}"`;
782
- const input = (0, import_parser.parseSignatureInput)(value, label);
783
- const bytes = await signBase(
784
- buildWebBotAuthSignatureBase(input, { method: "GET", url: response.request.url, headers: {} }, response),
785
- signingKeys[i]
832
+ const signed = await createWebBotSignature(
833
+ {
834
+ kind: "response",
835
+ status: response.status,
836
+ fields: coveredFields([(0, import_http_message_sig.component)("content-digest")], response.headers),
837
+ request: { kind: "request", method: "GET", targetUri: response.request.url, fields: [] }
838
+ },
839
+ {
840
+ label: `key${i + 1}`,
841
+ components: [(0, import_http_message_sig.component)("@authority", { req: true }), (0, import_http_message_sig.component)("content-digest")],
842
+ parameters: {
843
+ // Backdate created so verifiers whose clocks run slightly behind accept a fresh proof.
844
+ created: now - DIRECTORY_CLOCK_SKEW_SECONDS,
845
+ expires: now + lifetime,
846
+ keyid: keys[i].kid,
847
+ alg: algorithm(keys[i]),
848
+ tag: DIRECTORY_TAG
849
+ },
850
+ signer: webBotSigner(signingKeys[i])
851
+ }
786
852
  );
787
- signatureInputs.push(value);
788
- signatures.push(`${label}=:${Buffer.from(bytes).toString("base64")}:`);
853
+ signatureInputs.push(signed.signatureInput);
854
+ signatures.push(signed.signature);
789
855
  }
790
856
  response.headers["signature-input"] = signatureInputs.join(", ");
791
857
  response.headers.signature = signatures.join(", ");
@@ -7,6 +7,12 @@ import {
7
7
  sign as nodeSign
8
8
  } from "crypto";
9
9
  import { parseDictionary, parseItem, serializeItem } from "structured-headers";
10
+ import {
11
+ component,
12
+ componentIdentity,
13
+ createSignature,
14
+ isSignatureError
15
+ } from "http-message-sig";
10
16
  import { ssrfSafeFetch } from "../net/index.mjs";
11
17
  import { eTldPlusOne } from "./agent-resolver/etld.mjs";
12
18
  import { canonicalAgentOrigin } from "./agent-resolver/canonicalize.mjs";
@@ -136,11 +142,11 @@ function receivedAgentOrigin(value) {
136
142
  }
137
143
  }
138
144
  function buildWebBotAuthSignatureBase(input, request, response) {
139
- const lines = input.components.map((component) => {
140
- const bare = component.split(";", 1)[0];
145
+ const lines = input.components.map((component2) => {
146
+ const bare = component2.split(";", 1)[0];
141
147
  let identifier;
142
148
  try {
143
- identifier = parseItem(`"${bare}"${component.slice(bare.length)}`);
149
+ identifier = parseItem(`"${bare}"${component2.slice(bare.length)}`);
144
150
  } catch {
145
151
  return bad("component");
146
152
  }
@@ -358,8 +364,8 @@ async function verifyWebBotAuth(request, options) {
358
364
  (c) => c.startsWith("signature-agent;key=") && !verified.some((s) => selected.find((i) => i.label === s.label).components.includes(c))
359
365
  );
360
366
  if (agentComponents.length !== 1) denied("agent_not_covered");
361
- const component = parseItem(`"${agentComponents[0].replace("signature-agent;", 'signature-agent";')}`);
362
- const memberKey = component[1].get("key");
367
+ const component2 = parseItem(`"${agentComponents[0].replace("signature-agent;", 'signature-agent";')}`);
368
+ const memberKey = component2[1].get("key");
363
369
  if (typeof memberKey !== "string" || memberKey !== input.label) bad("agent_member");
364
370
  const member = agents.get(memberKey);
365
371
  if (!member) denied("agent_not_covered");
@@ -627,7 +633,7 @@ class SignatureDirectoryResolver {
627
633
  return this.getVerifiedDirectory(origin)?.proof;
628
634
  }
629
635
  }
630
- async function signBase(base, key) {
636
+ function webBotSigner(key) {
631
637
  const jwk = "privateKey" in key ? key.privateKey : key.publicKey;
632
638
  const alg = algorithm(jwk);
633
639
  if (!["test", "development"].includes(process.env.NODE_ENV ?? "") && TEST_KIDS.has(jwkThumbprint(jwk)))
@@ -635,17 +641,40 @@ async function signBase(base, key) {
635
641
  if ("provider" in key) {
636
642
  if (key.provider.keyid !== jwkThumbprint(jwk) || key.provider.algorithm !== alg)
637
643
  throw new TypeError("Web Bot Auth provider identity does not match its public key");
638
- return key.provider.sign(Buffer.from(base));
644
+ return { algorithm: alg, sign: async (data) => Buffer.from(await key.provider.sign(Buffer.from(data))) };
639
645
  }
640
646
  if (alg === "rsa-pss-sha512") {
641
647
  const privateKey = createPrivateKey({ key: jwk, format: "jwk" });
642
- return nodeSign("sha512", Buffer.from(base), {
643
- key: privateKey,
644
- padding: cryptoConstants.RSA_PKCS1_PSS_PADDING,
645
- saltLength: 64
646
- });
648
+ return {
649
+ algorithm: alg,
650
+ sign: (data) => nodeSign("sha512", Buffer.from(data), {
651
+ key: privateKey,
652
+ padding: cryptoConstants.RSA_PKCS1_PSS_PADDING,
653
+ saltLength: 64
654
+ })
655
+ };
656
+ }
657
+ return { algorithm: alg, sign: (data) => signWithPrivateJwkAsync(alg, jwk, Buffer.from(data)) };
658
+ }
659
+ async function createWebBotSignature(...args) {
660
+ try {
661
+ return await createSignature(...args);
662
+ } catch (error) {
663
+ if (isSignatureError(error)) throw new WebBotAuthError(400, "component_value");
664
+ throw error;
647
665
  }
648
- return signWithPrivateJwkAsync(alg, jwk, Buffer.from(base));
666
+ }
667
+ function componentDescriptor(identifier) {
668
+ const bare = identifier.split(";", 1)[0];
669
+ const [, params] = parseItem(`"${bare}"${identifier.slice(bare.length)}`);
670
+ return component(bare, Object.fromEntries(params));
671
+ }
672
+ function coveredFields(components, headers) {
673
+ const names = new Set(components.filter((c) => !c.name.startsWith("@")).map((c) => c.name));
674
+ return [...names].flatMap((name) => {
675
+ const value = getHeaderValue(headers, name);
676
+ return value === void 0 ? [] : [{ name, value }];
677
+ });
649
678
  }
650
679
  async function signWebBotAuth(request, key, options) {
651
680
  const origin = canonicalAgentOrigin(options.origin);
@@ -655,6 +684,7 @@ async function signWebBotAuth(request, key, options) {
655
684
  const now = Math.floor((options.now ?? (() => Date.now() / 1e3))());
656
685
  const window = options.windowSeconds ?? 300;
657
686
  const nonce = options.nonce ?? randomBytes(64).toString("base64url");
687
+ if (!Number.isSafeInteger(now)) throw new TypeError("Invalid Web Bot Auth clock");
658
688
  if (!Number.isInteger(window) || window < 1 || window > 300 || !/^[A-Za-z0-9_+\/-]{22,256}={0,2}$/.test(nonce))
659
689
  throw new TypeError("Invalid Web Bot Auth lifetime or nonce");
660
690
  if (!options.relay && ["signature", "signature-input", "signature-agent"].some(
@@ -667,42 +697,69 @@ async function signWebBotAuth(request, key, options) {
667
697
  for (const [name, value] of Object.entries(request.headers))
668
698
  if (value !== void 0 && !["signature", "signature-input", "signature-agent", "content-digest"].includes(name.toLowerCase()))
669
699
  headers[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
670
- const components = ['"@method"', '"@target-uri"', '"@authority"'];
700
+ const components = [component("@method"), component("@target-uri"), component("@authority")];
671
701
  if (request.body !== void 0 && request.body !== "") {
672
702
  if (!getHeaderValue(headers, "content-type"))
673
703
  throw new TypeError("Body-bearing Web Bot Auth requests need Content-Type");
674
704
  headers["content-digest"] = computeContentDigest(request.body, "rfc8941-base64");
675
- components.push('"content-type"', '"content-digest"');
705
+ components.push(component("content-type"), component("content-digest"));
676
706
  }
677
707
  if (options.relay && ["signature", "signature-input", "signature-agent"].some((name) => !getHeaderValue(request.headers, name)))
678
708
  throw new TypeError("Relay requires the existing signature fields");
679
709
  const earlier = options.relay ? inputs(request.headers) : [];
680
- if (earlier.some((input2) => input2.label === label || input2.params.tag !== WEB_BOT_AUTH_PROFILE))
710
+ if (earlier.some((input) => input.label === label || input.params.tag !== WEB_BOT_AUTH_PROFILE))
681
711
  throw new TypeError("Relay requires existing Web Bot Auth signatures and a fresh label");
682
712
  const priorAgent = options.relay ? getHeaderValue(request.headers, "signature-agent") : void 0;
683
713
  if (priorAgent) dictionary(priorAgent, "Signature-Agent");
684
714
  headers["signature-agent"] = `${priorAgent ? priorAgent + ", " : ""}${label}="${origin}"`;
685
- components.push(`"signature-agent";key="${label}"`);
686
- for (const inner of earlier) {
687
- for (const component of inner.components) {
688
- const bare = component.split(";", 1)[0];
689
- const encoded = `"${bare}"${component.slice(bare.length)}`;
690
- if (!components.includes(encoded)) components.push(encoded);
715
+ if (priorAgent) dictionary(headers["signature-agent"], "Signature-Agent");
716
+ components.push(component("signature-agent", { key: label }));
717
+ const covered = new Set(components.map(componentIdentity));
718
+ const cover = (descriptor) => {
719
+ const identity = componentIdentity(descriptor);
720
+ if (!covered.has(identity)) {
721
+ covered.add(identity);
722
+ components.push(descriptor);
691
723
  }
692
- components.push(`"signature-input";key="${inner.label}"`, `"signature";key="${inner.label}"`);
724
+ };
725
+ for (const inner of earlier) {
726
+ for (const identifier of inner.components) cover(componentDescriptor(identifier));
727
+ cover(component("signature-input", { key: inner.label }));
728
+ cover(component("signature", { key: inner.label }));
693
729
  }
694
730
  const priorInput = options.relay ? getHeaderValue(request.headers, "signature-input") : void 0;
695
731
  const priorSignature = options.relay ? getHeaderValue(request.headers, "signature") : void 0;
696
732
  if (options.relay && (!priorInput || !priorSignature || !priorAgent))
697
733
  throw new TypeError("Relay requires the existing signature fields");
698
- headers["signature-input"] = `${priorInput ? priorInput + ", " : ""}${label}=(${components.join(" ")});created=${now};expires=${now + window};nonce="${nonce}";keyid="${jwkThumbprint(jwk)}";alg="${algorithm(jwk)}";tag="web-bot-auth"`;
699
- const input = parseSignatureInput(headers["signature-input"], label);
734
+ if (priorInput) headers["signature-input"] = priorInput;
700
735
  if (priorSignature) headers.signature = priorSignature;
701
- const bytes = await signBase(
702
- buildWebBotAuthSignatureBase(input, { ...request, url: new URL(request.url).href, headers }),
703
- key
736
+ for (const inner of earlier)
737
+ buildWebBotAuthSignatureBase(inner, { ...request, url: new URL(request.url).href, headers });
738
+ const targetUri = new URL(request.url);
739
+ targetUri.hash = "";
740
+ const signed = await createWebBotSignature(
741
+ {
742
+ kind: "request",
743
+ method: request.method.toUpperCase(),
744
+ targetUri: targetUri.href,
745
+ fields: coveredFields(components, headers)
746
+ },
747
+ {
748
+ label,
749
+ components,
750
+ parameters: {
751
+ created: now,
752
+ expires: now + window,
753
+ nonce,
754
+ keyid: jwkThumbprint(jwk),
755
+ alg: algorithm(jwk),
756
+ tag: WEB_BOT_AUTH_PROFILE
757
+ },
758
+ signer: webBotSigner(key)
759
+ }
704
760
  );
705
- headers.signature = `${priorSignature ? priorSignature + ", " : ""}${label}=:${Buffer.from(bytes).toString("base64")}:`;
761
+ headers["signature-input"] = `${priorInput ? priorInput + ", " : ""}${signed.signatureInput}`;
762
+ headers.signature = `${priorSignature ? priorSignature + ", " : ""}${signed.signature}`;
706
763
  return headers;
707
764
  }
708
765
  async function createSignedSignatureDirectory(originUrl, signingKeys, options = {}) {
@@ -737,15 +794,29 @@ async function createSignedSignatureDirectory(originUrl, signingKeys, options =
737
794
  };
738
795
  const signatureInputs = [], signatures = [];
739
796
  for (let i = 0; i < keys.length; i++) {
740
- const label = `key${i + 1}`;
741
- const value = `${label}=("@authority";req "content-digest");created=${now - DIRECTORY_CLOCK_SKEW_SECONDS};expires=${now + lifetime};keyid="${keys[i].kid}";alg="${algorithm(keys[i])}";tag="${DIRECTORY_TAG}"`;
742
- const input = parseSignatureInput(value, label);
743
- const bytes = await signBase(
744
- buildWebBotAuthSignatureBase(input, { method: "GET", url: response.request.url, headers: {} }, response),
745
- signingKeys[i]
797
+ const signed = await createWebBotSignature(
798
+ {
799
+ kind: "response",
800
+ status: response.status,
801
+ fields: coveredFields([component("content-digest")], response.headers),
802
+ request: { kind: "request", method: "GET", targetUri: response.request.url, fields: [] }
803
+ },
804
+ {
805
+ label: `key${i + 1}`,
806
+ components: [component("@authority", { req: true }), component("content-digest")],
807
+ parameters: {
808
+ // Backdate created so verifiers whose clocks run slightly behind accept a fresh proof.
809
+ created: now - DIRECTORY_CLOCK_SKEW_SECONDS,
810
+ expires: now + lifetime,
811
+ keyid: keys[i].kid,
812
+ alg: algorithm(keys[i]),
813
+ tag: DIRECTORY_TAG
814
+ },
815
+ signer: webBotSigner(signingKeys[i])
816
+ }
746
817
  );
747
- signatureInputs.push(value);
748
- signatures.push(`${label}=:${Buffer.from(bytes).toString("base64")}:`);
818
+ signatureInputs.push(signed.signatureInput);
819
+ signatures.push(signed.signature);
749
820
  }
750
821
  response.headers["signature-input"] = signatureInputs.join(", ");
751
822
  response.headers.signature = signatures.join(", ");
@@ -82,8 +82,9 @@ function formatStoryboardResultsAsJUnit(results, options = {}) {
82
82
  const suiteGroup = options.suite_groups?.[sb.storyboard_id];
83
83
  const suiteClassname = suiteGroup ? `${suiteGroup}.${sb.storyboard_id}` : sb.storyboard_id;
84
84
  const suiteCases = [];
85
- let suiteFailures = sb.failed_count;
85
+ let suiteFailures = 0;
86
86
  let representedSkipped = 0;
87
+ let suiteSkipped = 0;
87
88
  const passPhases = sb.passes?.length ? sb.passes.map((pass) => ({ label: `Pass ${pass.pass_index} \u203A `, phases: pass.phases })) : [{ label: "", phases: sb.phases }];
88
89
  for (const pass of passPhases) {
89
90
  for (const phase of pass.phases) {
@@ -93,6 +94,7 @@ function formatStoryboardResultsAsJUnit(results, options = {}) {
93
94
  const time = ((step.duration_ms || 0) / 1e3).toFixed(3);
94
95
  if (step.skipped) {
95
96
  representedSkipped += 1;
97
+ suiteSkipped += 1;
96
98
  totalSkipped += 1;
97
99
  suiteCases.push(
98
100
  ` <testcase classname="${xmlEscape(suiteClassname)}" name="${xmlEscape(name)}" time="${time}">
@@ -102,7 +104,6 @@ function formatStoryboardResultsAsJUnit(results, options = {}) {
102
104
  continue;
103
105
  }
104
106
  if (!step.passed) {
105
- totalFailures += 1;
106
107
  const failureDetails = [
107
108
  step.error,
108
109
  ...step.validations.filter((v) => !v.passed).map(
@@ -114,6 +115,17 @@ function formatStoryboardResultsAsJUnit(results, options = {}) {
114
115
  // report body.
115
116
  ...hintLines(step.hints)
116
117
  ].filter(Boolean).join("\n");
118
+ if (step.counts_toward_failure === false) {
119
+ suiteCases.push(
120
+ ` <testcase classname="${xmlEscape(suiteClassname)}" name="${xmlEscape(name)}" time="${time}">
121
+ <system-out>${xmlEscape(`Optional phase failure; excluded from storyboard verdict.
122
+ ${failureDetails}`)}</system-out>
123
+ </testcase>`
124
+ );
125
+ continue;
126
+ }
127
+ totalFailures += 1;
128
+ suiteFailures += 1;
117
129
  const message = step.error || firstHintMessage(step) || "validation failed";
118
130
  suiteCases.push(
119
131
  ` <testcase classname="${xmlEscape(suiteClassname)}" name="${xmlEscape(name)}" time="${time}">
@@ -136,6 +148,7 @@ function formatStoryboardResultsAsJUnit(results, options = {}) {
136
148
  if (hasStoryboardFixtureSkip) {
137
149
  totalTests += 1;
138
150
  totalSkipped += 1;
151
+ suiteSkipped += 1;
139
152
  suiteCases.push(
140
153
  ` <testcase classname="${xmlEscape(suiteClassname)}" name="Fixture resolution" time="0.000">
141
154
  <skipped message="${xmlEscape(fixtureGap.detail)}"/>
@@ -159,7 +172,7 @@ ${assertion.error}` : ""}`;
159
172
  totalDuration += sb.total_duration_ms || 0;
160
173
  const suiteTests = suiteCases.length;
161
174
  suites.push(
162
- ` <testsuite name="${xmlEscape(suiteGroup ? `${suiteGroup} \u203A ${sb.storyboard_title}` : sb.storyboard_title)}"${suiteGroup ? ` package="${xmlEscape(`adcp.${suiteGroup}`)}"` : ""} tests="${suiteTests}" failures="${suiteFailures}" skipped="${sb.skipped_count}" time="${((sb.total_duration_ms || 0) / 1e3).toFixed(3)}" timestamp="${sb.tested_at || (/* @__PURE__ */ new Date()).toISOString()}">
175
+ ` <testsuite name="${xmlEscape(suiteGroup ? `${suiteGroup} \u203A ${sb.storyboard_title}` : sb.storyboard_title)}"${suiteGroup ? ` package="${xmlEscape(`adcp.${suiteGroup}`)}"` : ""} tests="${suiteTests}" failures="${suiteFailures}" skipped="${suiteSkipped}" time="${((sb.total_duration_ms || 0) / 1e3).toFixed(3)}" timestamp="${sb.tested_at || (/* @__PURE__ */ new Date()).toISOString()}">
163
176
  ` + suiteCases.join("\n") + `
164
177
  </testsuite>`
165
178
  );
@@ -58,8 +58,9 @@ function formatStoryboardResultsAsJUnit(results, options = {}) {
58
58
  const suiteGroup = options.suite_groups?.[sb.storyboard_id];
59
59
  const suiteClassname = suiteGroup ? `${suiteGroup}.${sb.storyboard_id}` : sb.storyboard_id;
60
60
  const suiteCases = [];
61
- let suiteFailures = sb.failed_count;
61
+ let suiteFailures = 0;
62
62
  let representedSkipped = 0;
63
+ let suiteSkipped = 0;
63
64
  const passPhases = sb.passes?.length ? sb.passes.map((pass) => ({ label: `Pass ${pass.pass_index} \u203A `, phases: pass.phases })) : [{ label: "", phases: sb.phases }];
64
65
  for (const pass of passPhases) {
65
66
  for (const phase of pass.phases) {
@@ -69,6 +70,7 @@ function formatStoryboardResultsAsJUnit(results, options = {}) {
69
70
  const time = ((step.duration_ms || 0) / 1e3).toFixed(3);
70
71
  if (step.skipped) {
71
72
  representedSkipped += 1;
73
+ suiteSkipped += 1;
72
74
  totalSkipped += 1;
73
75
  suiteCases.push(
74
76
  ` <testcase classname="${xmlEscape(suiteClassname)}" name="${xmlEscape(name)}" time="${time}">
@@ -78,7 +80,6 @@ function formatStoryboardResultsAsJUnit(results, options = {}) {
78
80
  continue;
79
81
  }
80
82
  if (!step.passed) {
81
- totalFailures += 1;
82
83
  const failureDetails = [
83
84
  step.error,
84
85
  ...step.validations.filter((v) => !v.passed).map(
@@ -90,6 +91,17 @@ function formatStoryboardResultsAsJUnit(results, options = {}) {
90
91
  // report body.
91
92
  ...hintLines(step.hints)
92
93
  ].filter(Boolean).join("\n");
94
+ if (step.counts_toward_failure === false) {
95
+ suiteCases.push(
96
+ ` <testcase classname="${xmlEscape(suiteClassname)}" name="${xmlEscape(name)}" time="${time}">
97
+ <system-out>${xmlEscape(`Optional phase failure; excluded from storyboard verdict.
98
+ ${failureDetails}`)}</system-out>
99
+ </testcase>`
100
+ );
101
+ continue;
102
+ }
103
+ totalFailures += 1;
104
+ suiteFailures += 1;
93
105
  const message = step.error || firstHintMessage(step) || "validation failed";
94
106
  suiteCases.push(
95
107
  ` <testcase classname="${xmlEscape(suiteClassname)}" name="${xmlEscape(name)}" time="${time}">
@@ -112,6 +124,7 @@ function formatStoryboardResultsAsJUnit(results, options = {}) {
112
124
  if (hasStoryboardFixtureSkip) {
113
125
  totalTests += 1;
114
126
  totalSkipped += 1;
127
+ suiteSkipped += 1;
115
128
  suiteCases.push(
116
129
  ` <testcase classname="${xmlEscape(suiteClassname)}" name="Fixture resolution" time="0.000">
117
130
  <skipped message="${xmlEscape(fixtureGap.detail)}"/>
@@ -135,7 +148,7 @@ ${assertion.error}` : ""}`;
135
148
  totalDuration += sb.total_duration_ms || 0;
136
149
  const suiteTests = suiteCases.length;
137
150
  suites.push(
138
- ` <testsuite name="${xmlEscape(suiteGroup ? `${suiteGroup} \u203A ${sb.storyboard_title}` : sb.storyboard_title)}"${suiteGroup ? ` package="${xmlEscape(`adcp.${suiteGroup}`)}"` : ""} tests="${suiteTests}" failures="${suiteFailures}" skipped="${sb.skipped_count}" time="${((sb.total_duration_ms || 0) / 1e3).toFixed(3)}" timestamp="${sb.tested_at || (/* @__PURE__ */ new Date()).toISOString()}">
151
+ ` <testsuite name="${xmlEscape(suiteGroup ? `${suiteGroup} \u203A ${sb.storyboard_title}` : sb.storyboard_title)}"${suiteGroup ? ` package="${xmlEscape(`adcp.${suiteGroup}`)}"` : ""} tests="${suiteTests}" failures="${suiteFailures}" skipped="${suiteSkipped}" time="${((sb.total_duration_ms || 0) / 1e3).toFixed(3)}" timestamp="${sb.tested_at || (/* @__PURE__ */ new Date()).toISOString()}">
139
152
  ` + suiteCases.join("\n") + `
140
153
  </testsuite>`
141
154
  );