@myelinbridge/cli 0.12.2 → 0.14.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 (3) hide show
  1. package/README.md +352 -168
  2. package/bin/myelin.js +776 -61
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,202 +1,386 @@
1
1
  # @myelinbridge/cli
2
2
 
3
- Push R&D data deliveries into [Myelin](https://myelinbridge.com) from a pipeline —
4
- preflight against the client's published quality rules, resumable upload, submit,
5
- and track review outcomes. Full API reference: https://myelinbridge.com/developers.
3
+ Deliver R&D data to your client through [Myelin](https://myelinbridge.com), from a
4
+ terminal or a script: run the client's quality checks on a folder before uploading a
5
+ byte, upload with resume, hand the delivery over for review, follow the outcome.
6
6
 
7
- ## Quick start (~10 minutes from key to first submit)
7
+ Node 20 or later. Nothing to install: `npx @myelinbridge/cli <command>` fetches it on
8
+ each run. Or `npm i -g @myelinbridge/cli` once, and the binary is `myelin`.
9
+ Full API reference: https://myelinbridge.com/developers.
10
+
11
+ ## First delivery
12
+
13
+ Your client's bridge owner creates your key in **Bridge → API**. It starts with
14
+ `myl_live_` and is shown once.
8
15
 
9
16
  ```bash
10
- export MYELIN_API_KEY=myl_live_… # created by your bridge owner in Bridge → API
11
-
12
- npx @myelinbridge/cli ping # verifies auth, prints your projects
13
- npx @myelinbridge/cli datasets # what you can deliver to, and whose move it is
14
- npx @myelinbridge/cli contract --dataset onco1-wes # what is expected of your delivery
15
- npx @myelinbridge/cli sample-depth 1 --dataset onco1-wes # once, before your first submit
16
- npx @myelinbridge/cli roles --dataset onco1-wes # which file plays which role
17
- npx @myelinbridge/cli roles set samplesheet /metadata/samplesheet.csv --dataset onco1-wes
18
- npx @myelinbridge/cli check ./run_042 --dataset onco1-wes # validate BEFORE uploading a byte
19
- npx @myelinbridge/cli push ./run_042 --dataset onco1-wes --submit
17
+ export MYELIN_API_KEY=myl_live_… # Windows: setx MYELIN_API_KEY myl_live_… (then open a new terminal)
18
+
19
+ npx @myelinbridge/cli ping # 1. the key works, and what it can reach
20
+ npx @myelinbridge/cli datasets # 2. what you can deliver to
21
+ npx @myelinbridge/cli contract --dataset onco1-wes # 3. what the client expects of a delivery
22
+ npx @myelinbridge/cli check ./run_042 --dataset onco1-wes # 4. their checks on your folder — nothing is uploaded
23
+ npx @myelinbridge/cli push ./run_042 --dataset onco1-wes --submit # 5. upload, then hand over for review
24
+ npx @myelinbridge/cli status ONCO1-WES-001 --watch # 6. follow the review
20
25
  ```
21
26
 
22
- - **Declare your sample depth once, first.** The client creates and describes the
23
- dataset; you own your output structure, so you tell Myelin at which folder depth a
24
- sample sits — `0` = the delivery root is one sample, `1` (default) = each top-level
25
- folder is a sample, `2` = one level deeper. Sample-scoped quality checks group by it,
26
- so getting it right up front is what makes per-sample verdicts mean anything. It is
27
- **idempotent** (safe to assert on every pipeline run, before and after the lock) and
28
- **locks once your first batch leaves draft**, so that verdicts stay comparable across
29
- deliveries — after that, re-asserting the current value still succeeds and only a
30
- *change* exits `2`. This is the only dataset field you can write.
31
-
32
- - **Declare which file plays which role — whenever you like (0.11.0).** Quality
33
- checks read files by ROLE (`samplesheet`, `checksum_manifest`, `qc_report`,
34
- `subject_roster`, `capture_bed`), not by a filing convention you never agreed
35
- to. `roles` shows what is declared and what Myelin detected in your
36
- deliveries; `roles set <role> <path>` confirms a file; `roles none <role>`
37
- states your dataset has no such file (a statement, not a gap). Nothing is
38
- ever required to deliver — an undeclared role just means the checks that read
39
- it report *"could not check"* instead of running, so declaring is how you
40
- activate more checks before upload and catch problems before review does.
41
- Unlike `sample-depth` it **never locks**, and it is idempotent, so a pipeline
42
- can assert it on every run. The samplesheet is **sticky**: send it once and
43
- every later delivery reconciles against it — a partial delivery (93 of 96) is
44
- reported as a state, never as a failure.
45
-
46
- - **Your client's own fields show up, read-only (0.12.0).** Clients can define
47
- their own metadata fields on datasets (a sponsor study code, a work order, a
48
- therapeutic area). `contract` prints them under CLIENT FIELDS — including a
49
- required field nobody has filled in yet, shown as *"— required, not filled
50
- in yet"* (0.12.2) — and `check` tells you the same thing via preflight;
51
- informational only: their team fills them in Myelin, there is nothing to
52
- change in your delivery, and metadata never moves the exit code. In
53
- `--json`, datasets carry a `metadata` object
54
- (`{key: {label, type, value, required?}}` — `required: true` and
55
- `value: null` mark a required field with no value yet) and preflight
56
- carries `metadata.missing_required`.
57
-
58
- - **A missing path in `roles set` is refused, not guessed (0.11.1).** Flags are
59
- not positional arguments: `roles set samplesheet --dataset onco1-wes` (path
60
- omitted) used to declare the samplesheet at the literal path `--dataset` and
61
- print a tick. Every samplesheet check then reported *"could not check — the
62
- declared file is not in the delivery"* on a delivery that was fine. The
63
- command now exits with its usage line instead.
64
-
65
- - **`--dataset` takes the slug in any case (0.11.0).** Slugs are matched
66
- case-insensitively (`onco1-wes` and `ONCO1-WES` name the same dataset); an id
67
- or an exact name works too.
68
-
69
- - **Resume = re-run.** `push` is idempotent: already-uploaded files are skipped
70
- (path + size), and within a large file, parts that already landed are skipped
71
- too (S3 multipart). Uploads go direct to storage over short-lived presigned
72
- URLs — no credential is stored on your machine, and revoking the API key cuts
73
- off signing immediately.
74
- - **Transient failures retry themselves.** Since 0.8.0 a rate limit (`429`)
75
- waits out `Retry-After` and retries on every command, and a part upload that
76
- hits a storage hiccup (5xx, an edge timeout, an expired URL) re-signs, backs
77
- off, and retries up to 4 attempts before surfacing — an unattended pipeline
78
- run survives a blip. A retried `--submit` whose first attempt actually landed
79
- gets a success echo (`already_submitted: true`), not a false failure.
80
- - **Slow connections shrink their parts.** Since 0.9.0, when the same part dies
81
- twice on a timeout (the edge kills any PUT that runs too long — HTTP 524),
82
- `push` restarts the file with smaller parts, 64 → 16 → 5 MB, instead of
83
- retrying the same slice into the same wall. A hospital-grade uplink down to
84
- roughly 30 KB/s can now complete a delivery; it is slower, but it finishes.
85
- - **Every file is fingerprinted.** Since 0.7.0, `push` computes an MD5 of each
86
- file while it uploads and records it with the delivery. If the client's
87
- quality contract includes a checksum-manifest check, your delivery verifies
88
- against your own `md5_manifest.csv` instead of reading as "unverifiable".
89
- - **Read the contract before you build the delivery.** `contract` prints what the
90
- client expects — every check as one plain sentence, grouped by what it answers
91
- (completeness, structure, validity, consistency, integrity, privacy), and marked
92
- `!` when a failure blocks validation or `?` when the client's reviewer confirms
93
- it rather than asking you to fix it. It also tells you which checks `check` can
94
- verify locally and which only run once the files are uploaded, so nothing about
95
- the bar is a surprise at review time.
96
-
97
- - **`check` costs nothing.** It evaluates your local file list against the dataset's
98
- quality checks server-side — same engine, same verdicts as submit — without
99
- uploading. Exit code 2 means the delivery would not be validatable as it stands:
100
- a blocking rule fails, **or** a blocking rule could not be evaluated at all.
101
- - **A check has five answers, not three** (since 0.10.0). Alongside *passed*,
102
- *flagged* and *failed*, the engine can now say it **could not check** something
103
- (a missing precondition, or a rule it cannot evaluate) and that a rule **does
104
- not apply** to your delivery. This matters to you in one specific way: a rule
105
- the client wrote badly used to come back as a *failure on your delivery*. It
106
- now says the rule could not be evaluated, and tells you it is not yours to fix.
107
- `check` prints the count it could not check and what each one needs — supply it
108
- and those checks run on your next delivery, which is fewer rejections later.
109
- - **Some checks are confirmed, not fixed.** A rule marked *needs confirmation*
110
- asks nothing of you: the client's reviewer records what they decided and the
111
- delivery proceeds. `check` reports these and never exits 2 on them.
112
- - **The fix loop is machine-readable.** On `changes_requested`,
113
- `myelin status <batch> --json` returns the failed files, reviewer comments, and
114
- rule remediation hints; fix, re-`push --submit`, unchanged files keep their
115
- review votes.
116
- - **Limits.** `push` declares 500 files per call and chunks automatically — nothing
117
- for you to split. `check` sends your whole file list in one preflight
118
- call, which accepts at most 10 000 files — a larger delivery cannot be preflighted
119
- by `check` today (the API answers `400 too_many_files`); its checks still run at
120
- submit. A delivery holds at most 25 000 files (submit refuses above that with
121
- `422 submit_blocked`), and a checksum manifest is inspected up to 16 MiB.
27
+ That is the whole path. Four things worth knowing about it:
122
28
 
123
- ## If you are the client, not the partner
29
+ - **`check` uploads nothing.** It sends your file list, and the same engine that runs
30
+ at submit answers. Exit code `2` means the delivery could not be validated as it
31
+ stands: a blocking check failed, or a blocking check could not be evaluated at all.
32
+ Exit `0` here means the checks that can run before upload pass; checks that read
33
+ file contents run at submit, and `contract` tells you which ones. Above 10 000 files
34
+ `check` cannot run (`too_many_files`, exit `1`): skip it, the same checks run at submit.
35
+ - **`push` sends every file under the folder** (dotfiles included, symlinks followed; a
36
+ broken link or an unreadable file stops the command and is named), and it is safe to
37
+ re-run: files already uploaded are skipped, and inside a large file, parts that already
38
+ landed are skipped too. A crashed run resumes with the same command. Stage a clean
39
+ folder if your pipeline leaves work files behind. `push` does not run `check` first.
40
+ Without `--submit` the draft stays open, one per dataset, and you submit it later, from
41
+ the CLI or from the portal; a later `push` of a different folder joins that same draft.
42
+ At the end it prints the delivery's name (`ONCO1-WES-001`), which `status` and `recall`
43
+ take.
44
+ - **One delivery at a time per dataset.** While a delivery is submitted or in review,
45
+ `push` to that dataset exits `2` with `delivery_locked` until the reviewer validates it
46
+ or sends it back. `recall` takes it back while it is still `submitted`. If you deliver
47
+ more often than your client reviews, agree a turnaround with them, or ask for one
48
+ dataset per run.
49
+ - **After `--submit`, a person decides.** A reviewer on your client's side opens the
50
+ delivery in their portal and either validates it, after which Myelin copies the files
51
+ into their bucket, or asks for changes; the turnaround is theirs, and Myelin notifies
52
+ them. `status` tells you what to fix: on `changes_requested` it prints the files the
53
+ reviewer flagged, their comments, and a hint per failed rule. Fix, `push --submit`
54
+ again: files you did not touch keep their review votes.
55
+ - **`datasets` empty, or `dataset_not_active`?** The client has not finished setup on
56
+ their side. Three things must exist first, all created by them: an active bridge, a
57
+ project your key is scoped to, and an activated dataset inside it. Ask your client
58
+ contact, naming the missing piece.
59
+
60
+ ### What you will see
61
+
62
+ An example session, human mode. Your names, rules and counts will differ; the shape will not.
63
+
64
+ ```
65
+ $ myelin ping
66
+ ✓ key "pipeline" (myl_live_Yxwfeh…) · bridge Acme ↔ Genos CRO · 1 project(s)
67
+ Next: myelin datasets
68
+
69
+ $ myelin datasets
70
+ PROJECT DATASET STATUS YOUR MOVE?
71
+ ONCO1 onco1-wes active —
72
+ Next: myelin contract --dataset onco1-wes
73
+
74
+ $ myelin contract --dataset onco1-wes
75
+ WES cohort — delivery contract v3
76
+ 6 checks · 3 block validation · 5 checkable before upload · 1 reviewed by a person
77
+
78
+ COMPLETENESS — is everything here?
79
+ ! a samplesheet is present at the delivery root
80
+ ! every sample in the samplesheet has R1 and R2 reads
81
+ STRUCTURE — is it laid out as agreed?
82
+ · file names follow {sample}_{lane}_{R1|R2}.fastq.gz
83
+ INTEGRITY — are the bytes what you say they are?
84
+ ! every file matches your checksum manifest (checked at submission)
85
+ REVIEWER JUDGEMENT — decided by a person, not the engine
86
+ · the consent scope covers this cohort
87
+
88
+ Run "myelin check <dir> --dataset onco1-wes" to test 5 of these locally.
89
+
90
+ $ myelin check ./run_042 --dataset onco1-wes
91
+ Evaluating 129 files (26.1 GB) against quality checks v3…
92
+ ✓ samplesheet present passed
93
+ ✗ paired reads complete failed — BLOCKING
94
+ hint: S07 has R1 but no R2 — add S07_L001_R2.fastq.gz or drop S07 from the samplesheet
95
+ ✓ file naming passed
96
+ … checksum manifest matches reads file contents; runs at submit
97
+ ○ consent scope a reviewer decides this
98
+ 1 blocking issue(s). Fix before pushing to avoid a review round-trip.
99
+ $ echo $?
100
+ 2
101
+
102
+ $ myelin push ./run_042 --dataset onco1-wes --submit # after the fix
103
+ Draft ONCO1-WES-004 created.
104
+ Uploading 130/130 files (26.1 GB) · resumable
105
+ ✓ /samplesheet.csv (1/130)
106
+ ✓ /S01/S01_L001_R1.fastq.gz (2/130)
107
+
108
+ All files confirmed.
109
+ Submitting… auto-checks: 5 passed, 0 flagged, 0 failed of 6.
110
+ ✓ Batch submitted for review. Track: myelin status ONCO1-WES-004 --watch
111
+
112
+ $ myelin status ONCO1-WES-004 --watch
113
+ ONCO1-WES-004: submitted — reviewer's move.
114
+ ONCO1-WES-004: in_review — reviewer's move.
115
+ ONCO1-WES-004: transferred — delivered to your client’s bucket.
116
+ ```
117
+
118
+ ### Two optional declarations
119
+
120
+ - **`sample-depth`** is the folder depth a sample sits at: `1` means each top-level
121
+ folder is one sample and is the default, `0` means the whole delivery is one sample,
122
+ `2` one level deeper. Set it only if the default is wrong for your layout. It locks
123
+ once your first delivery leaves draft, so per-sample verdicts stay comparable; if you
124
+ are not sure which depth your client expects, ask before the first submit.
125
+ - **`roles`** says which file plays which role: `samplesheet`, `checksum_manifest`,
126
+ `qc_report`, `subject_roster`, `capture_bed`. Nothing is required to deliver; a role
127
+ you declare turns on the checks that read that file, so more of the contract is
128
+ verified before review. `roles none <role>` records that your dataset has no such
129
+ file. It never locks. A checksum manifest is read in the shapes tools write: plain
130
+ `md5sum` output, or a delimited file with a filename column and an md5 column, paths
131
+ relative to the delivery root.
132
+
133
+ ## Commands
124
134
 
125
- Two kinds of key exist, and they are not interchangeable. Everything above needs a
126
- **partner key** (write: upload, submit). A **client key** is read-only and answers the
127
- question your own systems ask once the data has landed: *the bucket is full of UUIDs —
128
- what is this?*
135
+ `<ds>` is a dataset slug (any case), an id, or an exact name, as `datasets` prints them.
129
136
 
130
- Where they live follows from what they are. A **partner key belongs to one bridge**, because a
131
- partner does. A **client key belongs to your organisation**: one key, every partner, one answer.
132
- Your organisation admin creates it in **Organisation API keys**. To narrow one to a single
133
- partner's data, scope it to that partner's projects.
137
+ | Command | Key | What it does |
138
+ |---|---|---|
139
+ | `ping` | partner | Checks the key and prints the bridge and the projects it can reach. |
140
+ | `datasets` | partner | Lists the datasets you can deliver to, and whose move it is. |
141
+ | `contract --dataset <ds>` | partner | Every check the dataset expects, one sentence each: `!` blocks validation, `~` is confirmed by the client's reviewer rather than fixed by you. Says which checks `check` can run locally. |
142
+ | `check <dir> --dataset <ds>` | partner | Runs the dataset's checks on your local file list, server-side, without uploading. Exit `2` when a blocking check fails or could not be evaluated. |
143
+ | `push <dir> --dataset <ds> [--submit] [--replace]` | partner | Creates or resumes the dataset's draft and uploads what has not landed yet. `--submit` hands it to review. `--replace` overwrites a path already declared with a different size or checksum, which otherwise exits `2`. |
144
+ | `status <batch> [--watch]` | partner | Where the delivery stands and whose move it is; on `changes_requested`, what to fix. Takes an id or a display name. `--watch` polls every 20 s and returns at the first final state: exit `0` at `transferred`, `2` at `changes_requested`, `1` at `rejected` or `transfer_failed`. A review can take days: for an unattended job, prefer a webhook. |
145
+ | `recall <batch>` | partner | Takes a submitted delivery back before the review starts; it becomes a draft again. Once a reviewer has opened it, `recall_failed`. |
146
+ | `projects` | partner | The projects this key is scoped to, with their status. |
147
+ | `sample-depth <0-5> --dataset <ds>` | partner | Declares the folder depth a sample sits at. Idempotent; locks once a delivery leaves draft. |
148
+ | `roles [set <role> <path> / none <role> / clear <role>] --dataset <ds>` | partner | Shows, declares or clears which file plays which role. Idempotent, never locks. |
149
+ | `sandbox <file>` | partner | Uploads one file under 4 MB as the partner-side test transfer your client's bridge-activation checklist asks for. Not a test environment, and not a step before `push`. |
150
+ | `deliveries [--project <id>] [--dataset <id>] [--since <iso>] [--limit <n>] [--cursor <ts>]` | client | What landed in your bucket, newest first. Page with `--cursor` from the printed `next_cursor`. |
151
+ | `resolve <path or prefix or id>` | client | A `gs://` or `s3://` path, a bare prefix or an id, back to the project, dataset, delivery and file it belongs to. |
152
+ | `version` | | Prints the CLI version. |
153
+ | `help [<command>]` | | This list, or one command in detail. Works without a key. |
154
+
155
+ ## From a script
156
+
157
+ Every command takes `--json`: one JSON object on stdout, no prose. Exit codes:
158
+ `0` ok · `1` error · `2` blocked, meaning a blocking check failed or could not be
159
+ evaluated, a declared path conflicts, or the API refused on the delivery's state or the
160
+ client's setup (`delivery_locked`, `submit_blocked`, `dataset_not_active`,
161
+ `api_disabled`, `bridge_paused`, `sample_depth_locked`).
134
162
 
135
163
  ```bash
136
- export MYELIN_API_KEY=myl_live_…
164
+ myelin check "$OUT" --dataset onco1-wes || exit $?
165
+ myelin push "$OUT" --dataset onco1-wes --submit
166
+ ```
167
+
168
+ Things a script should know:
169
+
170
+ - **An unknown option is an error** (`push --sumbit` exits `1` before a byte moves).
171
+ - **Pin the version you tested** in a pipeline: `npx @myelinbridge/cli@<version>`. Bare
172
+ `npx @myelinbridge/cli` runs whatever is latest today. `--json` shapes and exit codes
173
+ follow the API's rule: fields may appear, existing ones keep their meaning.
174
+ - **Rotating a key revokes the old one at once.** There is no overlap window, so rotate
175
+ between runs, not during one; a run that starts with a revoked key fails `401` and
176
+ resumes with the new key. A key may also carry an expiry the client chose when creating
177
+ it (`GET /v1/me` → `expires_at`).
178
+ - **No client-side timeout on API calls, by design**: a preflight of 10 000 files takes
179
+ minutes. Bound the command with your runner's timeout if you need one. A connection
180
+ refused, a DNS failure or a socket that never answers is reported at once (exit `1`) and
181
+ not retried; a `429` and a failed part upload are (see below).
182
+ - **`status --watch` exits `0` at `transferred`, `2` at `changes_requested`, `1` at
183
+ `rejected` or `transfer_failed`.** A network blip mid-watch is retried five times; if the
184
+ runner dies, re-run it: the state is server-side and nothing is lost.
185
+ - **`--json` shapes.** `ping`, `contract`, `check`, `deliveries` and `resolve` print the API
186
+ response as is. `datasets` prints `{ datasets: [{ project, dataset, status, quality_version,
187
+ open_draft, id }] }`. `push` prints `{ batch_id, files, submitted: false }` for a draft and
188
+ the submit response (`{ batch_id, status, already_submitted, auto_checks }`) with
189
+ `--submit`. `status` prints `{ batch }`, plus `findings` on `changes_requested`. The delivery
190
+ name that `status` takes is `batch.display_name`; the id works too.
191
+ - **Paths** are relative to the delivery root, `/`-separated; a leading `/` is optional.
192
+
193
+ `check --json` carries the numbers to branch on:
194
+
195
+ | Field | |
196
+ |---|---|
197
+ | `blocking_failures` | Blocking checks that were evaluated and failed. |
198
+ | `blocking_not_evaluated` | Blocking checks the engine could not evaluate. These stop the reviewer validating just as surely, so `check` exits `2` on them too. |
199
+ | `must_acknowledge_failures` | Checks the client asked to be told about. Reported, never exited on. |
200
+ | `counts` | `passed`, `flagged`, `failed`, `not_evaluated`, `not_applicable`, plus `deferred` and `manual`. |
201
+ | `metadata.missing_required` | The client's own required fields still empty on the dataset. Informational: their team fills them in Myelin, nothing to change in your delivery, and it never moves the exit code. |
202
+
203
+ A check that did not conclude says why in `details.abstained`: `rule` means the
204
+ client's own rule could not be evaluated and there is nothing on your side to fix;
205
+ `data` means something the check needs is missing from your delivery; `nothing_to_evaluate`
206
+ means the rule matched none of your files.
207
+
208
+ An API error in `--json` mode is `{ "error", "code", "status", "request_id" }`. Branch on
209
+ `code`; quote `request_id` when you report a problem, it names the exact server-side log
210
+ line. The id is printed in human mode too.
211
+
212
+ ## What push does when something goes wrong
213
+
214
+ - **A crash or a lost connection.** Re-run the same command. A file already uploaded at
215
+ the same path and size is skipped; within a large file, parts that already landed are
216
+ skipped.
217
+ - **A rate limit.** A `429` waits out `Retry-After` and retries, on every command.
218
+ - **A part that fails.** On a storage error, an edge timeout or an expired upload URL,
219
+ the part is re-signed and retried with backoff, four attempts, before the error surfaces.
220
+ - **A slow uplink.** When the same part dies twice on a timeout, the file restarts with
221
+ smaller parts, 64 → 16 → 5 MiB. A link down to roughly 30 KB/s completes; slowly, but
222
+ it finishes.
223
+ - **A retried `--submit`.** If the first attempt actually landed, the retry is answered
224
+ with `already_submitted: true`, not a failure.
225
+ - **Checksums.** An MD5 of each file is computed while it uploads and recorded with the
226
+ delivery, so a checksum-manifest check verifies against your own manifest.
227
+
228
+ Uploads go straight to storage over short-lived presigned URLs. No credential for the
229
+ client's storage is ever on your machine, and revoking the key cuts off signing at once.
230
+
231
+ ## Limits
232
+
233
+ | | |
234
+ |---|---|
235
+ | Files per delivery | 25 000. Submit refuses above that. |
236
+ | Files per `check` | 10 000 in one call. A larger delivery cannot be preflighted by `check`; its checks still run at submit. |
237
+ | Files per declare call | 500. `push` chunks automatically. |
238
+ | Per-file size | 20 GiB by default, set per deployment. |
239
+ | Checksum manifest | Inspected up to 16 MiB. |
240
+ | API rate | 120 requests per minute per key. Part uploads go direct to storage and do not count. |
241
+ | Throughput | On the order of 100 MB/s aggregate on a clean link (four parts in parallel); a 100 GB run is around twenty minutes. Your uplink is the limit. |
242
+
243
+ ## Where your files are, and for how long
137
244
 
138
- # What has landed, newest first (page with --cursor from the printed next_cursor)
139
- myelin deliveries --dataset <dataset-id>
245
+ Uploads land in Myelin's holding area, Supabase Storage in the EU (Ireland), never in your
246
+ client's bucket. There they can be read by you, by your client's reviewers on that project,
247
+ and by the transfer worker's service identity; Myelin's own operator role has no
248
+ customer-data access. Once the reviewer validates, the files are copied into the client's
249
+ bucket and the copy is verified. The holding copy is purged after that verified delivery, or
250
+ after a rejection, plus a grace window of seven days by default; each purge writes an audit
251
+ row. An empty draft with no activity for 24 hours is purged; a draft holding uploaded files
252
+ is kept, and its owner reminded. The full position is at https://myelinbridge.com/security.
140
253
 
141
- # What is this object, exactly?
254
+ There is no self-serve test environment. To rehearse without real data, ask your client for
255
+ a dataset created for that purpose, or ask Myelin for a sandbox. Questions about the setup on
256
+ the client's side go to your client contact; questions about the API go to the address on
257
+ https://myelinbridge.com/developers.
258
+
259
+ ## Environment
260
+
261
+ | Variable | |
262
+ |---|---|
263
+ | `MYELIN_API_KEY` | Required. Created by your bridge owner in **Bridge → API**. |
264
+ | `MYELIN_API_URL` | Optional. Defaults to `https://myelinbridge.com/api/v1`. |
265
+ | `MYELIN_API_HEADER` | Optional. Extra headers sent with every API call, one `Name: value` per line, for a Myelin deployment fronted by something that authenticates before Myelin does. It can never override `Authorization`. |
266
+ | `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` | Optional. Honoured on every call; lowercase spellings read too. See below. |
267
+
268
+ ## If you are the client, not the partner
269
+
270
+ Everything above needs a **partner key**. A **client key** is read-only and answers the
271
+ question your own systems ask once the data has landed: the bucket is full of ids, what
272
+ is this? Your organisation admin creates it in **Organisation → API keys**; one key covers
273
+ every partner. To narrow one to a single partner's data, scope it to that partner's projects.
274
+
275
+ ```bash
276
+ export MYELIN_API_KEY=myl_live_…
277
+
278
+ myelin deliveries --dataset <dataset-id> # what has landed, newest first
142
279
  myelin resolve gs://acme-landing/inbox/acme-cro/4319…/de99…/7aa5…/data/SAMPLE_01/reads.fastq.gz
143
280
  # file · gs://…/reads.fastq.gz
144
281
  # project ONCO1 — Oncology discovery
145
- # dataset WES batch 7 (Genomics)
282
+ # dataset WES cohort (Genomics)
146
283
  # batch ONCO1-WES-007 · #7
147
284
  # validated 2026-08-07T18:05:12Z · delivered 2026-08-07T18:06:20Z
148
285
  # manifest gs://…/7aa5…/_myelin/manifest.json
149
- # file reads.fastq.gz · 4096 bytes
150
286
  ```
151
287
 
152
- `resolve` accepts a full `gs://`/`s3://` URI, a bare prefix, or a single id, and answers at
153
- whatever granularity the path supports.
154
-
155
288
  Each delivery also carries the same record **as a file**, written next to the data at
156
- `_myelin/manifest.json` (plus `_myelin/files.csv`, a flat table you can load straight into a
157
- warehouse). Prefer the file for anything auditable: it is frozen at delivery time, needs no
158
- credentials, and does not depend on Myelin being reachable. Use the API when you want it live.
289
+ `_myelin/manifest.json`, with `_myelin/files.csv` beside it as a flat table. Prefer the
290
+ file for anything auditable: it is frozen at delivery time, needs no credentials, and does
291
+ not depend on Myelin being reachable. A partner key calling these gets `403 wrong_key_side`,
292
+ and vice versa.
159
293
 
160
- A partner key calling these gets `403 wrong_key_side`, and vice versa.
294
+ ## For your IT department
161
295
 
162
- ## Machine mode
296
+ Everything below is derived from what the code actually connects to. If you need to hand
297
+ one page to a network team, hand them this one.
163
298
 
164
- Every command takes `--json`. Exit codes: `0` ok · `1` error · `2` blocked
165
- (blocking preflight failure, **a blocking rule the engine could not evaluate**,
166
- locked delivery, blocked submit, locked sample depth).
299
+ ### What the CLI needs to reach
167
300
 
168
- `check --json` carries the numbers to branch on rather than the prose:
301
+ Outbound **TCP 443 only**, to two hosts:
169
302
 
170
- | Field | |
303
+ | Host | Why |
171
304
  |---|---|
172
- | `blocking_failures` | Blocking rules that were evaluated and failed. Unchanged meaning since the first release. |
173
- | `blocking_not_evaluated` | Blocking rules the engine could not evaluate. **New in 0.10.0** these stop the reviewer validating just as surely, which is why `check` now exits `2` on them too. |
174
- | `must_acknowledge_failures` | Rules the client asked to be told about. Reported, never exited on. |
175
- | `counts` | `passed`, `flagged`, `failed`, `not_evaluated`, `not_applicable` (which sum to `evaluated`), plus `deferred`, `manual` and `checks` (`evaluated + deferred + manual === checks`). |
176
-
177
- Every per-check result carries `details.abstained` when the engine did not
178
- conclude: `rule` means **the client's rule** could not be evaluated and there is
179
- nothing on your side to fix; `data` means something the check needs is missing
180
- from the delivery; `nothing_to_evaluate` means the rule matched none of your
181
- files. Branch on that before you page anyone.
182
-
183
- API errors carry structure, not just prose: in `--json` mode an error is
184
- `{ "error", "code", "status", "request_id" }` — branch on `code`, and quote the
185
- `request_id` when reporting a problem (it is printed in human mode too; it lets
186
- Myelin find the exact server-side log line). `myelin version` prints the CLI
187
- version.
305
+ | `myelinbridge.com` | the API auth, the quality contract, preflight, part signing, submit, status |
306
+ | `cdjlhyyrbogzkoaujzmj.storage.supabase.co` | the bytes uploads go **straight to storage** over short-lived presigned URLs, never through the API host |
188
307
 
189
- ## Environment
308
+ The second one surprises people, and it is the single commonest way a delivery
309
+ half-works: `myelin ping` and `myelin check` succeed, `myelin push` stalls on part 1.
310
+ That failure names the host it could not reach, so the allowlist gap is legible from
311
+ the error.
190
312
 
191
- | Variable | |
192
- |---|---|
193
- | `MYELIN_API_KEY` | Required. Created by your bridge owner in **Bridge API**. |
194
- | `MYELIN_API_URL` | Optional. Defaults to `https://myelinbridge.com/api/v1`. |
195
- | `MYELIN_API_HEADER` | Optional. Extra headers sent with every API call, one `Name: value` per line — for a Myelin deployment fronted by something that authenticates before Myelin does (a corporate gateway, an SSO-protected preview). It can never override `Authorization`. |
313
+ If your client runs Myelin at their own address, `MYELIN_API_URL` names the first host
314
+ and the second is that deployment's storage host. You never have to guess it: every
315
+ Myelin deployment publishes the authoritative list in its own `content-security-policy`
316
+ header:
317
+
318
+ ```bash
319
+ curl -sI https://myelinbridge.com | tr ';' '\n' | grep connect-src
320
+ # connect-src 'self' https://<ref>.supabase.co wss://<ref>.supabase.co https://<ref>.storage.supabase.co
321
+ ```
322
+
323
+ A wildcard rule works if your proxy prefers one: `*.supabase.co`.
324
+
325
+ ### What a person using the web portal needs
326
+
327
+ Someone reviewing a delivery in a browser needs the two hosts above **plus** the
328
+ Supabase API host, because sign-in and the session live there:
329
+
330
+ | Host | Port | Why |
331
+ |---|---|---|
332
+ | `myelinbridge.com` | 443 | the application itself |
333
+ | `cdjlhyyrbogzkoaujzmj.supabase.co` | 443 | sign-in and session |
334
+ | `cdjlhyyrbogzkoaujzmj.storage.supabase.co` | 443 | browser uploads, the same presigned PUTs the CLI makes |
335
+
336
+ Plain HTTPS is enough. The policy also permits `wss:` to the Supabase host, but the
337
+ portal opens no WebSocket today; if your proxy distinguishes the two, you do not need to
338
+ allow WebSocket upgrades for the portal to work.
339
+
340
+ Nothing else is contacted: scripts and fonts are served by the application itself and no
341
+ analytics or tracker host is involved. That is not a promise in a README; it is the
342
+ `content-security-policy` header above, which the browser enforces on every response.
343
+
344
+ ### What you do *not* need to open
345
+
346
+ - **No inbound anything.** Nothing connects to your network. The one exception is opt-in
347
+ and yours to choose: if you register a webhook endpoint, Myelin makes an outbound HTTPS
348
+ call to an address you publish.
349
+ - **No access to the destination bucket.** The client's cloud bucket is written by
350
+ Myelin, server-side, after review. A partner never touches it and never needs a
351
+ credential for it.
352
+ - **No FTP, SSH, rsync or fixed source IPs.** One protocol, one port.
353
+
354
+ ### Behind a proxy
355
+
356
+ Set the standard variables; the CLI honours them on every call, the API and the part
357
+ uploads alike:
358
+
359
+ ```bash
360
+ export HTTPS_PROXY=http://proxy.corp.example:8080
361
+ export NO_PROXY=.corp.example,10.0.0.0/8
362
+ myelin ping # prints "via proxy http://proxy.corp.example:8080" when one is in use
363
+ ```
364
+
365
+ - `HTTPS_PROXY` for https targets, `HTTP_PROXY` for http, `NO_PROXY` to exclude.
366
+ Lowercase spellings (`https_proxy`, …) are read too; both are in the wild.
367
+ - Credentials go in the URL: `http://user:pass@proxy:8080` (Basic). They are never
368
+ printed, logged, or echoed in an error.
369
+ - `NO_PROXY` accepts a host, a suffix (`.corp.example`, `*.corp.example`), `host:port`,
370
+ an IPv4 CIDR (`10.0.0.0/8`), or `*` for everything.
371
+ - **HTTP CONNECT proxies only.** A `socks5://` value is refused with a message rather
372
+ than silently bypassed: a CLI that quietly dials direct while you believe it is proxied
373
+ is worse than one that stops.
374
+ - **Basic proxy authentication only.** An NTLM or Kerberos proxy is not supported; put a
375
+ local CONNECT proxy that speaks it (cntlm, px) in front and point `HTTPS_PROXY` at that.
376
+ - A typo in a proxy variable fails immediately, before any command runs.
377
+ - **TLS terminates at Myelin, not at your proxy.** Through a CONNECT tunnel the proxy sees
378
+ the hostname and nothing else. If your proxy performs TLS interception, point Node at
379
+ its CA: `export NODE_EXTRA_CA_CERTS=/etc/ssl/corp-root.pem`.
196
380
 
197
381
  ## Webhooks instead of polling
198
382
 
199
- Register an HTTPS endpoint (portal Bridge → API, or `POST /v1/webhook-endpoints`)
200
- to receive signed events (`batch.validated`, `batch.changes_requested`,
201
- `batch.transferred`, …). Verification snippets: https://myelinbridge.com/developers.
202
- Polling fallback: `GET /v1/events?cursor=…`.
383
+ Register an HTTPS endpoint (portal Bridge → API, or `POST /v1/webhook-endpoints`) to
384
+ receive signed events (`batch.validated`, `batch.changes_requested`, `batch.transferred`,
385
+ …). Verification snippets: https://myelinbridge.com/developers. Polling fallback:
386
+ `GET /v1/events?cursor=…`.