# Installers

Publish app installers (.pkg, .dmg, .zip) with permanent download links. Each upload is one immutable version.

## Before uploading (macOS)

heremyapp does not sign, notarize or vouch for installers. Sign the app and the .pkg with Developer ID,
notarize and staple before uploading, then check:

```bash
spctl --assess --type install -vv MyApp.pkg    # expect "accepted" and "source=Notarized Developer ID"
```

Agents: if this check fails, stop and ask the user before uploading. Unsigned installers are blocked by
Gatekeeper on users' Macs.

Notarizing needs credentials stored in the keychain (`xcrun notarytool store-credentials PROFILE_NAME ...`, then
`xcrun notarytool submit MyApp.pkg --keychain-profile PROFILE_NAME --wait`). If no profile exists, ask the user to run
`store-credentials` themselves in a terminal. It uses their Apple ID app-specific password or an App Store Connect API
key; never ask them to paste those into the conversation.

## Names and versions

- `filename`: the bare file name, without folders. 1-100 characters of letters, digits, `.`, `_` or `-`, ending in
  `.pkg`, `.dmg` or `.zip`. Keep it the same across releases (`MyApp.pkg`, not `MyApp-1.2.0.pkg`).
- `version`: a [semantic version](https://semver.org) such as `1.2.0`, normally the app's
  `CFBundleShortVersionString`. It must be higher than the previous release of the same file.
- `build` (recommended): the app's build number, such as `CFBundleVersion` (`46` or `1.2.46`). It must also increase.
- `notes` (optional): a one-line summary of what changed. Show it on the landing page as release notes.
- See [Releases and versioning](https://heremyapp.com/docs/releases.md) for how to choose the next version and build.
- `latest` means the most recently published version, not the highest version number.

Because the filename is stable, the latest link is predictable before you upload:
`https://heremyapp.com/dl/ACCOUNT/SPACE/latest/FILENAME`. Publishing the installer before the site means the link works as soon
as the site is live.

Always use this **absolute** URL in the site. The site runs on its own origin, so a relative link such as `/dl/...`
would point at the site itself and return 404.

## 1. Measure the file

```bash
FILE=./build/MyApp.pkg
NAME=$(basename "$FILE")
SIZE=$(stat -f%z "$FILE" 2>/dev/null || stat -c%s "$FILE")
SHA=$(shasum -a 256 "$FILE" | cut -d' ' -f1)
```

## 2. Register it

```bash
curl -sS --fail-with-body -X POST "$API/spaces/my-app/artifacts" -H "$AUTH" -H "content-type: application/json" \
  -d "{\"filename\":\"$NAME\",\"version\":\"1.2.0\",\"build\":\"46\",\"notes\":\"Faster sync\",\"size\":$SIZE,\"sha256\":\"$SHA\"}"
```

Success is `201` with the artifact in `"status":"pending"`:

```json
{"id":"art_3kq9...","space":"peter/my-app","filename":"MyApp.pkg","version":"1.2.0","size":18234567,
 "sha256":"4583...75fa","status":"pending",
 "download_url":"https://heremyapp.com/dl/peter/my-app/v/1.2.0/MyApp.pkg",
 "latest_url":"https://heremyapp.com/dl/peter/my-app/latest/MyApp.pkg",
 "upload":{"mode":"single","method":"PUT","url":"https://api.heremyapp.com/v1/artifacts/art_3kq9.../content",
  "command":"curl -sS --fail-with-body -T MyApp.pkg -H \"Authorization: Bearer $HEREMYAPP_TOKEN\" https://api.heremyapp.com/v1/artifacts/art_3kq9.../content"}}
```

For a larger file, `upload` looks like this instead:

```json
{"mode":"multipart","part_size":52428800,"part_count":3,
 "part_url":"https://api.heremyapp.com/v1/artifacts/art_3kq9.../parts/{part_number}","complete_url":"https://api.heremyapp.com/v1/artifacts/art_3kq9.../complete",
 "command":"split -b 52428800 -a 3 MyApp.pkg /tmp/art_3kq9....part. && ..."}
```

`upload.mode` tells you which upload to do. `upload.command` is a ready-made shell command that contains no secrets.
It refers to the file by its bare name, so run it from the folder that contains the file (`cd "$(dirname "$FILE")"`),
with `HEREMYAPP_TOKEN` set. Or follow the steps below with your own path.

Registering the same filename and version again while it is still pending replaces the pending one.

## 3a. Upload in one request (`"mode":"single"`, up to 95 MiB)

```bash
curl -sS --fail-with-body -T "$FILE" -H "$AUTH" "$API/artifacts/ARTIFACT_ID/content"
```

The response is the artifact with `"status":"active"`.

## 3b. Upload in parts (`"mode":"multipart"`, up to 500 MiB)

`upload.part_size` (in bytes; use it as `PART_SIZE` below) and `upload.part_count` describe the parts. Parts are numbered from 1, and every part except the
last must be exactly `part_size` bytes. A failed part can simply be sent again.

```bash
split -b PART_SIZE -a 3 "$FILE" /tmp/heremyapp-part.
n=1; ok=1
for f in /tmp/heremyapp-part.*; do
  curl -sS --fail-with-body -T "$f" -H "$AUTH" "$API/artifacts/ARTIFACT_ID/parts/$n" || { ok=0; break; }
  n=$((n+1))
done
[ "$ok" = 1 ] && curl -sS --fail-with-body -X POST -H "$AUTH" "$API/artifacts/ARTIFACT_ID/complete"
rm -f /tmp/heremyapp-part.*
```

`complete` returns the artifact with `"status":"active"`, or `missing_parts` listing parts still to send.

## Verification

The server checks the exact size, the SHA-256 and the file signature (pkg = xar archive, dmg = UDIF image, zip).
If a check fails (`checksum_mismatch`, `invalid_file_content`), the artifact is discarded: fix the file or the
values and register again. Published versions are immutable, and each release must have a higher version and build
than the last one (`409 version_not_increasing` or `build_not_increasing`, with suggested values in `details`).

## Download links

- `latest_url`: `https://heremyapp.com/dl/ACCOUNT/SPACE/latest/FILENAME`. Use this for the download button.
- `download_url`: `https://heremyapp.com/dl/ACCOUNT/SPACE/v/VERSION/FILENAME`. This exact version, forever.

Downloads use `Content-Disposition: attachment`, support resumable Range requests and HEAD, and send the SHA-256 in
`ETag` and `X-Checksum-SHA256`.

```bash
# quick check without downloading: which version latest points to, and its hash
curl -sIL https://heremyapp.com/dl/ACCOUNT/my-app/latest/MyApp.pkg | grep -iE "^location|^x-checksum-sha256"
# full check: download and hash
curl -fsSL -o /tmp/check.pkg https://heremyapp.com/dl/ACCOUNT/my-app/latest/MyApp.pkg && shasum -a 256 /tmp/check.pkg
```

## Manage

- `GET $API/spaces/my-app/artifacts` lists installers with `downloads` and `bytes_served`.
- `GET $API/artifacts/ARTIFACT_ID` shows one, including upload instructions while it is pending.
- `DELETE $API/artifacts/ARTIFACT_ID` removes a version. Its URL then returns `410 Gone`, `latest` falls back to
  the previous version, and the version cannot be reused.
