Skip to main content

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ải file từ cùng origin OTA, kiểm tra size/sha256 trước khi áp dụng.
Chú ý:
Giá trịVai trò và nơi sử dụng
signatureMetadata 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 tokenCredential 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-KeyCredential 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.
SHA-256 đối chiếu bytes với hash do publisher khai báo, giúp phát hiện dữ liệu sai; nó không chứng minh nguồn gốc bundle khi publisher có thể cung cấp cả bytes và hash. Chữ ký S3 SigV4 dùng truy cập storage cũng không phải chữ ký phát hành cho mobile. Nếu mobile cần trusted signing, đó là hợp đồng trust/key verification riêng, chưa được backend này cung cấp.

2. API

Origin: https://ota.zhst.app.
Method và pathXác thựcKết quả thành công
GET /ota/{app}/{platform}/manifest.jsonKhông200, {"channels":{...}}; app hợp lệ chưa có release trả {"channels":{}}, Cache-Control: no-cache
GET /ota/{app}/{platform}/{file}KhôngBundle, 200; byte range hợp lệ có thể trả 206
POST /ota/{app}/{platform}/releasesBearer; thêm upload key nếu app đã cấu hình201, một entry manifest, không có envelope {ok,data}
GET /ota/{app}/{platform}/releasesBearer200, cùng cấu trúc manifest; hỗ trợ lọc channel, binary
DELETE /ota/{app}/{platform}/releases?channel=...&binary=...Bearer204, không body; xóa toàn bộ release của cặp đó
Đổi upload key không đổi auth download/list/DELETE. Backend không làm download private: manifest và bundle vẫn đọc ẩn danh.

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:
Manifest bọc chính entry đó tại 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:
Với app chưa cấu hình upload key, bỏ dòng kiểm tra 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ến BASE, 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:
File ví dụ không chứng minh có object đó trên máy thật. Trước khi áp dụng, đối chiếu byte length và SHA-256 tải về với entry. Download có 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:
URL-encode / 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:
Khi đã cấu hình, 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:
Đây là mô tả envelope, không phải JSON để gửi hay credential ví dụ. Key do server sinh bằng 64 byte ngẫu nhiên, standard base64 88 ký tự, chỉ persist hash. Lần tạo đầu bật bảo vệ cho riêng app đó; không tạo thì vẫn dùng auth bearer hiện có. Rotate thay hash ngay: push mới qua bước xác thực phải dùng key mới. Request đã xác thực trước khi rotate có thể hoàn tất; không coi rotate là hủy upload đang chạy. Các release cũ, đường tải, list và DELETE không đổi. Không có grace period hoặc API tắt bảo vệ được mô tả ở đây. Ví dụ chạy trong console của workspace cùng origin, đã đăng nhập; response chứa key chỉ giữ trong biến để chuyển vào secret store được duyệt, không console.log toàn response:
Thao tác thứ hai thực sự rotate, không phải kiểm tra read-only; chỉ chạy trong phạm vi đã được phép. Dùng controls workspace là lựa chọn thông thường. Nếu gọi ngoài browser, cần session cookie hợp lệ và hai header CSRF nêu trên; không chia sẻ cookie hay dùng nó làm secret CI dài hạn.

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} trong data, không phải manifest.
  • POST token-create JSON {app,name} trả 201 với {token,record} trong data; POST token-revoke JSON {id} thu hồi bearer. Tạo bearer mới không tự thu hồi các bearer cũ.
  • POST release-promote JSON {id} chọn lại release theo row ID admin; POST release-delete JSON {id} xóa riêng bản đó. Không dùng manifest ID timestamp thay row ID.
  • POST release-upload multipart gồm app, 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 vi app/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ìnhCách xử lý
400 INVALID_INPUT, INVALID_MULTIPART, INVALID_LENGTH, BUNDLE_MISSINGKiểm tra schema, boolean text chính xác, standard base64, multipart boundary và file không rỗng.
400 SHA_MISMATCHTí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_REVOKEDKiể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_MISMATCHSai namespace app với bearer.
403 UPLOAD_KEY_REQUIRED, UPLOAD_KEY_INVALIDApp đã 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, CSRFAdmin 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_FOUNDKiểm tra app/path, expiry/delete và object/backend; không coi mọi manifest rỗng là lỗi.
409 APP_DISABLEDQuản trị kiểm tra app đang bị disable.
405 METHOD_NOT_ALLOWEDDùng đúng GET/POST/DELETE và Allow; endpoint rotate không phải GET.
413 BUNDLE_TOO_LARGE, BODY_TOO_LARGEGiả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_TYPEPublic upload cần multipart đúng boundary; JSON admin cần application/json.
400/503 SECRET_KEY_MISSING, 500 SECRET_UNREADABLEKiể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_CONFIGUREDKiể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_ERRORTra runtime log đã sanitize theo môi trường và request/time window, migration/binding/config đúng; không công bố stack/secret.