@bugmole/cli 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/.bugmole.env.example +20 -0
  2. package/LICENSE +7 -0
  3. package/README.md +293 -0
  4. package/TESTING.md +117 -0
  5. package/bugmole.config.yaml +39 -0
  6. package/package.json +85 -0
  7. package/scripts/billing/paypal-setup.mjs +121 -0
  8. package/scripts/bugmole-continue.ts +318 -0
  9. package/scripts/bugmole-init.ts +188 -0
  10. package/scripts/bugmole.cjs +17 -0
  11. package/scripts/bugmole.test.ts +344 -0
  12. package/scripts/bugmole.ts +657 -0
  13. package/scripts/ensure-maestro.cjs +79 -0
  14. package/scripts/ios-tunnel-keeper.sh +45 -0
  15. package/scripts/ios-wda-keeper.sh +66 -0
  16. package/scripts/sync-plan-catalog.d.mts +3 -0
  17. package/scripts/sync-plan-catalog.mjs +16 -0
  18. package/scripts/ui-parity-diff.py +65 -0
  19. package/scripts/ui-parity-requirements.txt +1 -0
  20. package/scripts/verify-manage-to-plans.mts +194 -0
  21. package/spec/app-ui-audit.schema.json +176 -0
  22. package/spec/blockers.yaml +79 -0
  23. package/spec/bugs.index.json +42 -0
  24. package/spec/design-dna.schema.json +38 -0
  25. package/spec/domain_rules.yaml +24 -0
  26. package/spec/flows/flow_manage-to-plans-64c3c61b.yaml +8 -0
  27. package/spec/flows/flow_screen-to-evidence-6c3ab309.yaml +7 -0
  28. package/spec/journey_graph.yaml +126 -0
  29. package/spec/journeys.graph.json +2618 -0
  30. package/spec/plans/dashboard-smoke.flow.yaml +20 -0
  31. package/spec/plans/example.flow.yaml +99 -0
  32. package/spec/plans/flow_api-keys-to-logout-7396cd5d.flow.yaml +33 -0
  33. package/spec/plans/flow_api-keys-to-screen-157e10a6.flow.yaml +33 -0
  34. package/spec/plans/flow_manage-to-audits-176c1eb8.flow.yaml +33 -0
  35. package/spec/plans/flow_manage-to-blockers-633282c8.flow.yaml +112 -0
  36. package/spec/plans/flow_manage-to-devices-ad57e202.flow.yaml +33 -0
  37. package/spec/plans/flow_manage-to-logout-86c54656.flow.yaml +33 -0
  38. package/spec/plans/flow_manage-to-manage-resource-action-6325f99a.flow.yaml +185 -0
  39. package/spec/plans/flow_manage-to-parity-issues-01fa2579.flow.yaml +34 -0
  40. package/spec/plans/flow_manage-to-plans-64c3c61b.flow.yaml +123 -0
  41. package/spec/plans/flow_manage-to-screen-6857f915.flow.yaml +106 -0
  42. package/spec/plans/flow_manage-to-tasks-c5791c95.flow.yaml +33 -0
  43. package/spec/plans/flow_profile-to-screen-6045aa3d.flow.yaml +33 -0
  44. package/spec/plans/flow_register-to-api-auth-login-460a9cc8.flow.yaml +34 -0
  45. package/spec/plans/flow_screen-to-audits-07666356.flow.yaml +33 -0
  46. package/spec/plans/flow_screen-to-blockers-091d258b.flow.yaml +33 -0
  47. package/spec/plans/flow_screen-to-devices-cad72f75.flow.yaml +33 -0
  48. package/spec/plans/flow_screen-to-evidence-6c3ab309.flow.yaml +126 -0
  49. package/spec/plans/flow_screen-to-parity-issues-2218e3ad.flow.yaml +34 -0
  50. package/spec/plans/flow_screen-to-plans-8232a94f.flow.yaml +33 -0
  51. package/spec/plans/flow_screen-to-tasks-540671d1.flow.yaml +33 -0
  52. package/spec/plans/login.flow.yaml +22 -0
  53. package/spec/plans/owner-operations.flow.yaml +20 -0
  54. package/spec/project_config.yaml +55 -0
  55. package/spec/roles.yaml +30 -0
  56. package/spec/schema.md +394 -0
  57. package/spec/test-case-results.schema.json +62 -0
  58. package/spec/test-cases.schema.json +85 -0
  59. package/spec/ui-parity-audit.schema.json +194 -0
  60. package/spec/ui-reverse-engineering.schema.json +94 -0
  61. package/src/billing/plan-catalog.test.ts +46 -0
  62. package/src/billing/plan-catalog.ts +199 -0
  63. package/src/integrations/aws-sigv4.test.ts +42 -0
  64. package/src/integrations/aws-sigv4.ts +72 -0
  65. package/src/integrations/device-farm.ts +155 -0
  66. package/src/integrations/github-app.test.ts +57 -0
  67. package/src/integrations/github-app.ts +143 -0
  68. package/src/integrations/gitlab.ts +81 -0
  69. package/src/integrations/temp-email.test.ts +123 -0
  70. package/src/integrations/temp-email.ts +175 -0
  71. package/src/integrations/testflight-feedback.test.ts +51 -0
  72. package/src/integrations/testflight-feedback.ts +173 -0
  73. package/src/integrations/webdriver-client.ts +131 -0
  74. package/src/mcp/server.test.ts +1220 -0
  75. package/src/mcp/server.ts +3064 -0
  76. package/src/mcp/write-test-cases.test.ts +287 -0
  77. package/src/registry/api-key-client.ts +39 -0
  78. package/src/registry/control-plane-client.ts +212 -0
  79. package/src/registry/migrations/0001_registry.sql +47 -0
  80. package/src/registry/migrations/0002_device_authorizations.sql +23 -0
  81. package/src/registry/migrations/0003_project_environments.sql +25 -0
  82. package/src/registry/migrations/0004_device_authorization_email.sql +1 -0
  83. package/src/registry/migrations/0005_testing_control_plane.sql +55 -0
  84. package/src/registry/migrations/0006_workspaces.sql +36 -0
  85. package/src/registry/migrations/0007_project_apps.sql +26 -0
  86. package/src/registry/migrations/0008_agent_tasks.sql +30 -0
  87. package/src/registry/migrations/0009_journey_revisions.sql +17 -0
  88. package/src/registry/migrations/0010_agent_task_journey.sql +2 -0
  89. package/src/registry/migrations/0011_run_journey_revision.sql +1 -0
  90. package/src/registry/migrations/0012_canonical_flow_execution.sql +10 -0
  91. package/src/registry/migrations/0013_device_sessions.sql +22 -0
  92. package/src/registry/migrations/0014_agent_task_step.sql +1 -0
  93. package/src/registry/migrations/0015_agent_task_attempts.sql +6 -0
  94. package/src/registry/migrations/0016_github_issue_tracker.sql +54 -0
  95. package/src/registry/migrations/0017_run_targets.sql +7 -0
  96. package/src/registry/migrations/0018_run_fix_from.sql +4 -0
  97. package/src/registry/migrations/0019_workspace_flags.sql +9 -0
  98. package/src/registry/migrations/0020_orgs.sql +40 -0
  99. package/src/registry/migrations/0021_billing_core.sql +58 -0
  100. package/src/registry/migrations/0022_cloud_runners.sql +19 -0
  101. package/src/registry/migrations/0023_signup.sql +4 -0
  102. package/src/registry/migrations/0024_billing.sql +67 -0
  103. package/src/registry/migrations/0025_notifications.sql +47 -0
  104. package/src/registry/migrations/0026_repo_bindings.sql +28 -0
  105. package/src/registry/migrations/0027_feedback.sql +29 -0
  106. package/src/registry/migrations/0028_devices.sql +48 -0
  107. package/src/registry/migrations/0029_sso.sql +31 -0
  108. package/src/registry/migrations/0030_workspace_domains.sql +18 -0
  109. package/src/registry/migrations/0031_gitlab_and_teams.sql +45 -0
  110. package/src/registry/migrations/0032_personas.sql +15 -0
  111. package/src/registry/migrations/0033_bugmole_rename.sql +11 -0
  112. package/src/registry/task-scheduling.test.ts +100 -0
  113. package/src/registry/task-scheduling.ts +80 -0
  114. package/src/registry-worker/ai/platform-model.ts +77 -0
  115. package/src/registry-worker/ai/routes.test.ts +88 -0
  116. package/src/registry-worker/ai/routes.ts +90 -0
  117. package/src/registry-worker/artifacts.test.ts +98 -0
  118. package/src/registry-worker/artifacts.ts +85 -0
  119. package/src/registry-worker/billing/billing-core.test.ts +175 -0
  120. package/src/registry-worker/billing/checkout-routes.ts +209 -0
  121. package/src/registry-worker/billing/enforcement.ts +69 -0
  122. package/src/registry-worker/billing/entitlements.ts +108 -0
  123. package/src/registry-worker/billing/ledger.ts +186 -0
  124. package/src/registry-worker/billing/paypal/api.ts +259 -0
  125. package/src/registry-worker/billing/paypal/client.ts +91 -0
  126. package/src/registry-worker/billing/paypal/provider.ts +143 -0
  127. package/src/registry-worker/billing/paypal.test.ts +466 -0
  128. package/src/registry-worker/billing/provider.ts +114 -0
  129. package/src/registry-worker/billing/routes.ts +66 -0
  130. package/src/registry-worker/billing/subscriptions.ts +780 -0
  131. package/src/registry-worker/billing/thresholds.ts +107 -0
  132. package/src/registry-worker/core.ts +308 -0
  133. package/src/registry-worker/devices/devices.test.ts +185 -0
  134. package/src/registry-worker/devices/policy.ts +71 -0
  135. package/src/registry-worker/devices/routes.ts +453 -0
  136. package/src/registry-worker/domains/domains.test.ts +210 -0
  137. package/src/registry-worker/domains/routes.ts +139 -0
  138. package/src/registry-worker/domains.ts +88 -0
  139. package/src/registry-worker/email/sender.ts +75 -0
  140. package/src/registry-worker/env.d.ts +14716 -0
  141. package/src/registry-worker/features.ts +20 -0
  142. package/src/registry-worker/feedback/feedback.test.ts +230 -0
  143. package/src/registry-worker/feedback/format.ts +148 -0
  144. package/src/registry-worker/feedback/routes.ts +386 -0
  145. package/src/registry-worker/flags.ts +39 -0
  146. package/src/registry-worker/github/checks.test.ts +177 -0
  147. package/src/registry-worker/github/checks.ts +374 -0
  148. package/src/registry-worker/gitlab/checks.test.ts +141 -0
  149. package/src/registry-worker/gitlab/checks.ts +349 -0
  150. package/src/registry-worker/hooks.ts +54 -0
  151. package/src/registry-worker/index.ts +2077 -0
  152. package/src/registry-worker/jobs/index.ts +29 -0
  153. package/src/registry-worker/jobs/retention.ts +68 -0
  154. package/src/registry-worker/mcp/mcp.test.ts +355 -0
  155. package/src/registry-worker/mcp/routes.ts +215 -0
  156. package/src/registry-worker/mcp/token.ts +126 -0
  157. package/src/registry-worker/mcp/tools.ts +563 -0
  158. package/src/registry-worker/notifications/alerts.ts +212 -0
  159. package/src/registry-worker/notifications/notifications.test.ts +298 -0
  160. package/src/registry-worker/notifications/outbox.ts +83 -0
  161. package/src/registry-worker/notifications/routes.ts +280 -0
  162. package/src/registry-worker/notifications/secrets.ts +49 -0
  163. package/src/registry-worker/notifications/slack.ts +96 -0
  164. package/src/registry-worker/notifications/teams.ts +46 -0
  165. package/src/registry-worker/org/audit.ts +116 -0
  166. package/src/registry-worker/org/routes.test.ts +163 -0
  167. package/src/registry-worker/org/routes.ts +302 -0
  168. package/src/registry-worker/personas/personas.test.ts +78 -0
  169. package/src/registry-worker/personas/routes.ts +100 -0
  170. package/src/registry-worker/repo-triggers.ts +20 -0
  171. package/src/registry-worker/routes/index.ts +74 -0
  172. package/src/registry-worker/run-events.ts +24 -0
  173. package/src/registry-worker/runner/dispatch.ts +219 -0
  174. package/src/registry-worker/runner/jobs.ts +43 -0
  175. package/src/registry-worker/runner/metering.ts +82 -0
  176. package/src/registry-worker/runner/policy.ts +59 -0
  177. package/src/registry-worker/runner/routes.ts +171 -0
  178. package/src/registry-worker/runner/runner.test.ts +358 -0
  179. package/src/registry-worker/runner/tokens.ts +93 -0
  180. package/src/registry-worker/runs.test.ts +60 -0
  181. package/src/registry-worker/signup/policy.ts +57 -0
  182. package/src/registry-worker/signup/routes.ts +106 -0
  183. package/src/registry-worker/signup/signup.test.ts +81 -0
  184. package/src/registry-worker/sso/aegis.ts +141 -0
  185. package/src/registry-worker/sso/membership.ts +157 -0
  186. package/src/registry-worker/sso/routes.ts +458 -0
  187. package/src/registry-worker/sso/sso.test.ts +344 -0
  188. package/src/registry-worker/testing/d1-shim.ts +180 -0
  189. package/src/registry-worker/testing/harness.ts +137 -0
  190. package/src/runner-worker/index.ts +108 -0
  191. package/src/runtime/ai-analysis.ts +97 -0
  192. package/src/runtime/ai-exploration.test.ts +32 -0
  193. package/src/runtime/ai-exploration.ts +69 -0
  194. package/src/runtime/ai-repair.ts +74 -0
  195. package/src/runtime/ai-work.test.ts +99 -0
  196. package/src/runtime/android-screen-record.test.ts +75 -0
  197. package/src/runtime/android-screen-record.ts +192 -0
  198. package/src/runtime/app-understanding.test.ts +123 -0
  199. package/src/runtime/app-understanding.ts +201 -0
  200. package/src/runtime/appium-driver.test.ts +179 -0
  201. package/src/runtime/appium-driver.ts +295 -0
  202. package/src/runtime/blocker-resolution.test.ts +113 -0
  203. package/src/runtime/blocker-resolution.ts +111 -0
  204. package/src/runtime/browser-matrix.integration.test.ts +212 -0
  205. package/src/runtime/browser-matrix.test.ts +143 -0
  206. package/src/runtime/browser-matrix.ts +200 -0
  207. package/src/runtime/canonical-flow.test.ts +52 -0
  208. package/src/runtime/config-validate.ts +185 -0
  209. package/src/runtime/continuous-execution.ts +291 -0
  210. package/src/runtime/cursor-applescript.ts +573 -0
  211. package/src/runtime/cursor-cli-driver.test.ts +78 -0
  212. package/src/runtime/cursor-cli-driver.ts +156 -0
  213. package/src/runtime/cursor-driver-example.ts +117 -0
  214. package/src/runtime/cursor-driver-index.ts +65 -0
  215. package/src/runtime/cursor-driver-init.ts +277 -0
  216. package/src/runtime/cursor-driver-run.test.ts +15 -0
  217. package/src/runtime/cursor-driver-run.ts +323 -0
  218. package/src/runtime/cursor-driver.ts +332 -0
  219. package/src/runtime/cursor-llm-example.ts +90 -0
  220. package/src/runtime/cursor-llm.ts +206 -0
  221. package/src/runtime/cursor-mcp-monitor.ts +386 -0
  222. package/src/runtime/device-clouds/browserstack.ts +73 -0
  223. package/src/runtime/device-clouds/device-farm.ts +52 -0
  224. package/src/runtime/device-clouds/index.ts +92 -0
  225. package/src/runtime/device-clouds/kobiton.ts +70 -0
  226. package/src/runtime/device-clouds/targets.ts +44 -0
  227. package/src/runtime/device-clouds/types.ts +62 -0
  228. package/src/runtime/diff-proposal.ts +84 -0
  229. package/src/runtime/discovery-task.test.ts +29 -0
  230. package/src/runtime/discovery-task.ts +284 -0
  231. package/src/runtime/driver-recovery.ts +69 -0
  232. package/src/runtime/driver.ts +79 -0
  233. package/src/runtime/environment.test.ts +104 -0
  234. package/src/runtime/environment.ts +137 -0
  235. package/src/runtime/executor.test.ts +509 -0
  236. package/src/runtime/executor.ts +921 -0
  237. package/src/runtime/explorer.test.ts +101 -0
  238. package/src/runtime/explorer.ts +1013 -0
  239. package/src/runtime/failure-analysis.test.ts +111 -0
  240. package/src/runtime/failure-analysis.ts +272 -0
  241. package/src/runtime/fixtures/fake-maestro.sh +36 -0
  242. package/src/runtime/flow-language.test.ts +268 -0
  243. package/src/runtime/flow-language.ts +414 -0
  244. package/src/runtime/init-wizard.ts +354 -0
  245. package/src/runtime/ios-screen-record.test.ts +68 -0
  246. package/src/runtime/ios-screen-record.ts +155 -0
  247. package/src/runtime/journey-editor.ts +452 -0
  248. package/src/runtime/journey-evidence.test.ts +161 -0
  249. package/src/runtime/journey-evidence.ts +180 -0
  250. package/src/runtime/journey-graph.test.ts +257 -0
  251. package/src/runtime/journey-graph.ts +170 -0
  252. package/src/runtime/legacy-names.ts +32 -0
  253. package/src/runtime/llm-example.ts +105 -0
  254. package/src/runtime/llm.ts +527 -0
  255. package/src/runtime/local-browser.test.ts +45 -0
  256. package/src/runtime/local-browser.ts +48 -0
  257. package/src/runtime/local-registry-stub.test.ts +325 -0
  258. package/src/runtime/local-registry-stub.ts +803 -0
  259. package/src/runtime/maestro-driver.test.ts +84 -0
  260. package/src/runtime/maestro-driver.ts +209 -0
  261. package/src/runtime/mole-voice.ts +21 -0
  262. package/src/runtime/nav-crawl.test.ts +100 -0
  263. package/src/runtime/nav-crawl.ts +153 -0
  264. package/src/runtime/pipeline.test.ts +405 -0
  265. package/src/runtime/pipeline.ts +833 -0
  266. package/src/runtime/planner.test.ts +37 -0
  267. package/src/runtime/planner.ts +274 -0
  268. package/src/runtime/platform-ai.ts +76 -0
  269. package/src/runtime/playwright-driver.test.ts +93 -0
  270. package/src/runtime/playwright-driver.ts +620 -0
  271. package/src/runtime/project-spec.ts +140 -0
  272. package/src/runtime/record-run-verdicts.ts +68 -0
  273. package/src/runtime/reporter.test.ts +56 -0
  274. package/src/runtime/reporter.ts +158 -0
  275. package/src/runtime/reset.test.ts +44 -0
  276. package/src/runtime/reset.ts +61 -0
  277. package/src/runtime/reviewer.test.ts +73 -0
  278. package/src/runtime/reviewer.ts +158 -0
  279. package/src/runtime/run-job.ts +136 -0
  280. package/src/runtime/run-once.test.ts +207 -0
  281. package/src/runtime/run-once.ts +168 -0
  282. package/src/runtime/run.ts +132 -0
  283. package/src/runtime/screen-recording.ts +34 -0
  284. package/src/runtime/serve-gateway.test.ts +74 -0
  285. package/src/runtime/serve-gateway.ts +164 -0
  286. package/src/runtime/serve-worker.test.ts +23 -0
  287. package/src/runtime/serve-worker.ts +278 -0
  288. package/src/runtime/site-discovery.test.ts +168 -0
  289. package/src/runtime/site-discovery.ts +308 -0
  290. package/src/runtime/target-runner.ts +144 -0
  291. package/src/runtime/test-case-verdicts.test.ts +94 -0
  292. package/src/runtime/test-case-verdicts.ts +120 -0
  293. package/src/runtime/ui-reverse-engineering/coordinator.test.ts +59 -0
  294. package/src/runtime/ui-reverse-engineering/coordinator.ts +391 -0
  295. package/src/runtime/ui-reverse-engineering/types.ts +197 -0
  296. package/src/runtime/web-suite.test.ts +97 -0
  297. package/src/runtime/web-suite.ts +176 -0
  298. package/src/storage/create-object-store.ts +144 -0
  299. package/src/storage/keys.ts +34 -0
  300. package/src/storage/local-artifact-server.test.ts +314 -0
  301. package/src/storage/local-artifact-server.ts +357 -0
  302. package/src/storage/object-store.test.ts +28 -0
  303. package/src/storage/object-store.ts +101 -0
  304. package/src/storage/registry-object-store.ts +88 -0
  305. package/src/storage/remote-object-store.ts +104 -0
  306. package/src/storage/storage-directory.test.ts +43 -0
  307. package/src/storage/storage-directory.ts +24 -0
  308. package/tsconfig.json +24 -0
@@ -0,0 +1,194 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://app.bugmole.com/schemas/ui-parity-audit.schema.json",
4
+ "title": "Bugmole UI Parity Audit",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schemaVersion",
9
+ "auditId",
10
+ "reference",
11
+ "current",
12
+ "summary",
13
+ "flowMatrix",
14
+ "comparisons",
15
+ "issues",
16
+ "fixPrompt",
17
+ "blockedVerification"
18
+ ],
19
+ "properties": {
20
+ "schemaVersion": { "const": 1 },
21
+ "auditId": { "type": "string", "minLength": 1 },
22
+ "workspaceId": { "type": "string" },
23
+ "projectId": { "type": "string" },
24
+ "appId": { "type": "string" },
25
+ "appName": { "type": "string" },
26
+ "dnaRevision": { "type": "integer", "minimum": 1 },
27
+ "designDnaRefs": { "type": "array", "items": { "type": "string" } },
28
+ "tokenDrift": { "type": "array", "items": { "$ref": "#/$defs/tokenDrift" } },
29
+ "startedAt": { "type": "string", "format": "date-time" },
30
+ "completedAt": { "type": "string", "format": "date-time" },
31
+ "reference": { "$ref": "#/$defs/experience" },
32
+ "current": { "$ref": "#/$defs/experience" },
33
+ "summary": {
34
+ "type": "object",
35
+ "additionalProperties": false,
36
+ "required": [
37
+ "screensTotal",
38
+ "actionsTotal",
39
+ "visualDifferencePercent",
40
+ "interactionParityScore",
41
+ "apiParityScore",
42
+ "authenticationParityScore",
43
+ "fixedIssues",
44
+ "unresolvedIssues"
45
+ ],
46
+ "properties": {
47
+ "screensTotal": { "type": "integer", "minimum": 0 },
48
+ "actionsTotal": { "type": "integer", "minimum": 0 },
49
+ "visualDifferencePercent": { "type": "number", "minimum": 0, "maximum": 100 },
50
+ "interactionParityScore": { "type": "number", "minimum": 0, "maximum": 100 },
51
+ "apiParityScore": { "type": "number", "minimum": 0, "maximum": 100 },
52
+ "authenticationParityScore": { "type": "number", "minimum": 0, "maximum": 100 },
53
+ "fixedIssues": { "type": "integer", "minimum": 0 },
54
+ "unresolvedIssues": { "type": "integer", "minimum": 0 }
55
+ }
56
+ },
57
+ "flowMatrix": {
58
+ "type": "array",
59
+ "items": { "$ref": "#/$defs/flow" }
60
+ },
61
+ "comparisons": {
62
+ "type": "array",
63
+ "items": { "$ref": "#/$defs/comparison" }
64
+ },
65
+ "issues": {
66
+ "type": "array",
67
+ "items": { "$ref": "#/$defs/issue" }
68
+ },
69
+ "fixPrompt": { "type": "string" },
70
+ "artifacts": {
71
+ "type": "array",
72
+ "items": { "$ref": "#/$defs/artifact" }
73
+ },
74
+ "blockedVerification": {
75
+ "type": "array",
76
+ "items": {
77
+ "type": "object",
78
+ "additionalProperties": false,
79
+ "required": ["description", "reason"],
80
+ "properties": {
81
+ "description": { "type": "string" },
82
+ "reason": { "type": "string" }
83
+ }
84
+ }
85
+ }
86
+ },
87
+ "$defs": {
88
+ "experience": {
89
+ "type": "object",
90
+ "additionalProperties": false,
91
+ "required": ["kind", "location"],
92
+ "properties": {
93
+ "kind": { "enum": ["url", "html", "old_app_link", "image", "figma", "other"] },
94
+ "location": { "type": "string" },
95
+ "viewport": { "type": "string" },
96
+ "notes": { "type": "string" }
97
+ }
98
+ },
99
+ "flow": {
100
+ "type": "object",
101
+ "additionalProperties": false,
102
+ "required": ["flowId", "referenceScreen", "currentRoute", "authState", "actions", "status"],
103
+ "properties": {
104
+ "flowId": { "type": "string" },
105
+ "referenceScreen": { "type": "string" },
106
+ "currentRoute": { "type": "string" },
107
+ "authState": { "enum": ["guest", "authenticated", "logged_out", "unknown"] },
108
+ "states": { "type": "array", "items": { "type": "string" } },
109
+ "actions": { "type": "array", "items": { "type": "string" } },
110
+ "status": { "enum": ["matched", "partially_matched", "unmatched", "blocked"] },
111
+ "notes": { "type": "string" }
112
+ }
113
+ },
114
+ "comparison": {
115
+ "type": "object",
116
+ "additionalProperties": false,
117
+ "required": [
118
+ "screenId",
119
+ "differencePercent",
120
+ "changedPixels",
121
+ "totalPixels",
122
+ "heatmap",
123
+ "mismatches"
124
+ ],
125
+ "properties": {
126
+ "screenId": { "type": "string" },
127
+ "referenceCapture": { "type": "string" },
128
+ "currentCapture": { "type": "string" },
129
+ "diffCapture": { "type": "string" },
130
+ "heatmapCapture": { "type": "string" },
131
+ "differencePercent": { "type": "number", "minimum": 0, "maximum": 100 },
132
+ "changedPixels": { "type": "integer", "minimum": 0 },
133
+ "totalPixels": { "type": "integer", "minimum": 1 },
134
+ "threshold": { "type": "integer", "minimum": 0, "maximum": 255 },
135
+ "heatmap": { "type": "string" },
136
+ "mismatches": { "type": "array", "items": { "type": "string" } },
137
+ "behavioralMismatches": { "type": "array", "items": { "type": "string" } },
138
+ "backendMismatches": { "type": "array", "items": { "type": "string" } }
139
+ }
140
+ },
141
+ "issue": {
142
+ "type": "object",
143
+ "additionalProperties": false,
144
+ "required": ["issueId", "category", "status", "description"],
145
+ "properties": {
146
+ "issueId": { "type": "string" },
147
+ "category": { "enum": ["visual", "behavioral", "backend", "authentication", "platform"] },
148
+ "status": {
149
+ "enum": [
150
+ "confirmed_match",
151
+ "fixed",
152
+ "unresolved",
153
+ "blocked",
154
+ "platform_limitation",
155
+ "missing_backend"
156
+ ]
157
+ },
158
+ "severity": { "enum": ["high", "medium", "low"] },
159
+ "description": { "type": "string" },
160
+ "evidence": { "type": "array", "items": { "type": "string" } },
161
+ "fixPrompt": { "type": "string" },
162
+ "screenId": {
163
+ "type": "string",
164
+ "description": "The comparisons[].screenId (and, when the flow is graph-linked, the matching journey node id) this issue was found on. Optional so older audits without it remain valid; the dashboard groups issues without a screenId as ungrouped rather than guessing."
165
+ }
166
+ }
167
+ },
168
+ "artifact": {
169
+ "type": "object",
170
+ "additionalProperties": false,
171
+ "required": ["kind", "path"],
172
+ "properties": {
173
+ "kind": {
174
+ "enum": ["reference_screenshot", "current_screenshot", "diff", "heatmap", "log", "other"]
175
+ },
176
+ "path": { "type": "string" },
177
+ "screenId": { "type": "string" }
178
+ }
179
+ },
180
+ "tokenDrift": {
181
+ "type": "object",
182
+ "additionalProperties": false,
183
+ "required": ["tokenId", "category", "description", "severity"],
184
+ "properties": {
185
+ "tokenId": { "type": "string" },
186
+ "category": { "type": "string" },
187
+ "description": { "type": "string" },
188
+ "severity": { "enum": ["high", "medium", "low"] },
189
+ "referenceValue": {},
190
+ "currentValue": {}
191
+ }
192
+ }
193
+ }
194
+ }
@@ -0,0 +1,94 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://app.bugmole.com/schemas/ui-reverse-engineering.schema.json",
4
+ "title": "Bugmole UI Reverse Engineering Job",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "jobId", "projectId", "dnaRevision", "status", "stage", "inputs", "regions", "atomicNodes", "mismatches", "playbooks"],
8
+ "properties": {
9
+ "schemaVersion": { "const": 1 },
10
+ "jobId": { "type": "string", "minLength": 1 },
11
+ "projectId": { "type": "string", "minLength": 1 },
12
+ "dnaRevision": { "type": "integer", "minimum": 1 },
13
+ "status": { "enum": ["queued", "running", "blocked", "completed", "failed"] },
14
+ "stage": { "enum": ["input", "measure", "layout", "dna", "atoms", "compose", "validate", "reconcile", "complete"] },
15
+ "inputs": {
16
+ "type": "array",
17
+ "items": {
18
+ "type": "object",
19
+ "additionalProperties": false,
20
+ "required": ["id", "kind", "location"],
21
+ "properties": {
22
+ "id": { "type": "string", "minLength": 1 },
23
+ "kind": { "enum": ["image", "figma", "pdf", "url", "html", "existing_app"] },
24
+ "location": { "type": "string", "minLength": 1 },
25
+ "viewport": {
26
+ "type": "object",
27
+ "additionalProperties": false,
28
+ "properties": {
29
+ "width": { "type": "number", "minimum": 1 },
30
+ "height": { "type": "number", "minimum": 1 }
31
+ }
32
+ },
33
+ "checksum": { "type": "string" },
34
+ "status": { "enum": ["active", "archived"] },
35
+ "relatedJourneyIds": { "type": "array", "items": { "type": "string" } },
36
+ "appId": { "type": "string" },
37
+ "environmentId": { "type": "string" },
38
+ "registeredAt": { "type": "string", "format": "date-time" },
39
+ "metadata": { "type": "object" }
40
+ }
41
+ }
42
+ },
43
+ "regions": { "type": "array" },
44
+ "atomicNodes": { "type": "array" },
45
+ "mismatches": { "type": "array" },
46
+ "playbooks": {
47
+ "type": "array",
48
+ "items": {
49
+ "type": "object",
50
+ "additionalProperties": false,
51
+ "required": ["id", "name", "purpose", "status", "steps"],
52
+ "properties": {
53
+ "id": { "enum": ["design-dna", "atomic-design"] },
54
+ "name": { "type": "string" },
55
+ "purpose": { "type": "string" },
56
+ "status": { "enum": ["active", "blocked"] },
57
+ "steps": { "type": "array", "items": { "type": "string" } },
58
+ "referenceUrl": { "type": "string", "format": "uri" }
59
+ }
60
+ }
61
+ },
62
+ "storybook": {
63
+ "type": "object",
64
+ "additionalProperties": false,
65
+ "required": ["detected", "components"],
66
+ "properties": {
67
+ "detected": { "type": "boolean" },
68
+ "configPath": { "type": "string" },
69
+ "blockedReason": { "type": "string" },
70
+ "components": {
71
+ "type": "array",
72
+ "items": {
73
+ "type": "object",
74
+ "additionalProperties": false,
75
+ "required": ["id", "title", "storyNames", "sourcePath", "level", "confidence"],
76
+ "properties": {
77
+ "id": { "type": "string" },
78
+ "title": { "type": "string" },
79
+ "componentName": { "type": "string" },
80
+ "storyNames": { "type": "array", "items": { "type": "string" } },
81
+ "sourcePath": { "type": "string" },
82
+ "level": { "enum": ["atom", "molecule", "organism", "template", "page"] },
83
+ "confidence": { "enum": ["high", "medium", "low", "blocked"] }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ },
89
+ "playbookRefs": { "type": "array", "items": { "type": "string" } },
90
+ "blockedReasons": { "type": "array", "items": { "type": "string" } },
91
+ "createdAt": { "type": "string", "format": "date-time" },
92
+ "updatedAt": { "type": "string", "format": "date-time" }
93
+ }
94
+ }
@@ -0,0 +1,46 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import test from "node:test";
4
+ import { COPY_HEADER, catalogCopy, catalogSource } from "../../scripts/sync-plan-catalog.mjs";
5
+ import { AI_CREDIT_COST, PLANS, entitlementsFor, formatUsd, includedQuantity, overageCents } from "./plan-catalog.js";
6
+
7
+ test("the dashboard's copy of the plan catalog matches the source", async () => {
8
+ const [source, copy] = await Promise.all([readFile(catalogSource, "utf8"), readFile(catalogCopy, "utf8")]);
9
+ assert.equal(copy, COPY_HEADER + source, "run `npm run sync:catalog`");
10
+ });
11
+
12
+ test("plans get strictly more capacity as they get more expensive", () => {
13
+ const paid = [PLANS.free, PLANS.team, PLANS.business];
14
+ for (let index = 1; index < paid.length; index++) {
15
+ for (const key of ["cloudMinutes", "aiCredits", "parallel", "historyDays", "projects"] as const) {
16
+ assert.ok((paid[index][key] ?? Infinity) > (paid[index - 1][key] ?? 0), `${paid[index].id}.${key}`);
17
+ }
18
+ assert.ok(paid[index].priceMonthlyCents! > paid[index - 1].priceMonthlyCents!);
19
+ assert.ok(paid[index].priceYearlyMonthlyCents! < paid[index].priceMonthlyCents!, "yearly is a discount");
20
+ }
21
+ });
22
+
23
+ test("free usage stops at the allowance while paid plans can buy more", () => {
24
+ const free = entitlementsFor("free");
25
+ assert.equal(overageCents(free, "browser_minute"), null);
26
+ assert.equal(overageCents(free, "device_minute"), null);
27
+ const team = entitlementsFor("team");
28
+ assert.equal(overageCents(team, "browser_minute"), 4);
29
+ assert.equal(overageCents(team, "device_minute"), 25);
30
+ assert.equal(includedQuantity(team, "device_minute"), 0);
31
+ assert.equal(includedQuantity(team, "ai_credit"), 1500);
32
+ });
33
+
34
+ test("contract overrides replace only what they name", () => {
35
+ const custom = entitlementsFor("enterprise", { cloudMinutes: 50_000, features: { scim: false } });
36
+ assert.equal(custom.cloudMinutes, 50_000);
37
+ assert.equal(custom.historyDays, 365);
38
+ assert.equal(custom.features.scim, false);
39
+ assert.equal(custom.features.sso, true);
40
+ });
41
+
42
+ test("AI credit costs match the pricing page", () => {
43
+ assert.deepEqual(AI_CREDIT_COST, { analysis: 1, repair: 20, exploration: 100, discovery: 0 });
44
+ assert.equal(formatUsd(24_900), "$249");
45
+ assert.equal(formatUsd(4), "$0.04");
46
+ });
@@ -0,0 +1,199 @@
1
+ // The one definition of Bugmole's plans. The registry enforces it, the
2
+ // dashboard shows it, and the bugmole.com pricing page is built from it, so a
3
+ // price or limit changes in exactly one place.
4
+ //
5
+ // Keep this file dependency-free and limited to type-strippable TypeScript:
6
+ // Node imports it directly in the docs build, and `npm run sync:catalog`
7
+ // copies it verbatim into the dashboard (apps/qa/src/lib/plan-catalog.ts).
8
+
9
+ export type PlanId = "free" | "team" | "business" | "enterprise";
10
+ export type BillingInterval = "month" | "year";
11
+ export type UsageKind = "browser_minute" | "device_minute" | "ai_credit";
12
+ export type AiAction = "analysis" | "repair" | "exploration" | "discovery";
13
+
14
+ export type PlanFeatures = {
15
+ prChecks: boolean;
16
+ slack: boolean;
17
+ teams: boolean;
18
+ aiFixes: boolean;
19
+ deviceClouds: boolean;
20
+ sso: boolean;
21
+ scim: boolean;
22
+ auditLogExport: boolean;
23
+ prioritySupport: boolean;
24
+ selfHostedWorkers: boolean;
25
+ };
26
+
27
+ export type PlanDef = {
28
+ id: PlanId;
29
+ name: string;
30
+ audience: string;
31
+ /** Monthly price when billed monthly; null means custom pricing. */
32
+ priceMonthlyCents: number | null;
33
+ /** Per-month price when billed yearly; null when there is no yearly price. */
34
+ priceYearlyMonthlyCents: number | null;
35
+ /** null means no fixed limit (set per contract). */
36
+ projects: number | null;
37
+ cloudMinutes: number | null;
38
+ aiCredits: number | null;
39
+ parallel: number | null;
40
+ historyDays: number | null;
41
+ /** Cents per unit beyond the allowance; null means usage stops at the allowance. */
42
+ overage: { browserMinuteCents: number | null; creditCents: number | null };
43
+ features: PlanFeatures;
44
+ };
45
+
46
+ const NO_FEATURES: PlanFeatures = {
47
+ prChecks: false,
48
+ slack: false,
49
+ teams: false,
50
+ aiFixes: false,
51
+ deviceClouds: false,
52
+ sso: false,
53
+ scim: false,
54
+ auditLogExport: false,
55
+ prioritySupport: false,
56
+ selfHostedWorkers: false,
57
+ };
58
+
59
+ export const PLANS: Record<PlanId, PlanDef> = {
60
+ free: {
61
+ id: "free",
62
+ name: "Free",
63
+ audience: "Individuals and evaluation",
64
+ priceMonthlyCents: 0,
65
+ priceYearlyMonthlyCents: 0,
66
+ projects: 1,
67
+ cloudMinutes: 300,
68
+ aiCredits: 100,
69
+ parallel: 2,
70
+ historyDays: 7,
71
+ overage: { browserMinuteCents: null, creditCents: null },
72
+ features: { ...NO_FEATURES },
73
+ },
74
+ team: {
75
+ id: "team",
76
+ name: "Team",
77
+ audience: "Teams shipping continuously",
78
+ priceMonthlyCents: 24_900,
79
+ priceYearlyMonthlyCents: 19_900,
80
+ projects: 10,
81
+ cloudMinutes: 3_000,
82
+ aiCredits: 1_500,
83
+ parallel: 8,
84
+ historyDays: 30,
85
+ overage: { browserMinuteCents: 4, creditCents: 1 },
86
+ features: { ...NO_FEATURES, prChecks: true, slack: true, teams: true, aiFixes: true },
87
+ },
88
+ business: {
89
+ id: "business",
90
+ name: "Business",
91
+ audience: "Organizations testing across apps, devices and teams",
92
+ priceMonthlyCents: 89_900,
93
+ priceYearlyMonthlyCents: 71_900,
94
+ projects: 50,
95
+ cloudMinutes: 15_000,
96
+ aiCredits: 7_500,
97
+ parallel: 25,
98
+ historyDays: 90,
99
+ overage: { browserMinuteCents: 3, creditCents: 1 },
100
+ features: {
101
+ ...NO_FEATURES,
102
+ prChecks: true,
103
+ slack: true,
104
+ teams: true,
105
+ aiFixes: true,
106
+ deviceClouds: true,
107
+ sso: true,
108
+ auditLogExport: true,
109
+ prioritySupport: true,
110
+ },
111
+ },
112
+ enterprise: {
113
+ id: "enterprise",
114
+ name: "Enterprise",
115
+ audience: "Large and regulated organizations",
116
+ priceMonthlyCents: null,
117
+ priceYearlyMonthlyCents: null,
118
+ projects: null,
119
+ cloudMinutes: null,
120
+ aiCredits: null,
121
+ parallel: null,
122
+ historyDays: 365,
123
+ overage: { browserMinuteCents: 3, creditCents: 1 },
124
+ features: {
125
+ prChecks: true,
126
+ slack: true,
127
+ teams: true,
128
+ aiFixes: true,
129
+ deviceClouds: true,
130
+ sso: true,
131
+ scim: true,
132
+ auditLogExport: true,
133
+ prioritySupport: true,
134
+ selfHostedWorkers: true,
135
+ },
136
+ },
137
+ };
138
+
139
+ export const PLAN_IDS = Object.keys(PLANS) as PlanId[];
140
+
141
+ /** Credits each AI action costs. A project's own model key costs no credits. */
142
+ export const AI_CREDIT_COST: Record<AiAction, number> = {
143
+ analysis: 1,
144
+ repair: 20,
145
+ exploration: 100,
146
+ discovery: 0,
147
+ };
148
+
149
+ /** Real devices on Bugmole's own AWS Device Farm account: pay as you go, none included. */
150
+ export const DEVICE_FARM_CENTS_PER_MIN = 25;
151
+
152
+ /** Contract-specific adjustments stored with a workspace's plan (Enterprise, trials). */
153
+ export type PlanOverrides = Partial<Pick<PlanDef, "projects" | "cloudMinutes" | "aiCredits" | "parallel" | "historyDays">> & {
154
+ features?: Partial<PlanFeatures>;
155
+ overage?: Partial<PlanDef["overage"]>;
156
+ };
157
+
158
+ export type Entitlements = Omit<PlanDef, "name" | "audience" | "priceMonthlyCents" | "priceYearlyMonthlyCents">;
159
+
160
+ export function isPlanId(value: unknown): value is PlanId {
161
+ return typeof value === "string" && (PLAN_IDS as string[]).includes(value);
162
+ }
163
+
164
+ export function entitlementsFor(plan: PlanId, overrides: PlanOverrides = {}): Entitlements {
165
+ const base = PLANS[plan];
166
+ const pick = <K extends "projects" | "cloudMinutes" | "aiCredits" | "parallel" | "historyDays">(key: K) =>
167
+ overrides[key] !== undefined ? (overrides[key] as PlanDef[K]) : base[key];
168
+ return {
169
+ id: base.id,
170
+ projects: pick("projects"),
171
+ cloudMinutes: pick("cloudMinutes"),
172
+ aiCredits: pick("aiCredits"),
173
+ parallel: pick("parallel"),
174
+ historyDays: pick("historyDays"),
175
+ overage: { ...base.overage, ...overrides.overage },
176
+ features: { ...base.features, ...overrides.features },
177
+ };
178
+ }
179
+
180
+ /** How much of the allowance a usage kind has; device minutes have none on any plan. */
181
+ export function includedQuantity(entitlements: Entitlements, kind: UsageKind): number | null {
182
+ if (kind === "browser_minute") return entitlements.cloudMinutes;
183
+ if (kind === "ai_credit") return entitlements.aiCredits;
184
+ return 0;
185
+ }
186
+
187
+ /** Cents per unit beyond the allowance, or null when usage must stop at the allowance. */
188
+ export function overageCents(entitlements: Entitlements, kind: UsageKind): number | null {
189
+ if (kind === "browser_minute") return entitlements.overage.browserMinuteCents;
190
+ if (kind === "ai_credit") return entitlements.overage.creditCents;
191
+ // Real devices need a paid plan with a subscription; the registry checks that.
192
+ return entitlements.id === "free" ? null : DEVICE_FARM_CENTS_PER_MIN;
193
+ }
194
+
195
+ export function formatUsd(cents: number, { minimumFractionDigits }: { minimumFractionDigits?: number } = {}): string {
196
+ const dollars = cents / 100;
197
+ const fraction = minimumFractionDigits ?? (Number.isInteger(dollars) ? 0 : 2);
198
+ return `$${dollars.toLocaleString("en-US", { minimumFractionDigits: fraction, maximumFractionDigits: Math.max(fraction, 2) })}`;
199
+ }
@@ -0,0 +1,42 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { signAwsRequest } from "./aws-sigv4.js";
4
+
5
+ // AWS's published SigV4 example ("get-vanilla"), from the aws-sig-v4 test suite,
6
+ // adjusted for the x-amz-content-sha256 header this signer always includes.
7
+ test("SigV4 matches an independent calculation", async () => {
8
+ const { createHash, createHmac } = await import("node:crypto");
9
+ const credentials = { accessKeyId: "AKIDEXAMPLE", secretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY" };
10
+ const now = new Date("2015-08-30T12:36:00Z");
11
+ const body = '{"arn":"x"}';
12
+ const headers = await signAwsRequest({
13
+ credentials, region: "us-west-2", service: "devicefarm", method: "POST",
14
+ url: "https://devicefarm.us-west-2.amazonaws.com/", headers: { "Content-Type": "application/x-amz-json-1.1", "X-Amz-Target": "DeviceFarm_20150623.ListDevices" },
15
+ body, now,
16
+ });
17
+ const payloadHash = createHash("sha256").update(body).digest("hex");
18
+ const canonical = [
19
+ "POST", "/", "",
20
+ `content-type:application/x-amz-json-1.1\nhost:devicefarm.us-west-2.amazonaws.com\nx-amz-content-sha256:${payloadHash}\nx-amz-date:20150830T123600Z\nx-amz-target:DeviceFarm_20150623.ListDevices\n`,
21
+ "content-type;host;x-amz-content-sha256;x-amz-date;x-amz-target",
22
+ payloadHash,
23
+ ].join("\n");
24
+ const scope = "20150830/us-west-2/devicefarm/aws4_request";
25
+ const toSign = ["AWS4-HMAC-SHA256", "20150830T123600Z", scope, createHash("sha256").update(canonical).digest("hex")].join("\n");
26
+ let key: Buffer = createHmac("sha256", `AWS4${credentials.secretAccessKey}`).update("20150830").digest();
27
+ for (const part of ["us-west-2", "devicefarm", "aws4_request"]) key = createHmac("sha256", key).update(part).digest();
28
+ const signature = createHmac("sha256", key).update(toSign).digest("hex");
29
+ assert.equal(headers.authorization, `AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/${scope}, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date;x-amz-target, Signature=${signature}`);
30
+ assert.equal(headers["x-amz-date"], "20150830T123600Z");
31
+ assert.equal(headers.host, undefined, "fetch sets the host itself");
32
+ });
33
+
34
+ test("the AWS reference vector for a known key derivation", async () => {
35
+ // From AWS docs "Examples of how to derive a signing key": the canonical get-vanilla request.
36
+ const headers = await signAwsRequest({
37
+ credentials: { accessKeyId: "AKIDEXAMPLE", secretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY" },
38
+ region: "us-east-1", service: "service", method: "GET", url: "https://example.amazonaws.com/?Param2=value2&Param1=value1",
39
+ headers: {}, body: "", now: new Date("2015-08-30T12:36:00Z"),
40
+ });
41
+ assert.match(headers.authorization, /^AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE\/20150830\/us-east-1\/service\/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=[0-9a-f]{64}$/);
42
+ });
@@ -0,0 +1,72 @@
1
+ // AWS Signature Version 4 with WebCrypto, so the same code signs requests in
2
+ // Cloudflare Workers and in Node.
3
+
4
+ export type AwsCredentials = { accessKeyId: string; secretAccessKey: string; sessionToken?: string };
5
+
6
+ const encoder = new TextEncoder();
7
+
8
+ function hex(bytes: ArrayBuffer | Uint8Array): string {
9
+ return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
10
+ }
11
+
12
+ async function sha256Hex(value: string): Promise<string> {
13
+ return hex(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
14
+ }
15
+
16
+ async function hmac(key: ArrayBuffer | Uint8Array, value: string): Promise<ArrayBuffer> {
17
+ const imported = await crypto.subtle.importKey("raw", key as BufferSource, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
18
+ return crypto.subtle.sign("HMAC", imported, encoder.encode(value));
19
+ }
20
+
21
+ /** Signs a request with a string body. Returns the headers to send. */
22
+ export async function signAwsRequest(params: {
23
+ credentials: AwsCredentials;
24
+ region: string;
25
+ service: string;
26
+ method: string;
27
+ url: string;
28
+ headers: Record<string, string>;
29
+ body: string;
30
+ now?: Date;
31
+ }): Promise<Record<string, string>> {
32
+ const now = params.now ?? new Date();
33
+ const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
34
+ const date = amzDate.slice(0, 8);
35
+ const url = new URL(params.url);
36
+ const payloadHash = await sha256Hex(params.body);
37
+ const headers: Record<string, string> = {
38
+ ...Object.fromEntries(Object.entries(params.headers).map(([key, value]) => [key.toLowerCase(), value.trim()])),
39
+ host: url.host,
40
+ "x-amz-date": amzDate,
41
+ "x-amz-content-sha256": payloadHash,
42
+ ...(params.credentials.sessionToken ? { "x-amz-security-token": params.credentials.sessionToken } : {}),
43
+ };
44
+ const names = Object.keys(headers).sort();
45
+ const canonicalQuery = [...url.searchParams.entries()]
46
+ .map(([key, value]) => [encodeRfc3986(key), encodeRfc3986(value)])
47
+ .sort(([a, x], [b, y]) => (a === b ? (x < y ? -1 : 1) : a < b ? -1 : 1))
48
+ .map(([key, value]) => `${key}=${value}`)
49
+ .join("&");
50
+ const canonicalRequest = [
51
+ params.method.toUpperCase(),
52
+ url.pathname.split("/").map((part) => encodeRfc3986(decodeURIComponent(part))).join("/") || "/",
53
+ canonicalQuery,
54
+ names.map((name) => `${name}:${headers[name]}\n`).join(""),
55
+ names.join(";"),
56
+ payloadHash,
57
+ ].join("\n");
58
+ const scope = `${date}/${params.region}/${params.service}/aws4_request`;
59
+ const stringToSign = ["AWS4-HMAC-SHA256", amzDate, scope, await sha256Hex(canonicalRequest)].join("\n");
60
+ let key = await hmac(encoder.encode(`AWS4${params.credentials.secretAccessKey}`), date);
61
+ for (const part of [params.region, params.service, "aws4_request"]) key = await hmac(key, part);
62
+ const signature = hex(await hmac(key, stringToSign));
63
+ const { host: _host, ...rest } = headers;
64
+ return {
65
+ ...rest,
66
+ authorization: `AWS4-HMAC-SHA256 Credential=${params.credentials.accessKeyId}/${scope}, SignedHeaders=${names.join(";")}, Signature=${signature}`,
67
+ };
68
+ }
69
+
70
+ function encodeRfc3986(value: string): string {
71
+ return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
72
+ }