1. Định nghĩa
app là namespace đã đăng ký;
platform là android hoặc ios;
channel là nhánh/kênh dev, có thể chứa /;
binary là phiên bản native tương thích, ví dụ 1.0.66+93 hoặc 1.6.6.
Ý nghĩa bắt buộc cập nhật:
mandatory: true: app mobile phải áp dụng bản cập nhật tương thích được chọn; không cung cấp lựa chọn bỏ qua bản đó.mandatory: false: cập nhật không bắt buộc; đây là mặc định khi publisher không gửi trường này.- Mobile chọn đúng
app/platform/channel/binary, đọc entry trong manifest, tảifiletừ cùng origin OTA, kiểm trasize/sha256trước khi áp dụng.
| Giá trị | Vai trò và nơi sử dụng |
|---|---|
signature | Metadata công khai, opaque, standard base64; có trong manifest và response. Server kiểm tra định dạng nhưng không xác minh chữ ký, không chọn Ed25519 hay thuật toán nào. Không đặt secret vào trường này. |
| Bearer push token | Credential riêng tư, theo app; gửi trong Authorization. Dùng cho publish, list bảo vệ và DELETE. Không nhúng vào app mobile. |
X-OTA-Upload-Key | Credential riêng tư bổ sung, do admin sinh theo app; chỉ dùng với POST public releases khi app đã bật bảo vệ. Không phải signature, không đưa vào manifest/mobile. |
2. API
Origin:https://ota.zhst.app.
| Method và path | Xác thực | Kết quả thành công |
|---|---|---|
GET /ota/{app}/{platform}/manifest.json | Không | 200, {"channels":{...}}; app hợp lệ chưa có release trả {"channels":{}}, Cache-Control: no-cache |
GET /ota/{app}/{platform}/{file} | Không | Bundle, 200; byte range hợp lệ có thể trả 206 |
POST /ota/{app}/{platform}/releases | Bearer; thêm upload key nếu app đã cấu hình | 201, một entry manifest, không có envelope {ok,data} |
GET /ota/{app}/{platform}/releases | Bearer | 200, cùng cấu trúc manifest; hỗ trợ lọc channel, binary |
DELETE /ota/{app}/{platform}/releases?channel=...&binary=... | Bearer | 204, không body; xóa toàn bộ release của cặp đó |
Form publish
Đểcurl/FormData tự đặt multipart boundary; không tự gán Content-Type: multipart/form-data thiếu boundary.
Response 201 và manifest
Ví dụ một entry; signature dưới đây chỉ là 64 byte tuần tự mã hóa base64, không phải chữ ký đã ký/xác minh:
channels["dev/delivery"]["1.0.66+93"]. Nếu bỏ cả hai metadata khi publish, entry vẫn có "signature":"" và "mandatory":false. 201 có Cache-Control: no-store và Location trỏ bundle. file là đường tương đối do server sinh; không tự tái tạo từ branch. Manifest id là release ID; admin có row id riêng dùng để rollback/delete từng release.
Publish từ CI
CI lấy bearer/upload key từ secret store đã được phê duyệt và inject qua môi trường, không từ repository.OTA_PUBLIC_SIGNATURE là metadata công khai của artifact, không phải upload key. Không bật shell trace set -x, không log header hay response rotate.
Ví dụ Bash dưới đây dành cho app đã bật upload protection:
OTA_UPLOAD_KEY và header X-OTA-Upload-Key; bearer vẫn bắt buộc. Không lấy signature để điền header, không tự sinh upload key ở CI. Khi bật bảo vệ lần đầu, chuẩn bị secret CI trước rồi chuyển publish sang hai credential; có thể tạm dừng publisher trong thời gian chuyển giao.
Đọc, tải, list và gỡ
Dùng các biếnBASE, APP, PLATFORM, CHANNEL, BINARY từ ví dụ publish. FILE phải lấy nguyên từ response/manifest của bản cần tải:
Content-Length, ETag bằng sha256, application/octet-stream, không content coding và cache immutable; resume bằng một explicit range, ví dụ Range: bytes=1048576-. Không giả định suffix/multi-range được phục vụ từng phần; handler sở hữu quy tắc range.
Lệnh phá hủy, chỉ dùng khi đã duyệt đúng app/platform/channel/binary:
/ và + trong query; multipart text giữ nguyên. DELETE không phải rollback: nó gỡ mọi bản của cặp này và object tương ứng, không điều khiển bundle đã tải/cache hoặc bản đang chạy trên máy.
3. Admin API và upload protection
Admin API ở cùng origin/api/ota/{action}, dùng session EmDash có settings:manage, không dùng bearer publish để thay session. GET cần session; POST cần thêm Origin đúng origin và X-EmDash-Request: 1. JSON POST có Content-Type: application/json. Browser tự gửi cookie với credentials: "same-origin" và Origin khi POST; không tìm cách tự đặt Origin trong JavaScript. Multipart admin upload không tự đặt content type. Client workspace là mẫu thực thi.
Envelope thành công: {"ok":true,"data":...}. Lỗi: {"ok":false,"error":{"code":"...","message":"..."}}. Admin response dùng Cache-Control: private, no-store.
Xem trạng thái key, không đọc plaintext
GET /api/ota/upload-key?app=unitel-man trả 200:
configured là true, updatedAt là timestamp ISO. Không có API lấy lại key plaintext.
Tạo lần đầu hoặc rotate
POST /api/ota/upload-key-rotate, JSON {"app":"unitel-man"}, trả 201:
console.log toàn response:
Các tác vụ admin vận hành
Các payload khác đối chiếu dispatch admin và schema trước khi tự động hóa:GET info,GET releases(lọc và cursor),GET tokens,GET storage,GET audit: workspace tra cứu nguồn hiện hành. Admin releases trả{items,nextCursor}trongdata, không phải manifest.POST token-createJSON{app,name}trả201với{token,record}trongdata;POST token-revokeJSON{id}thu hồi bearer. Tạo bearer mới không tự thu hồi các bearer cũ.POST release-promoteJSON{id}chọn lại release theo row ID admin;POST release-deleteJSON{id}xóa riêng bản đó. Không dùng manifest ID timestamp thay row ID.POST release-uploadmultipart gồmapp,platform,bundle,channel,binary,note,commit,signature,mandatory. Backend tính sha256; session admin được phép upload không cần bearer hoặc upload key. Đây là đường quản trị được duyệt, không phải lối upload anonymous. Metadata có cùng validation/default như public publish.POST app-save,POST storage-save,POST storage-test,POST purge: thay đổi cấu hình hoặc dữ liệu; dùng workspace và đọc schema/service trước khi dùng API. Test storage ghi/đọc/xóa probe object, không phải thao tác read-only.
4. Release, rollback và retention
Trong phạm viapp/platform/channel/binary, backend giữ bản current và bản trước theo service và defaults. Bản cũ hơn bị xóa; retention mặc định 14 ngày tính từ createdAt, kể cả current. Push mới có sweep; Worker scheduled hỗ trợ kênh ngừng push.
Gửi lại current với cùng bytes/sha/size và cùng signature/mandatory là replay: trả bản đã lưu với 201, không nhân bản, không làm mới createdAt. Thay signature hoặc mandatory của cùng bytes tạo release mới, không âm thầm sửa metadata bản cũ. Không dùng replay làm keep-alive; cấu hình retention phù hợp lịch dev.
Runbook rollback: chọn đúng cặp trong Releases, xác nhận bản trước còn object và metadata phù hợp, chọn rollback/promote, rồi đọc lại manifest và thử app dev. Pointer thay đổi ngay; app chỉ nhận khi nó poll và thực thi logic cập nhật của mình. signature/mandatory đi cùng bản được chọn; promote không tự đổi cờ. Nếu bản cũ đã bị retention/delete, rollback không thể phục hồi bytes; cần artifact lưu trữ và publish lại hoặc restore đã duyệt.
Trước delete/retention purge, kiểm tra phạm vi và việc mất đường tải đối với thiết bị chưa tải. Bản đã nằm trong cache/máy không bị thu hồi bởi việc xóa ở server. Xóa metadata có thể thành công dù xóa object lỗi, để lại object orphan vẫn tính phí; kiểm tra log và dọn trong storage theo quyền vận hành, không suy ra đã hết object chỉ từ 204.
5. Mã lỗi
App error là{"error":{"code":"...","message":"..."}}; admin error thêm ok:false. Đọc cả HTTP status và code; không log token/header/body nhạy cảm khi báo lỗi. HTTP shared, multipart và storage adapters sở hữu mapping đầy đủ.
| Status/code điển hình | Cách xử lý |
|---|---|
400 INVALID_INPUT, INVALID_MULTIPART, INVALID_LENGTH, BUNDLE_MISSING | Kiểm tra schema, boolean text chính xác, standard base64, multipart boundary và file không rỗng. |
400 SHA_MISMATCH | Tính lại hash từ bytes thật đang gửi; không bỏ kiểm tra hash hoặc gửi một hash khác để che lỗi. |
401 AUTH_REQUIRED, TOKEN_INVALID, TOKEN_REVOKED | Kiểm tra bearer đúng app từ store; tạo/cấp lại theo quyền admin, không lấy từ mobile. |
403 TOKEN_APP_MISMATCH | Sai namespace app với bearer. |
403 UPLOAD_KEY_REQUIRED, UPLOAD_KEY_INVALID | App đã bật bảo vệ: gửi đúng key mới của app qua header, không lấy public signature thay thế. |
403 FORBIDDEN, CSRF | Admin session/quyền hoặc same-origin, Origin, X-EmDash-Request sai; không nới gate. |
404 APP_UNKNOWN, NOT_FOUND, BUNDLE_UNAVAILABLE, RELEASE_NOT_FOUND | Kiểm tra app/path, expiry/delete và object/backend; không coi mọi manifest rỗng là lỗi. |
409 APP_DISABLED | Quản trị kiểm tra app đang bị disable. |
405 METHOD_NOT_ALLOWED | Dùng đúng GET/POST/DELETE và Allow; endpoint rotate không phải GET. |
413 BUNDLE_TOO_LARGE, BODY_TOO_LARGE | Giảm bundle hoặc điều chỉnh ceiling sau khi đánh giá bộ nhớ host, kể cả multipart overhead. |
415 UNSUPPORTED_MEDIA_TYPE, CONTENT_TYPE | Public upload cần multipart đúng boundary; JSON admin cần application/json. |
400/503 SECRET_KEY_MISSING, 500 SECRET_UNREADABLE | Kiểm tra runtime key mã hóa, restore/rotation và nhập lại R2 secret qua workspace. |
502 STORAGE_UNAUTHORIZED, STORAGE_MISSING, STORAGE_FAILED; 503 STORAGE_NOT_CONFIGURED | Kiểm tra mode, binding, endpoint/bucket và quyền credential trong hồ sơ hạn chế quyền; chạy storage test được duyệt. |
500 INTERNAL_ERROR, PUSH_FAILED; 503 CONFIGURATION_ERROR | Tra runtime log đã sanitize theo môi trường và request/time window, migration/binding/config đúng; không công bố stack/secret. |