blob: 5c19cb1289d7df8059ec04dd8f932340eadad39f [file] [log] [blame] [view]
Hosted Pub Repository Specification Version 2
=============================================
This document specifies the REST API that a hosted pub _package repository_ must
implement.
A package repository is a server from which packages can be downloaded,
the default package repository is `'https://pub.dev'`.
It used to be [pub.dartlang.org](https://pub.dartlang.org).
## Hosted URL
A custom package repository is identified by a _hosted-url_, like
`https://pub.dev` or `https://some-server.com/prefix/pub/`.
The _hosted-url_ always includes protocol `http://` or `https://`.
For the purpose of this specification, the _hosted-url_ should always be
normalized such that it doesn't end with a slash (`/`). As all URL end-points
described in this specification includes slash prefix.
For the remainder of this specification, the placeholder `<hosted-url>` will be
used in place of a _hosted-url_ such as:
* `https://pub.dev`
* `https://some-server.com/prefix/pub`
* `https://pub.other-server.com/prefix`
* `http://localhost:8080`
A _hosted-url_ is **not allowed** to contain:
* [_user-info_](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1),
example: `https://user:passwd@host.com`.
* [query-string](https://datatracker.ietf.org/doc/html/rfc3986#section-3.4),
example: `https://host.com/?key=value`.
* [fragment](https://datatracker.ietf.org/doc/html/rfc3986#section-3.5),
example: `https://host.com/#fragment`.
## Custom Package Repository in `pubspec.yaml`
A package be published to a custom _package repository_ by overwriting the
`publish_to` key in `pubspec.yaml`, illustrated as follows:
```yaml
name: mypkg
version: 1.0.0
publish_to: <hosted-url>
```
When taking dependency upon a package from a custom _package repository_ the
dependency must be specified as follows:
```yaml
name: myapp
dependencies:
mypkg:
version: ^1.0.0
hosted:
url: <hosted-url>
name: mypkg
```
## Forward Compatibility
The `dart pub` client will always include an `Accept` header specifying the API
version requested, as follows:
* `Accept: application/vnd.pub.v2+json`
To ensure forward compatibility all API requests should include an `Accept`
header which specifies the version of the API being used. This allows future
versions of the API to change responses.
Clients are strongly encouraged to specify an `Accept` header. But for
compatibility will probably want to assume API version `2`,
if no `Accept` header is specified.
## Metadata headers
The `dart pub` client will attach a `User-Agent` header as follows:
* `User-Agent: Dart pub <sdk-version>`
Custom clients are strongly encouraged to specify a custom `User-Agent` that
allows _package repository operators_ to identify which client a request is
coming from. Including a URL allowing operators to reach owners/authors of the
client is good practice.
* `User-Agent: my-pub-bot/1.2.3 (+https://github.com/organization/<repository>)`
The `User-Agent` header also allows package repository to determine how many
different clients would be affected by an API change.
## Retrying with Exponential Backoff
The `dart pub` client will retry failed requests with
[exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff).
This aims to increase robustness against intermittent network issues, while not
overloading servers that are partially failing.
Clients are strongly encouraged to employ exponential backoff starting at 200ms,
400ms, etc. stopping after 5-7 retries. Excessive retries can have a negative impact
on servers and network performance.
## Rejecting Requests
The `dart pub` client will in many cases to display error messages when given a
response as follows:
```http
HTTP/1.1 4XX Bad Request
Content-Type: application/vnd.pub.v2+json
{
"error": {
"code": "<code>",
"message": "<message>",
},
}
```
The `<message>` is intended to be a brief human-readable explanation of what
when wrong and why the request failed. The `<code>` is a text string intended to
allow clients to handle special cases without using regular expressions to
parse the `<message>`.
## Authentication
The `dart pub` client allows users to save an opaque `<token>` for each
`<hosted-url>`. When the `dart pub` client makes a request to a `<hosted-url>`
for which it has a `<token>` stored, it will attach an `Authorization` header
as follows:
* `Authorization: Bearer <token>`
Tokens can be added to `dart pub` client using the command:
* `dart pub token add <hosted-url>`
This command will prompt the user for the `<token>` on stdin, reducing the risk
that the `<token>` is accidentally stored in shell history. For security reasons,
authentication can only be used when `<hosted-url>` uses HTTPS. For further
details on token management see: `dart pub token --help`.
The tokens are inserted verbatim in the header, therefore they have to adhere to
https://www.rfc-editor.org/rfc/rfc6750#section-2.1. This means they must match
the regex: `^[a-zA-Z0-9._~+/=-]+$`.
### Missing Authentication or Invalid Token
If the server requires authentication and the request does not carry an
`Authorization` header, or the token in the `Authorization` header is invalid
or expired, the server must respond:
```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="pub", message="<message>"
```
If the `dart pub` client receives a `401` response and the `dart pub` client has
a token for the given `<hosted-url>`, then the `dart pub` client knows for sure
that the token it has stored for the given `<hosted-url>` is invalid.
Hence, the `dart pub` client shall remove the token from local configuration.
Hence, a server shall not send `401` in cases where a token is valid but does
not have permissions to access the package in question.
When receiving a `401` response the `dart pub` client shall:
* Abort the current operation (exiting non-zero),
* Delete any token it may have stored for the given `<hosted-url>`,
* Inform the user that authentication is required,
* Print the `<message>` provided by the server.
The `<message>` allows a custom _package server_ to inform the user how a token
may be obtained.
For example, a server might specify a URL from which tokens can be created, as
illustrated below:
```http
GET /packages/mypkg HTTP/1.1
HOST: pub.example.com
User-Agent: Dart pub 2.15.0
Accept: application/vnd.pub.v2+json
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="pub", message="Obtain a token from https://pub.example.com/manage-tokens"
```
The `dart pub` will display the `message` in the terminal, so the user can
discover that they need to navigate to `https://pub.example.com/manage-tokens`.
Once the user opens this URL in the browser, the server is then free to ask the
user to sign-in using any browser-based authentication mechanism. Once signed in
the server can allow the user to create a token and tell the user to copy/paste
this into stdin for `dart pub token add pub.example.com`.
Package server authors are advised to consider security practices such as:
* Prevent a token from being returned again after its initial creation.
* Associate names with tokens (so users can remember what computer they
create the token for).
* Record last used date-time for each token.
* Expire tokens not frequently used.
* Token rotation through forced expiration.
### Insufficient Permissions
If the server requires authorization and the request carries an `Authorization`
header with a valid token, but this token authenticates the client as a user
that does not have permissions to access the given resource the server must
respond:
```http
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer realm="pub", message="<message>"
```
This behaves the same as `401` with the exception that the token will not be
automatically deleted from local configuration by the `dart pub` client. This
makes sense if the token is valid, but doesn't have sufficient permissions.
The `<message>` allows a custom package repository to indicate how sufficient
permissions can be granted. Depending on how permissions are managed on the
server, this could work in many different ways.
## List all versions of a package
**GET** `<hosted-url>/api/packages/<package>`
**Headers:**
* `Accept: application/vnd.pub.v2+json`
**Response**
* `Content-Type: application/vnd.pub.v2+json`
```js
{
"name": "<package>",
"isDiscontinued": true || false, /* optional field, false if omitted */
"replacedBy": "<package>", /* optional field, if isDiscontinued == true */
"latest": {
"version": "<version>",
"retracted": true || false, /* optional field, false if omitted */
"archive_url": "https://.../archive.tar.gz",
"archive_sha256": "95cbaad58e2cf32d1aa852f20af1fcda1820ead92a4b1447ea7ba1ba18195d27"
"pubspec": {
/* pubspec contents as JSON object */
}
},
"versions": [
{
"version": "<package>",
"retracted": true || false, /* optional field, false if omitted */
"archive_url": "https://.../archive.tar.gz",
"archive_sha256": "95cbaad58e2cf32d1aa852f20af1fcda1820ead92a4b1447ea7ba1ba18195d27"
"pubspec": {
/* pubspec contents as JSON object */
}
},
/* additional versions */
]
}
```
To fetch the package archive an HTTP `GET` request **following redirects** must
be made to the URL given as `archive_url`.
The response (after following redirects) must be a gzipped TAR archive.
The `archive_url` may be temporary and is allowed to include query-string
parameters. This allows for the server to return signed URLs for S3, GCS, or
other blob storage services. If temporary URLs are returned it is wise to not set
expiration to less than 25 minutes (to allow for retries and clock drift).
The `archive_sha256` should be the hex-encoded sha256 checksum of the file at
archive_url. It is an optional field that allows the pub client to verify the
integrity of the downloaded archive.
The `archive_sha256` also provides an easy way for clients to detect if
something has changed on the server. In the absence of this field, the client can
still download the archive to obtain a checksum and detect changes to the
archive.
If `<hosted-url>` for the server returning `archive_url` is a prefix of
`archive_url`, then the `Authorization: Bearer <token>` is also included when
`archive_url` is requested. Example: if `https://pub.example.com/path` returns
an `archive_url = 'https://pub.example.com/path/...'` then the request for
`https://pub.example.com/path/...` will include `Authorization` header.
This would however, not be the case if the same server returned
`archive_url = 'https://pub.example.com/blob/...'`.
## Publishing Packages
**GET** `<hosted-url>/api/packages/versions/new`
**Headers:**
* `Accept: application/vnd.pub.v2+json`
* `Authorization: Bearer <token>` (required)
**Response**
* `Content-Type: application/vnd.pub.v2+json`
```js
{
"url": "<multipart-upload-url>",
"fields": {
"<field-1>": "<value-1>",
"<field-2>": "<value-2>",
...,
"<field-N>": "<value-N>",
},
}
```
To publish a package an HTTP `GET` request for
`<hosted-url>/api/packages/versions/new` is made. This request returns an
`<multipart-upload-url>` and a dictionary of fields. To upload the package
archive a multi-part `POST` request is made to `<multipart-upload-url>` with
fields and the field `file` containing the gzipped tar archive.
```http
POST <path(multipart-upload-url)> HTTP/1.1
Host: <host(multipart-upload-url)>
Content-Length: <length>
Content-Type: multipart/form-data; boundary=<boundary>
--<boundary>
Content-Disposition: form-data; name="<urlencode(field-1)>"
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: binary
<value-1>
--<boundary>
Content-Disposition: form-data; name="<urlencode(field-2)>"
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: binary
<value-2>
...
--<boundary>
Content-Disposition: form-data; name="<urlencode(field-N)>"
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: binary
<value-N>
--<boundary>
Content-Type: application/octet-stream
Content-Disposition: form-data; name="file"; filename="package.tar.gz"
<gzipped archieve>
```
The above `POST` request to `<multipart-upload-url>` should respond as follows:
```http
HTTP/1.1 204 No Content
Location: <finalize-upload-url>
```
The client shall then issue a `GET` request to `<finalize-upload-url>`. As with
`archive_url` the client will only attach an `Authorization` if the
`<hosted-url>` is a prefix of `<finalize-upload-url>`. If the server wants to
accept the uploaded package the server should respond:
```http
HTTP/1.1 200 Ok
Content-Type: application/vnd.pub.v2+json
{
"success": {
"message": "<message>",
},
}
```
The server is allowed to consider the publishing incomplete until the `GET`
request for `<finalize-upload-url>` has been issued. Once this request has
succeeded the package is considered successfully published. If the server has
caches that need to expire before newly published packages become available,
or it has other out-of-band approvals that need to be given it's reasonable to
inform the user about this in the `<message>`.
If the server does not want to accept the uploaded package, it can respond:
```http
HTTP/1.1 400 Bad Request
Content-Type: application/vnd.pub.v2+json
{
"error": {
"code": "<code>",
"message": "<message>",
},
}
```
This can be used to forbid git dependencies in published packages, limit the
archive size, or enforce any other repository-specific constraints.
This upload flow allows for archives to be uploaded directly to a signed POST
URL for [S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/HTTPPOSTExamples.html),
[GCS](https://cloud.google.com/storage/docs/xml-api/post-object-forms) or
similar blob storage service. Both the
`<multipart-upload-url>` and `<finalize-upload-url>` are allowed to contain
query-string parameters, and both of these URLs need only be temporary.
------------
## (Deprecated) Inspect a specific version of a package
**Deprecated** as of Dart 2.8, use "List all versions of a package" instead.
Servers should still support this end-point for compatibility with older `pub` clients.
**GET** `<hosted-url>/api/packages/<package>/versions/<version>`
**Headers:**
* `Accept: application/vnd.pub.v2+json`
**Response**
* `Content-Type: application/vnd.pub.v2+json`
```js
{
"version": "1.1.0",
"archive_url": "https://.../archive.tar.gz",
"pubspec": {
/* pubspec contents as JSON object */
}
}
```
## (Deprecated) Download a specific version of a package
**Deprecated** as of Dart 2.8, use the `archive_url` returned from the "List all versions of a package".
Servers should still support this end-point for compatibility with older `pub` clients.
**GET** `<hosted-url>/packages/<package>/versions/<version>.tar.gz`
**Headers**
* `Accept: application/octet-stream` (optional)
**Response** (typically through a ยด30x` redirect)
* `Content-Type: application/octet-stream`
**Important:** The server MAY redirect the client to a different URL, clients
MUST support redirects.