@shortlink-org/portolan 0.1.0 → 0.2.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 (381) hide show
  1. package/README.md +69 -205
  2. package/cli/init.mjs +285 -0
  3. package/cli/init.test.mjs +158 -0
  4. package/cli/portolan.mjs +37 -73
  5. package/index.html +19 -0
  6. package/package.json +5 -10
  7. package/plugins/README.md +66 -27
  8. package/plugins/portolan-go.wasm +0 -0
  9. package/portolan.json +52 -73
  10. package/public/landing/cats/bug-hunter.webp +0 -0
  11. package/public/landing/cats/compass-nap.webp +0 -0
  12. package/public/landing/cats/diagrammer.webp +0 -0
  13. package/public/landing/cats/docs-reader.webp +0 -0
  14. package/public/landing/cats/laptop.webp +0 -0
  15. package/public/landing/cats/map-inspector.webp +0 -0
  16. package/public/landing/cats/server-break.webp +0 -0
  17. package/public/landing/cats/star-mapper.webp +0 -0
  18. package/public/landing/cats/system-builder.webp +0 -0
  19. package/public/landing/cats/thread-tangle.webp +0 -0
  20. package/public/og.png +0 -0
  21. package/public/readme-header.webp +0 -0
  22. package/schema/portolan.schema.json +180 -0
  23. package/scripts/builtin-plugins.mjs +14 -0
  24. package/scripts/delivery-presets.mjs +353 -0
  25. package/scripts/gen.mjs +37 -4
  26. package/scripts/history.mjs +126 -0
  27. package/scripts/history.test.mjs +110 -0
  28. package/scripts/host-plugins/fetch-bsr.mjs +351 -0
  29. package/{plugins/fetch-bsr/options.schema.json → scripts/host-plugins/fetch-bsr.options.json} +4 -0
  30. package/scripts/host-plugins/fetch-bsr.test.mjs +212 -0
  31. package/scripts/host-plugins/fetch-csr.mjs +386 -0
  32. package/scripts/host-plugins/fetch-csr.test.mjs +178 -0
  33. package/scripts/host-plugins/fetch-git.mjs +379 -0
  34. package/scripts/host-plugins/fetch-git.test.mjs +210 -0
  35. package/scripts/local-api.mjs +22 -4
  36. package/scripts/local-api.test.mjs +84 -2
  37. package/scripts/manifest.mjs +3 -2
  38. package/scripts/package-smoke.mjs +6 -2
  39. package/scripts/plugin-host.mjs +77 -18
  40. package/scripts/plugin-host.test.mjs +50 -1
  41. package/scripts/plugin-wasm-worker.mjs +6 -3
  42. package/scripts/run-builtin.mjs +6 -26
  43. package/scripts/schema.mjs +6 -1
  44. package/src/app/App.tsx +24 -429
  45. package/src/app/Breadcrumbs.test.ts +7 -0
  46. package/src/app/Breadcrumbs.tsx +15 -2
  47. package/src/app/CatalogApp.tsx +436 -0
  48. package/src/app/Sidebar.tsx +0 -1
  49. package/src/chat/ChatPanel.tsx +98 -41
  50. package/src/chat/Composer.tsx +4 -2
  51. package/src/chat/Conversation.tsx +79 -15
  52. package/src/chat/Header.tsx +43 -9
  53. package/src/chat/Starter.tsx +64 -3
  54. package/src/chat/Steps.tsx +1 -1
  55. package/src/chat/flags.test.ts +51 -14
  56. package/src/chat/flags.ts +8 -2
  57. package/src/chat/page-context.test.ts +51 -0
  58. package/src/chat/page-context.ts +128 -0
  59. package/src/chat/prompt.test.ts +49 -1
  60. package/src/chat/prompt.ts +42 -1
  61. package/src/chat/tools.ts +7 -2
  62. package/src/chat/transport.ts +16 -4
  63. package/src/components/ProblemRow.tsx +182 -0
  64. package/src/data.ts +20 -5
  65. package/src/er/ErCanvas.tsx +1 -1
  66. package/src/graph/DependencyGraph.tsx +10 -1
  67. package/src/graph/FocusedEventGraph.tsx +1 -1
  68. package/src/graph/GraphToolbar.tsx +2 -4
  69. package/src/index.css +220 -2
  70. package/src/landing/DraggableReveal.tsx +168 -0
  71. package/src/landing/EstateGraph.tsx +28 -0
  72. package/src/landing/FlowPlayback.tsx +282 -0
  73. package/src/landing/HeroMap.tsx +128 -0
  74. package/src/landing/LandingPage.tsx +757 -0
  75. package/src/landing/ProductFrame.tsx +68 -0
  76. package/src/landing/ProductTour.tsx +371 -0
  77. package/src/landing/catalog.ts +25 -0
  78. package/src/landing/motion.tsx +52 -0
  79. package/src/lib/all-problems.ts +28 -0
  80. package/src/lib/local-api.ts +35 -1
  81. package/src/lib/setup-info.test.ts +8 -0
  82. package/src/lib/setup-info.ts +3 -2
  83. package/src/lib/source-code.ts +2 -1
  84. package/src/likec4/C4View.tsx +5 -0
  85. package/src/likec4/InteractiveView.tsx +5 -1
  86. package/src/map/ContextMapGraph.tsx +47 -20
  87. package/src/pages/ContextMap.tsx +5 -1
  88. package/src/pages/FlowDetail.tsx +24 -7
  89. package/src/pages/Overview.tsx +68 -12
  90. package/src/pages/Problems.tsx +6 -193
  91. package/src/pages/Settings.tsx +133 -108
  92. package/src/pages/settings/DeliverySettings.tsx +229 -0
  93. package/src/pages/settings/PreferencesSettings.tsx +101 -0
  94. package/src/routes.test.ts +20 -0
  95. package/src/routes.ts +13 -1
  96. package/vite.config.ts +12 -8
  97. package/catalog/enum_test.go +0 -46
  98. package/catalog/model.go +0 -932
  99. package/catalog/roundtrip_test.go +0 -185
  100. package/catalog/via_test.go +0 -38
  101. package/go.mod +0 -14
  102. package/go.sum +0 -14
  103. package/internal/commands/cargo.go +0 -154
  104. package/internal/commands/commands.go +0 -143
  105. package/internal/commands/commands_test.go +0 -323
  106. package/internal/commands/gradle.go +0 -77
  107. package/internal/commands/justfile.go +0 -82
  108. package/internal/commands/makefile.go +0 -106
  109. package/internal/commands/maven.go +0 -93
  110. package/internal/commands/packagejson.go +0 -159
  111. package/internal/commands/pyproject.go +0 -169
  112. package/internal/commands/taskfile.go +0 -101
  113. package/internal/commands/testdata/estate/.cargo/config.toml +0 -7
  114. package/internal/commands/testdata/estate/Makefile +0 -33
  115. package/internal/commands/testdata/estate/Taskfile.yml +0 -21
  116. package/internal/commands/testdata/estate/build.gradle.kts +0 -21
  117. package/internal/commands/testdata/estate/justfile +0 -20
  118. package/internal/commands/testdata/estate/package.json +0 -13
  119. package/internal/commands/testdata/estate/pom.xml +0 -29
  120. package/internal/commands/testdata/estate/pyproject.toml +0 -27
  121. package/internal/commands/testdata/estate/xtask/src/main.rs +0 -27
  122. package/internal/commands/testdata/golden/commands.json +0 -305
  123. package/internal/gohttp/analyze.go +0 -2500
  124. package/internal/gohttp/endpoints.go +0 -1067
  125. package/internal/gohttp/roots.go +0 -320
  126. package/internal/gohttp/typed.go +0 -143
  127. package/internal/goscan/constants.go +0 -85
  128. package/internal/goscan/goscan_test.go +0 -227
  129. package/internal/goscan/names.go +0 -52
  130. package/internal/goscan/parse_test.go +0 -11
  131. package/internal/goscan/source.go +0 -36
  132. package/internal/goscan/tree.go +0 -155
  133. package/internal/goscan/types.go +0 -99
  134. package/internal/wsdl/ids.go +0 -127
  135. package/internal/wsdl/ids_test.go +0 -21
  136. package/internal/wsdl/model.go +0 -62
  137. package/internal/wsdl/parse.go +0 -920
  138. package/internal/wsdl/parse_test.go +0 -133
  139. package/plugin/describe.go +0 -107
  140. package/plugin/describe_test.go +0 -114
  141. package/plugin/protocol.go +0 -117
  142. package/plugin/schematest/schematest.go +0 -126
  143. package/plugins/extract-adr/describe.go +0 -19
  144. package/plugins/extract-adr/describe_test.go +0 -11
  145. package/plugins/extract-adr/extract.go +0 -153
  146. package/plugins/extract-adr/extract_test.go +0 -350
  147. package/plugins/extract-adr/history.go +0 -99
  148. package/plugins/extract-adr/main.go +0 -65
  149. package/plugins/extract-adr/parse.go +0 -642
  150. package/plugins/extract-adr/parse_test.go +0 -405
  151. package/plugins/extract-asyncapi/describe.go +0 -19
  152. package/plugins/extract-asyncapi/describe_test.go +0 -11
  153. package/plugins/extract-asyncapi/extract.go +0 -315
  154. package/plugins/extract-asyncapi/extract_test.go +0 -197
  155. package/plugins/extract-asyncapi/main.go +0 -48
  156. package/plugins/extract-asyncapi/spec.go +0 -151
  157. package/plugins/extract-commands/describe.go +0 -19
  158. package/plugins/extract-commands/describe_test.go +0 -11
  159. package/plugins/extract-commands/extract.go +0 -79
  160. package/plugins/extract-commands/extract_test.go +0 -92
  161. package/plugins/extract-commands/main.go +0 -55
  162. package/plugins/extract-csr/avro.go +0 -258
  163. package/plugins/extract-csr/describe.go +0 -19
  164. package/plugins/extract-csr/describe_test.go +0 -11
  165. package/plugins/extract-csr/extract.go +0 -338
  166. package/plugins/extract-csr/extract_test.go +0 -374
  167. package/plugins/extract-csr/jsonschema.go +0 -343
  168. package/plugins/extract-csr/lock.go +0 -25
  169. package/plugins/extract-csr/main.go +0 -84
  170. package/plugins/extract-csr/subject.go +0 -102
  171. package/plugins/extract-flows/describe.go +0 -19
  172. package/plugins/extract-flows/describe_test.go +0 -11
  173. package/plugins/extract-flows/extract.go +0 -91
  174. package/plugins/extract-flows/main.go +0 -45
  175. package/plugins/extract-flows/parse.go +0 -593
  176. package/plugins/extract-flows/parse_test.go +0 -204
  177. package/plugins/extract-glossary/describe.go +0 -19
  178. package/plugins/extract-glossary/describe_test.go +0 -11
  179. package/plugins/extract-glossary/extract.go +0 -115
  180. package/plugins/extract-glossary/extract_test.go +0 -220
  181. package/plugins/extract-glossary/main.go +0 -59
  182. package/plugins/extract-glossary/parse.go +0 -214
  183. package/plugins/extract-glossary/parse_test.go +0 -203
  184. package/plugins/extract-go/aggregate.go +0 -214
  185. package/plugins/extract-go/client.go +0 -409
  186. package/plugins/extract-go/client_test.go +0 -266
  187. package/plugins/extract-go/describe.go +0 -19
  188. package/plugins/extract-go/describe_test.go +0 -11
  189. package/plugins/extract-go/enum.go +0 -195
  190. package/plugins/extract-go/enum_test.go +0 -82
  191. package/plugins/extract-go/event.go +0 -99
  192. package/plugins/extract-go/extract.go +0 -191
  193. package/plugins/extract-go/extract_test.go +0 -261
  194. package/plugins/extract-go/flow.go +0 -1441
  195. package/plugins/extract-go/flow_test.go +0 -609
  196. package/plugins/extract-go/httpclient.go +0 -174
  197. package/plugins/extract-go/httpclient_test.go +0 -296
  198. package/plugins/extract-go/ids.go +0 -92
  199. package/plugins/extract-go/layout.go +0 -225
  200. package/plugins/extract-go/layout_test.go +0 -125
  201. package/plugins/extract-go/lifecycle.go +0 -324
  202. package/plugins/extract-go/lifecycle_test.go +0 -108
  203. package/plugins/extract-go/main.go +0 -79
  204. package/plugins/extract-go/operation.go +0 -157
  205. package/plugins/extract-go/source.go +0 -316
  206. package/plugins/extract-go/transport.go +0 -321
  207. package/plugins/extract-go/transport_test.go +0 -145
  208. package/plugins/extract-go/wiring.go +0 -434
  209. package/plugins/extract-go-nats/describe.go +0 -19
  210. package/plugins/extract-go-nats/describe_test.go +0 -11
  211. package/plugins/extract-go-nats/extract.go +0 -177
  212. package/plugins/extract-go-nats/extract_test.go +0 -389
  213. package/plugins/extract-go-nats/index.go +0 -379
  214. package/plugins/extract-go-nats/main.go +0 -42
  215. package/plugins/extract-go-nats/resolve.go +0 -161
  216. package/plugins/extract-go-nats/sites.go +0 -224
  217. package/plugins/extract-graphql/describe.go +0 -19
  218. package/plugins/extract-graphql/describe_test.go +0 -11
  219. package/plugins/extract-graphql/extract.go +0 -433
  220. package/plugins/extract-graphql/extract_test.go +0 -256
  221. package/plugins/extract-graphql/ids.go +0 -49
  222. package/plugins/extract-graphql/lex.go +0 -237
  223. package/plugins/extract-graphql/main.go +0 -51
  224. package/plugins/extract-graphql/parse.go +0 -621
  225. package/plugins/extract-graphql/parse_test.go +0 -122
  226. package/plugins/extract-http-clients/describe.go +0 -19
  227. package/plugins/extract-http-clients/describe_test.go +0 -11
  228. package/plugins/extract-http-clients/extract.go +0 -705
  229. package/plugins/extract-http-clients/extract_test.go +0 -1263
  230. package/plugins/extract-http-clients/main.go +0 -42
  231. package/plugins/extract-openapi/describe.go +0 -19
  232. package/plugins/extract-openapi/describe_test.go +0 -11
  233. package/plugins/extract-openapi/discover.go +0 -243
  234. package/plugins/extract-openapi/extract.go +0 -526
  235. package/plugins/extract-openapi/extract_test.go +0 -545
  236. package/plugins/extract-openapi/main.go +0 -75
  237. package/plugins/extract-openapi/spec.go +0 -350
  238. package/plugins/extract-project/describe.go +0 -19
  239. package/plugins/extract-project/describe_test.go +0 -11
  240. package/plugins/extract-project/extract.go +0 -221
  241. package/plugins/extract-project/extract_test.go +0 -109
  242. package/plugins/extract-project/main.go +0 -41
  243. package/plugins/extract-proto/ast.go +0 -125
  244. package/plugins/extract-proto/consumes.go +0 -77
  245. package/plugins/extract-proto/describe.go +0 -19
  246. package/plugins/extract-proto/extract.go +0 -293
  247. package/plugins/extract-proto/extract_test.go +0 -459
  248. package/plugins/extract-proto/ids.go +0 -89
  249. package/plugins/extract-proto/ids_test.go +0 -57
  250. package/plugins/extract-proto/lex.go +0 -285
  251. package/plugins/extract-proto/main.go +0 -100
  252. package/plugins/extract-proto/module.go +0 -120
  253. package/plugins/extract-proto/parse.go +0 -720
  254. package/plugins/extract-proto/parse_test.go +0 -307
  255. package/plugins/extract-proto/provides.go +0 -236
  256. package/plugins/extract-proto/resolve.go +0 -222
  257. package/plugins/extract-proto/resolve_test.go +0 -119
  258. package/plugins/extract-redis/describe.go +0 -19
  259. package/plugins/extract-redis/describe_test.go +0 -11
  260. package/plugins/extract-redis/extract.go +0 -183
  261. package/plugins/extract-redis/extract_test.go +0 -168
  262. package/plugins/extract-redis/keyspaces.go +0 -469
  263. package/plugins/extract-redis/main.go +0 -44
  264. package/plugins/extract-river/describe.go +0 -19
  265. package/plugins/extract-river/describe_test.go +0 -11
  266. package/plugins/extract-river/extract.go +0 -507
  267. package/plugins/extract-river/extract_test.go +0 -132
  268. package/plugins/extract-river/main.go +0 -42
  269. package/plugins/extract-sql/ddl.go +0 -893
  270. package/plugins/extract-sql/ddl_test.go +0 -401
  271. package/plugins/extract-sql/describe.go +0 -19
  272. package/plugins/extract-sql/describe_test.go +0 -11
  273. package/plugins/extract-sql/layout.go +0 -221
  274. package/plugins/extract-sql/layout_test.go +0 -55
  275. package/plugins/extract-sql/lineage.go +0 -107
  276. package/plugins/extract-sql/main.go +0 -141
  277. package/plugins/extract-sql/maps.go +0 -564
  278. package/plugins/extract-sql/maps_java.go +0 -117
  279. package/plugins/extract-sql/maps_rust.go +0 -333
  280. package/plugins/extract-sql/maps_rust_test.go +0 -70
  281. package/plugins/extract-sql/maps_test.go +0 -204
  282. package/plugins/extract-sql/maps_ts.go +0 -398
  283. package/plugins/extract-sql/maps_ts_test.go +0 -136
  284. package/plugins/extract-sql/projection.go +0 -70
  285. package/plugins/extract-sql/projection_test.go +0 -50
  286. package/plugins/extract-sql/store.go +0 -420
  287. package/plugins/extract-sql/store_test.go +0 -233
  288. package/plugins/extract-sql/view.go +0 -295
  289. package/plugins/extract-watermill/describe.go +0 -19
  290. package/plugins/extract-watermill/describe_test.go +0 -11
  291. package/plugins/extract-watermill/extract.go +0 -1228
  292. package/plugins/extract-watermill/extract_test.go +0 -234
  293. package/plugins/extract-watermill/main.go +0 -41
  294. package/plugins/extract-wsdl/describe.go +0 -19
  295. package/plugins/extract-wsdl/describe_test.go +0 -11
  296. package/plugins/extract-wsdl/extract.go +0 -148
  297. package/plugins/extract-wsdl/extract_test.go +0 -45
  298. package/plugins/extract-wsdl/main.go +0 -53
  299. package/plugins/fetch-bsr/auth.go +0 -114
  300. package/plugins/fetch-bsr/bsr.go +0 -240
  301. package/plugins/fetch-bsr/cache.go +0 -66
  302. package/plugins/fetch-bsr/describe.go +0 -19
  303. package/plugins/fetch-bsr/fetch.go +0 -192
  304. package/plugins/fetch-bsr/fetch_test.go +0 -465
  305. package/plugins/fetch-bsr/lock.go +0 -71
  306. package/plugins/fetch-bsr/main.go +0 -117
  307. package/plugins/fetch-csr/auth.go +0 -95
  308. package/plugins/fetch-csr/cache.go +0 -69
  309. package/plugins/fetch-csr/describe.go +0 -19
  310. package/plugins/fetch-csr/describe_test.go +0 -11
  311. package/plugins/fetch-csr/fetch.go +0 -254
  312. package/plugins/fetch-csr/fetch_test.go +0 -494
  313. package/plugins/fetch-csr/lock.go +0 -95
  314. package/plugins/fetch-csr/main.go +0 -122
  315. package/plugins/fetch-csr/registry.go +0 -209
  316. package/plugins/fetch-git/cache.go +0 -65
  317. package/plugins/fetch-git/describe.go +0 -19
  318. package/plugins/fetch-git/describe_test.go +0 -11
  319. package/plugins/fetch-git/fetch.go +0 -181
  320. package/plugins/fetch-git/fetch_test.go +0 -332
  321. package/plugins/fetch-git/git.go +0 -169
  322. package/plugins/fetch-git/lock.go +0 -72
  323. package/plugins/fetch-git/main.go +0 -127
  324. package/plugins/fetch-git/offline.go +0 -39
  325. package/plugins/fetch-git/pin.go +0 -90
  326. package/plugins/fetch-git/pin_test.go +0 -74
  327. package/plugins/gen-backstage/describe.go +0 -17
  328. package/plugins/gen-backstage/main.go +0 -22
  329. package/plugins/gen-backstage/plugin.go +0 -473
  330. package/plugins/gen-backstage/plugin_test.go +0 -145
  331. package/plugins/gen-backstage.wasm +0 -0
  332. package/plugins/gen-markdown/adr.go +0 -162
  333. package/plugins/gen-markdown/aggregate.go +0 -362
  334. package/plugins/gen-markdown/canonical.go +0 -93
  335. package/plugins/gen-markdown/context.go +0 -90
  336. package/plugins/gen-markdown/coverage_test.go +0 -89
  337. package/plugins/gen-markdown/describe.go +0 -19
  338. package/plugins/gen-markdown/describe_test.go +0 -11
  339. package/plugins/gen-markdown/external.go +0 -71
  340. package/plugins/gen-markdown/flow.go +0 -277
  341. package/plugins/gen-markdown/glossary.go +0 -76
  342. package/plugins/gen-markdown/glossary_test.go +0 -148
  343. package/plugins/gen-markdown/llms.go +0 -302
  344. package/plugins/gen-markdown/main.go +0 -22
  345. package/plugins/gen-markdown/markdown.go +0 -254
  346. package/plugins/gen-markdown/markdown_test.go +0 -100
  347. package/plugins/gen-markdown/module.go +0 -107
  348. package/plugins/gen-markdown/plugin.go +0 -39
  349. package/plugins/gen-markdown/quality_test.go +0 -176
  350. package/plugins/gen-markdown/redis_test.go +0 -38
  351. package/plugins/gen-markdown/render.go +0 -344
  352. package/plugins/gen-markdown/render_test.go +0 -186
  353. package/plugins/gen-markdown/service.go +0 -456
  354. package/plugins/gen-markdown/source.go +0 -177
  355. package/plugins/gen-markdown/store.go +0 -201
  356. package/plugins/gen-markdown.wasm +0 -0
  357. package/plugins/gen-mermaid/describe.go +0 -17
  358. package/plugins/gen-mermaid/main.go +0 -22
  359. package/plugins/gen-mermaid/plugin.go +0 -104
  360. package/plugins/gen-mermaid/plugin_test.go +0 -33
  361. package/plugins/gen-mermaid.wasm +0 -0
  362. package/plugins/openapi/ids.go +0 -261
  363. package/plugins/openapi/ids_test.go +0 -98
  364. package/plugins/verify-codeowners/describe.go +0 -19
  365. package/plugins/verify-codeowners/describe_test.go +0 -11
  366. package/plugins/verify-codeowners/main.go +0 -75
  367. package/plugins/verify-codeowners/match.go +0 -85
  368. package/plugins/verify-codeowners/match_test.go +0 -47
  369. package/plugins/verify-codeowners/owners.go +0 -164
  370. package/plugins/verify-codeowners/owners_test.go +0 -225
  371. package/plugins/verify-codeowners/parse.go +0 -90
  372. package/plugins/verify-codeowners/parse_test.go +0 -62
  373. package/plugins/verify-otel/describe.go +0 -19
  374. package/plugins/verify-otel/describe_test.go +0 -11
  375. package/plugins/verify-otel/main.go +0 -53
  376. package/plugins/verify-otel/match.go +0 -336
  377. package/plugins/verify-otel/otlp.go +0 -200
  378. package/plugins/verify-otel/verify.go +0 -734
  379. package/plugins/verify-otel/verify_test.go +0 -460
  380. /package/{plugins/fetch-csr/options.schema.json → scripts/host-plugins/fetch-csr.options.json} +0 -0
  381. /package/{plugins/fetch-git/options.schema.json → scripts/host-plugins/fetch-git.options.json} +0 -0
@@ -1,465 +0,0 @@
1
- package main
2
-
3
- // The fetcher, against a recorded registry. NEVER against a live one: a test
4
- // that reaches buf.build would fail on a train, and the whole point of this
5
- // plugin's design is that the build does not depend on a registry being up.
6
-
7
- import (
8
- "encoding/base64"
9
- "encoding/json"
10
- "net/http"
11
- "net/http/httptest"
12
- "os"
13
- "path/filepath"
14
- "sort"
15
- "strings"
16
- "testing"
17
-
18
- "github.com/shortlink-org/portolan/plugin"
19
- )
20
-
21
- const (
22
- moduleName = "buf.build/acme/shop"
23
- pinned = "c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6"
24
- )
25
-
26
- var protoFile = "syntax = \"proto3\";\n\npackage shop.v1;\n\nservice Orders {\n rpc PlaceOrder(PlaceOrderRequest) returns (PlaceOrderResponse);\n}\n\nmessage PlaceOrderRequest { string customer_id = 1; }\nmessage PlaceOrderResponse { string order_id = 1; }\n"
27
-
28
- // registry serves the two calls this plugin makes, from recorded shapes.
29
- func registry(t *testing.T, seen *[]string) *httptest.Server {
30
- t.Helper()
31
-
32
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33
- if seen != nil {
34
- *seen = append(*seen, r.Header.Get("Authorization"))
35
- }
36
-
37
- w.Header().Set("Content-Type", "application/json")
38
-
39
- switch r.URL.Path {
40
- case downloadMethod:
41
- var req downloadRequest
42
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
43
- t.Errorf("the download request is not json: %v", err)
44
- }
45
- // The wire shape, asserted here rather than assumed: a request that
46
- // stopped naming the module would still "pass" every other test.
47
- if len(req.Values) != 1 || req.Values[0].ResourceRef.Name == nil {
48
- t.Errorf("download request: %+v", req)
49
- } else {
50
- name := req.Values[0].ResourceRef.Name
51
- if name.Owner != "acme" || name.Module != "shop" {
52
- t.Errorf("download names %s/%s", name.Owner, name.Module)
53
- }
54
- }
55
-
56
- _, _ = w.Write([]byte(`{"contents":[{"commit":{"id":"` + pinned +
57
- `","digest":{"type":"DIGEST_TYPE_B5","value":"` +
58
- base64.StdEncoding.EncodeToString([]byte{0xde, 0xad, 0xbe, 0xef}) +
59
- `"}},"files":[{"path":"shop/v1/orders.proto","content":"` +
60
- base64.StdEncoding.EncodeToString([]byte(protoFile)) + `"}]}]}`))
61
-
62
- case commitsMethod:
63
- _, _ = w.Write([]byte(`{"commits":[{"id":"` + pinned +
64
- `","digest":{"type":"DIGEST_TYPE_B5","value":"` +
65
- base64.StdEncoding.EncodeToString([]byte{0xde, 0xad, 0xbe, 0xef}) + `"}}]}`))
66
-
67
- default:
68
- w.WriteHeader(http.StatusNotFound)
69
- _, _ = w.Write([]byte(`{"code":"not_found","message":"no such method"}`))
70
- }
71
- }))
72
- t.Cleanup(server.Close)
73
-
74
- return server
75
- }
76
-
77
- func options(t *testing.T, base, cache string) Options {
78
- t.Helper()
79
-
80
- return Options{
81
- Registry: base,
82
- Cache: cache,
83
- Modules: []Module{{Module: moduleName, Commit: pinned}},
84
- }
85
- }
86
-
87
- // A test must never inherit a real credential or a real offline setting from
88
- // whoever is running it.
89
- func isolate(t *testing.T) {
90
- t.Helper()
91
- t.Setenv(TokenEnv, "")
92
- t.Setenv(OfflineEnv, "")
93
- t.Setenv("CI", "")
94
- t.Setenv("HOME", t.TempDir())
95
- }
96
-
97
- func names(resp plugin.Response) []string {
98
- out := make([]string, 0, len(resp.Files))
99
- for _, f := range resp.Files {
100
- out = append(out, f.Name)
101
- }
102
- sort.Strings(out)
103
-
104
- return out
105
- }
106
-
107
- func contentsOf(resp plugin.Response, name string) string {
108
- for _, f := range resp.Files {
109
- if f.Name == name {
110
- return f.Contents
111
- }
112
- }
113
-
114
- return ""
115
- }
116
-
117
- // write puts a response into a directory the way the host's apply() would, so
118
- // the next run can replay it.
119
- func write(t *testing.T, dir string, resp plugin.Response) {
120
- t.Helper()
121
-
122
- for _, f := range resp.Files {
123
- at := filepath.Join(dir, filepath.FromSlash(f.Name))
124
- if err := os.MkdirAll(filepath.Dir(at), 0o755); err != nil {
125
- t.Fatal(err)
126
- }
127
- if err := os.WriteFile(at, []byte(f.Contents), 0o644); err != nil {
128
- t.Fatal(err)
129
- }
130
- }
131
- }
132
-
133
- func TestFetchWritesProtosAndALock(t *testing.T) {
134
- isolate(t)
135
- server := registry(t, nil)
136
- cache := t.TempDir()
137
-
138
- resp, err := fetch(options(t, server.URL, cache))
139
- if err != nil {
140
- t.Fatal(err)
141
- }
142
-
143
- // Each module in its own directory with its own lock, so extract-proto
144
- // finds the lock beside the files it is already reading.
145
- want := []string{"acme/shop/bsr.lock.json", "acme/shop/shop/v1/orders.proto"}
146
- if got := names(resp); !equal(got, want) {
147
- t.Fatalf("files: %v, want %v", got, want)
148
- }
149
- if contentsOf(resp, "acme/shop/shop/v1/orders.proto") != protoFile {
150
- t.Error("the proto came back changed")
151
- }
152
-
153
- var lock Lock
154
- if err := json.Unmarshal([]byte(contentsOf(resp, "acme/shop/bsr.lock.json")), &lock); err != nil {
155
- t.Fatal(err)
156
- }
157
- if len(lock.Modules) != 1 || lock.Modules[0].Commit != pinned {
158
- t.Fatalf("lock: %+v", lock)
159
- }
160
- if lock.Modules[0].Digest != "b5:deadbeef" {
161
- t.Errorf("digest: %q, want the b5:hex form buf writes", lock.Modules[0].Digest)
162
- }
163
- if len(lock.Modules[0].Files) != 1 || lock.Modules[0].Files[0].SHA256 != digestOf([]byte(protoFile)) {
164
- t.Errorf("the lock does not record what was written: %+v", lock.Modules[0].Files)
165
- }
166
- }
167
-
168
- // THE CI-GREEN GUARANTEE, TESTED.
169
- //
170
- // An offline run over a committed copy must produce output byte-identical to
171
- // the online run that wrote it, or `gen:check` reports drift on every build
172
- // that cannot reach the registry - which is every build in CI.
173
- func TestOfflineReplayIsByteIdentical(t *testing.T) {
174
- isolate(t)
175
- server := registry(t, nil)
176
- cache := t.TempDir()
177
-
178
- online, err := fetch(options(t, server.URL, cache))
179
- if err != nil {
180
- t.Fatal(err)
181
- }
182
- write(t, cache, online)
183
-
184
- t.Setenv(OfflineEnv, "1")
185
- replayed, err := fetch(options(t, server.URL, cache))
186
- if err != nil {
187
- t.Fatal(err)
188
- }
189
-
190
- if !equal(names(online), names(replayed)) {
191
- t.Fatalf("offline emitted %v, online emitted %v", names(replayed), names(online))
192
- }
193
- for _, f := range online.Files {
194
- if contentsOf(replayed, f.Name) != f.Contents {
195
- t.Errorf("%s differs between an online and an offline run", f.Name)
196
- }
197
- }
198
- if len(replayed.Warnings()) != 1 || !strings.Contains(replayed.Warnings()[0].Message, "not fetched") {
199
- t.Errorf("an offline run said nothing about not fetching: %+v", replayed.Warnings())
200
- }
201
- }
202
-
203
- // CI is set on every runner, so a build there never opens a socket even if
204
- // nobody remembered to set PORTOLAN_OFFLINE.
205
- func TestCiImpliesOffline(t *testing.T) {
206
- isolate(t)
207
- cache := t.TempDir()
208
- server := registry(t, nil)
209
-
210
- online, err := fetch(options(t, server.URL, cache))
211
- if err != nil {
212
- t.Fatal(err)
213
- }
214
- write(t, cache, online)
215
-
216
- t.Setenv("CI", "true")
217
- if _, err := fetch(options(t, server.URL, cache)); err != nil {
218
- t.Fatalf("a CI run over a committed copy failed: %v", err)
219
- }
220
- }
221
-
222
- // A vendored copy someone edited by hand stops matching its digest. That is
223
- // exactly the drift docs/adr/org.0001.md wants visible, so it is reported by
224
- // path rather than quietly overwritten.
225
- func TestEditedCacheIsReportedByPath(t *testing.T) {
226
- isolate(t)
227
- server := registry(t, nil)
228
- cache := t.TempDir()
229
-
230
- online, err := fetch(options(t, server.URL, cache))
231
- if err != nil {
232
- t.Fatal(err)
233
- }
234
- write(t, cache, online)
235
-
236
- at := filepath.Join(cache, "acme", "shop", "shop", "v1", "orders.proto")
237
- if err := os.WriteFile(at, []byte("// edited by hand\n"), 0o644); err != nil {
238
- t.Fatal(err)
239
- }
240
-
241
- t.Setenv(OfflineEnv, "1")
242
- _, err = fetch(options(t, server.URL, cache))
243
- if err == nil {
244
- t.Fatal("an edited vendored copy was accepted")
245
- }
246
- if !strings.Contains(err.Error(), "shop/v1/orders.proto") {
247
- t.Errorf("the error does not name the edited file: %v", err)
248
- }
249
- }
250
-
251
- // RULE 3. The host deletes files a step stops naming, so a failed fetch with
252
- // nothing to fall back to must be a non-zero exit and NOT a short file list -
253
- // dropping a repository's vendored protos because a laptop went offline is
254
- // worse than a red build.
255
- func TestOfflineWithNoCacheFailsRatherThanEmittingNothing(t *testing.T) {
256
- isolate(t)
257
- t.Setenv(OfflineEnv, "1")
258
-
259
- resp, err := fetch(options(t, "http://127.0.0.1:1", t.TempDir()))
260
- if err == nil {
261
- t.Fatalf("an empty cache produced a response instead of an error: %v", names(resp))
262
- }
263
- if len(resp.Files) != 0 {
264
- t.Errorf("a failed run emitted files: %v", names(resp))
265
- }
266
- }
267
-
268
- // The same rule when the registry is reachable but broken.
269
- func TestFailedFetchWithNoCacheFails(t *testing.T) {
270
- isolate(t)
271
- broken := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
272
- w.WriteHeader(http.StatusInternalServerError)
273
- _, _ = w.Write([]byte(`{"code":"internal","message":"the registry is having a day"}`))
274
- }))
275
- t.Cleanup(broken.Close)
276
-
277
- _, err := fetch(options(t, broken.URL, t.TempDir()))
278
- if err == nil {
279
- t.Fatal("a failing registry with no cache produced a response")
280
- }
281
- if !strings.Contains(err.Error(), "the registry is having a day") {
282
- t.Errorf("the registry's own message did not survive: %v", err)
283
- }
284
- }
285
-
286
- // A failing registry with a good copy in the tree is not a failure at all:
287
- // the output is unchanged and the build stays green.
288
- func TestFailedFetchFallsBackToTheCommittedCopy(t *testing.T) {
289
- isolate(t)
290
- cache := t.TempDir()
291
-
292
- server := registry(t, nil)
293
- online, err := fetch(options(t, server.URL, cache))
294
- if err != nil {
295
- t.Fatal(err)
296
- }
297
- write(t, cache, online)
298
-
299
- broken := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
300
- w.WriteHeader(http.StatusBadGateway)
301
- }))
302
- t.Cleanup(broken.Close)
303
-
304
- fallback, err := fetch(options(t, broken.URL, cache))
305
- if err != nil {
306
- t.Fatalf("a good committed copy did not save the run: %v", err)
307
- }
308
- if !equal(names(fallback), names(online)) {
309
- t.Errorf("the fallback emitted %v", names(fallback))
310
- }
311
- }
312
-
313
- // Offline there is nothing to resolve a label against, so an unpinned module
314
- // is refused by name rather than guessed at.
315
- func TestUnpinnedModuleIsRefusedOffline(t *testing.T) {
316
- isolate(t)
317
- t.Setenv(OfflineEnv, "1")
318
-
319
- opts := options(t, "http://127.0.0.1:1", t.TempDir())
320
- opts.Modules[0].Commit = ""
321
-
322
- _, err := fetch(opts)
323
- if err == nil {
324
- t.Fatal("an unpinned module was accepted offline")
325
- }
326
- if !strings.Contains(err.Error(), moduleName) {
327
- t.Errorf("the error does not name the module: %v", err)
328
- }
329
- }
330
-
331
- // Online it still works, but it is said out loud: two runs a day apart would
332
- // otherwise produce two different trees from one manifest.
333
- func TestUnpinnedModuleWarnsOnline(t *testing.T) {
334
- isolate(t)
335
- server := registry(t, nil)
336
-
337
- opts := options(t, server.URL, t.TempDir())
338
- opts.Modules[0].Commit = ""
339
-
340
- resp, err := fetch(opts)
341
- if err != nil {
342
- t.Fatal(err)
343
- }
344
- if len(resp.Warnings()) == 0 || !strings.Contains(resp.Warnings()[0].Message, "not pinned") {
345
- t.Errorf("an unpinned module was resolved silently: %+v", resp.Warnings())
346
- }
347
- }
348
-
349
- // A pin the tree does not hold is a fetch waiting to happen, not a copy to
350
- // serve. Serving the old one would make `gen:check` pass on the wrong bytes.
351
- func TestCacheAtTheWrongCommitIsRefused(t *testing.T) {
352
- isolate(t)
353
- cache := t.TempDir()
354
-
355
- server := registry(t, nil)
356
- online, err := fetch(options(t, server.URL, cache))
357
- if err != nil {
358
- t.Fatal(err)
359
- }
360
- write(t, cache, online)
361
-
362
- t.Setenv(OfflineEnv, "1")
363
- opts := options(t, server.URL, cache)
364
- opts.Modules[0].Commit = "0000000000000000000000000000000f"
365
-
366
- if _, err := fetch(opts); err == nil {
367
- t.Fatal("a copy at another commit was served for a different pin")
368
- }
369
- }
370
-
371
- // A credential changes whether the fetch succeeds, never what it says. If a
372
- // token could reach the output, a public copy and a private one would differ.
373
- func TestOutputIsIdenticalWithAndWithoutAToken(t *testing.T) {
374
- isolate(t)
375
-
376
- var seen []string
377
- server := registry(t, &seen)
378
-
379
- anonymous, err := fetch(options(t, server.URL, t.TempDir()))
380
- if err != nil {
381
- t.Fatal(err)
382
- }
383
-
384
- t.Setenv(TokenEnv, "a-real-looking-secret")
385
- authorised, err := fetch(options(t, server.URL, t.TempDir()))
386
- if err != nil {
387
- t.Fatal(err)
388
- }
389
-
390
- for _, f := range anonymous.Files {
391
- if contentsOf(authorised, f.Name) != f.Contents {
392
- t.Errorf("%s differs depending on whether a token was set", f.Name)
393
- }
394
- if strings.Contains(contentsOf(authorised, f.Name), "a-real-looking-secret") {
395
- t.Fatalf("the token reached %s", f.Name)
396
- }
397
- }
398
-
399
- if len(seen) < 2 || seen[0] != "" {
400
- t.Errorf("the anonymous request sent an Authorization header: %q", seen[0])
401
- }
402
- if seen[len(seen)-1] != "Bearer a-real-looking-secret" {
403
- t.Errorf("the token was not sent: %q", seen[len(seen)-1])
404
- }
405
- }
406
-
407
- // The environment is where a credential comes from, and portolan.json - which
408
- // is committed - is where it must never be.
409
- func TestTokenPrecedence(t *testing.T) {
410
- isolate(t)
411
-
412
- home := t.TempDir()
413
- t.Setenv("HOME", home)
414
- netrc := "machine buf.build login someone password from-netrc\n"
415
- if err := os.WriteFile(filepath.Join(home, ".netrc"), []byte(netrc), 0o600); err != nil {
416
- t.Fatal(err)
417
- }
418
-
419
- if got := token("buf.build"); got != "from-netrc" {
420
- t.Errorf("with no env var, the netrc password should be used: %q", got)
421
- }
422
- if got := token("other.example"); got != "" {
423
- t.Errorf("a netrc entry for another host was used: %q", got)
424
- }
425
-
426
- t.Setenv(TokenEnv, "from-env")
427
- if got := token("buf.build"); got != "from-env" {
428
- t.Errorf("the environment should win over the netrc: %q", got)
429
- }
430
- }
431
-
432
- // A cache directory is not optional: without one an offline run has nowhere to
433
- // replay from, and the failure would surface as a mysteriously empty step.
434
- func TestCacheIsRequired(t *testing.T) {
435
- isolate(t)
436
-
437
- opts := options(t, "http://127.0.0.1:1", "")
438
- if _, err := fetch(opts); err == nil || !strings.Contains(err.Error(), "cache") {
439
- t.Errorf("a missing cache was not refused clearly: %v", err)
440
- }
441
- }
442
-
443
- func TestModuleNameMustHaveThreeParts(t *testing.T) {
444
- isolate(t)
445
-
446
- opts := options(t, "http://127.0.0.1:1", t.TempDir())
447
- opts.Modules[0].Module = "acme/shop"
448
-
449
- if _, err := fetch(opts); err == nil || !strings.Contains(err.Error(), "module name") {
450
- t.Errorf("a two-part module name was not refused: %v", err)
451
- }
452
- }
453
-
454
- func equal(a, b []string) bool {
455
- if len(a) != len(b) {
456
- return false
457
- }
458
- for i := range a {
459
- if a[i] != b[i] {
460
- return false
461
- }
462
- }
463
-
464
- return true
465
- }
@@ -1,71 +0,0 @@
1
- package main
2
-
3
- // What was fetched, so it can be checked without fetching again.
4
- //
5
- // One lock per module directory rather than one for the whole step, so a
6
- // vendored module is self-describing: the protos and the statement of where
7
- // they came from sit together, and extract-proto finds the lock by looking
8
- // beside the files it is already reading.
9
-
10
- import (
11
- "crypto/sha256"
12
- "encoding/hex"
13
- "encoding/json"
14
- "sort"
15
- )
16
-
17
- // LockName is the file written beside each module's protos.
18
- const LockName = "bsr.lock.json"
19
-
20
- type Lock struct {
21
- Modules []LockModule `json:"modules"`
22
- }
23
-
24
- type LockModule struct {
25
- Module string `json:"module"`
26
-
27
- // Commit is what was actually fetched. A BSR commit is immutable, so a
28
- // pinned download is byte-reproducible and this is what makes replaying
29
- // from disk equivalent to fetching again.
30
- Commit string `json:"commit"`
31
- Digest string `json:"digest,omitempty"`
32
-
33
- Deps []string `json:"deps,omitempty"`
34
- Files []LockEntry `json:"files"`
35
- }
36
-
37
- type LockEntry struct {
38
- Path string `json:"path"`
39
-
40
- // SHA256 is over the bytes as written. It is what turns a cache into
41
- // something checkable: a vendored copy someone edited by hand stops
42
- // matching, which is exactly the drift docs/adr/org.0001.md wants visible.
43
- SHA256 string `json:"sha256"`
44
- Size int `json:"size"`
45
- }
46
-
47
- func digestOf(content []byte) string {
48
- sum := sha256.Sum256(content)
49
-
50
- return hex.EncodeToString(sum[:])
51
- }
52
-
53
- // encode writes the lock the way every generated file in this repository is
54
- // written: sorted, indented, newline-terminated. It is committed and compared
55
- // by `gen:check`, so an unstable order would rewrite it on every run.
56
- func (l Lock) encode() (string, error) {
57
- for i := range l.Modules {
58
- sort.Slice(l.Modules[i].Files, func(a, b int) bool {
59
- return l.Modules[i].Files[a].Path < l.Modules[i].Files[b].Path
60
- })
61
- sort.Strings(l.Modules[i].Deps)
62
- }
63
- sort.Slice(l.Modules, func(a, b int) bool { return l.Modules[a].Module < l.Modules[b].Module })
64
-
65
- out, err := json.MarshalIndent(l, "", " ")
66
- if err != nil {
67
- return "", err
68
- }
69
-
70
- return string(out) + "\n", nil
71
- }
@@ -1,117 +0,0 @@
1
- // Package main is portolan-fetch-bsr: a schema registry on one side, .proto
2
- // files committed to this repository on the other.
3
- //
4
- // It is the ONE plugin here that opens a socket, and it is split off from
5
- // portolan-extract-proto for exactly that reason. Extraction stays a pure
6
- // function of the tree; fetching is the step that can fail, need a credential,
7
- // or come back with something different than it did yesterday. Keeping them
8
- // apart is what lets CI verify an estate whose schemas live in a registry it
9
- // never talks to.
10
- //
11
- // The fetched protos are the plugin's Response.Files, not a side effect. The
12
- // host writes them, so they get a manifest entry, are compared by `gen:check`
13
- // like any other generated file, and are removed when the step stops naming
14
- // them. Refreshing a pin then produces ONE pull request holding the pin bump,
15
- // the proto diff, the lock diff and the fragment diff - which is the review
16
- // worth having.
17
- //
18
- // Wired into portolan.json for the order contract oms publishes. The shape of
19
- // the two steps, fetch and then read:
20
- //
21
- // {
22
- // "plugins": [
23
- // { "name": "bsr", "process": { "command": "go", "args": ["run", "./plugins/fetch-bsr"] } },
24
- // { "name": "proto", "process": { "command": "go", "args": ["run", "./plugins/extract-proto"] } }
25
- // ],
26
- // "extract": [
27
- // {
28
- // "plugin": "bsr",
29
- // "in": "examples/shop",
30
- // "out": "examples/shop/vendor/proto",
31
- // "options": {
32
- // "cache": "examples/shop/vendor/proto",
33
- // "modules": [
34
- // { "module": "buf.build/acme/shop", "commit": "c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6" }
35
- // ]
36
- // }
37
- // },
38
- // {
39
- // "plugin": "proto",
40
- // "in": "examples/shop",
41
- // "out": "examples/shop/portolan",
42
- // "options": {
43
- // "context": "shop",
44
- // "service": "oms",
45
- // "paths": ["vendor/proto/acme/shop"],
46
- // "out": "proto.json"
47
- // }
48
- // }
49
- // ]
50
- // }
51
- //
52
- // The bsr step is declared FIRST: extract steps run in list order, so its
53
- // protos and locks are on disk by the time the proto step reads them.
54
- //
55
- // In CI, set PORTOLAN_OFFLINE=1. The step then replays the committed copies,
56
- // checks them against their locks and emits an identical file list, so a fork's
57
- // pull request needs no secret and the registry being down cannot turn the tree
58
- // red.
59
- package main
60
-
61
- import (
62
- "fmt"
63
- "io"
64
- "os"
65
-
66
- "github.com/shortlink-org/portolan/plugin"
67
- )
68
-
69
- // Options are what the manifest tells the fetcher.
70
- //
71
- // No token, ever. portolan.json is committed; the credential comes from the
72
- // environment, and only auth.go looks at it.
73
- type Options struct {
74
- Modules []Module `json:"modules"`
75
-
76
- // Registry overrides the host in each module name, for a self-hosted BSR.
77
- Registry string `json:"registry,omitempty"`
78
-
79
- // Cache is where the last run's files were written, repo-relative, and
80
- // should be the same path as the step's `out`.
81
- //
82
- // Stated twice because a plugin is never told where its output goes. That
83
- // is the whole shape of the protocol - a plugin returns files and the host
84
- // decides what to do with them - and not an oversight to work around.
85
- Cache string `json:"cache"`
86
- }
87
-
88
- type Module struct {
89
- // Module is "<registry>/<owner>/<name>", e.g. "buf.build/acme/shop".
90
- Module string `json:"module"`
91
-
92
- // Commit pins it. A BSR commit is immutable, so a pinned download is
93
- // byte-reproducible - which is the only reason replaying from disk is
94
- // equivalent to fetching again. Leaving it out is allowed and warned about;
95
- // offline it is refused, because there is nothing to replay against.
96
- Commit string `json:"commit,omitempty"`
97
-
98
- // Ref is the label to resolve when there is no commit. Defaults to "main".
99
- Ref string `json:"ref,omitempty"`
100
-
101
- // Paths narrows the download to the directories actually used, which is how
102
- // a vendored copy stays small.
103
- Paths []string `json:"paths,omitempty"`
104
- }
105
-
106
- func main() {
107
- if err := run(os.Stdin, os.Stdout); err != nil {
108
- fmt.Fprintln(os.Stderr, "portolan-fetch-bsr:", err)
109
- os.Exit(1)
110
- }
111
- }
112
-
113
- func run(stdin io.Reader, stdout io.Writer) error {
114
- return plugin.Serve(stdin, stdout, descriptor(), func(_ plugin.Request, opts Options) (plugin.Response, error) {
115
- return fetch(opts)
116
- })
117
- }