@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.
- package/README.md +352 -168
- package/bin/myelin.js +776 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,202 +1,386 @@
|
|
|
1
1
|
# @myelinbridge/cli
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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_…
|
|
11
|
-
|
|
12
|
-
npx @myelinbridge/cli ping
|
|
13
|
-
npx @myelinbridge/cli datasets
|
|
14
|
-
npx @myelinbridge/cli contract --dataset onco1-wes
|
|
15
|
-
npx @myelinbridge/cli
|
|
16
|
-
npx @myelinbridge/cli
|
|
17
|
-
npx @myelinbridge/cli
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
partner
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
294
|
+
## For your IT department
|
|
161
295
|
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
+
Outbound **TCP 443 only**, to two hosts:
|
|
169
302
|
|
|
170
|
-
|
|
|
303
|
+
| Host | Why |
|
|
171
304
|
|---|---|
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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=…`.
|