afterpresets 0.1.0 → 0.3.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.
Files changed (173) hide show
  1. package/README.md +6 -4
  2. package/native-runtime/CMakeLists.txt +82 -0
  3. package/native-runtime/README.md +40 -0
  4. package/native-runtime/THIRD_PARTY_NOTICES.md +14 -0
  5. package/native-runtime/include/ap_native.h +223 -0
  6. package/native-runtime/src/apply.cpp +199 -0
  7. package/native-runtime/src/crypto.cpp +225 -0
  8. package/native-runtime/src/crypto.hpp +51 -0
  9. package/native-runtime/src/dialog.cpp +62 -0
  10. package/native-runtime/src/dialog.hpp +35 -0
  11. package/native-runtime/src/engine.cpp +909 -0
  12. package/native-runtime/src/engine.hpp +232 -0
  13. package/native-runtime/src/fsutil.cpp +291 -0
  14. package/native-runtime/src/fsutil.hpp +57 -0
  15. package/native-runtime/src/json.cpp +326 -0
  16. package/native-runtime/src/json.hpp +70 -0
  17. package/native-runtime/src/platform.hpp +83 -0
  18. package/native-runtime/src/platform_common.cpp +60 -0
  19. package/native-runtime/src/platform_mac.mm +314 -0
  20. package/native-runtime/src/platform_win.cpp +277 -0
  21. package/native-runtime/src/runtime.cpp +473 -0
  22. package/native-runtime/src/util.cpp +169 -0
  23. package/native-runtime/src/util.hpp +32 -0
  24. package/native-runtime/src/zip.cpp +243 -0
  25. package/native-runtime/src/zip.hpp +58 -0
  26. package/native-runtime/third_party/monocypher/LICENCE.md +167 -0
  27. package/native-runtime/third_party/monocypher/monocypher-ed25519.c +500 -0
  28. package/native-runtime/third_party/monocypher/monocypher-ed25519.h +140 -0
  29. package/native-runtime/third_party/monocypher/monocypher.c +2956 -0
  30. package/native-runtime/third_party/monocypher/monocypher.h +321 -0
  31. package/native-runtime/third_party/tinf/LICENSE +22 -0
  32. package/native-runtime/third_party/tinf/crc32.c +57 -0
  33. package/native-runtime/third_party/tinf/tinf.h +140 -0
  34. package/native-runtime/third_party/tinf/tinflate.c +636 -0
  35. package/native-runtime/vendor-files.json +39 -0
  36. package/package.json +6 -3
  37. package/runtime/docs/cep/host-after-effects.md +57 -0
  38. package/runtime/docs/cep/host-premiere.md +84 -0
  39. package/runtime/docs/{host.md → cep/host.md} +42 -16
  40. package/runtime/docs/{README.md → common/README.md} +10 -8
  41. package/runtime/docs/{assets.md → common/assets.md} +16 -2
  42. package/runtime/docs/{licensing.md → common/licensing.md} +1 -1
  43. package/runtime/docs/{panels.md → common/panels.md} +5 -5
  44. package/runtime/docs/{storage.md → common/storage.md} +5 -5
  45. package/runtime/docs/{updates.md → common/updates.md} +1 -1
  46. package/runtime/docs/native/README.md +44 -0
  47. package/runtime/docs/native/debugging.md +140 -0
  48. package/runtime/docs/native/rules.md +402 -0
  49. package/runtime/docs/native/testing.md +154 -0
  50. package/runtime/package.json +1 -1
  51. package/runtime/scripts/ae-sdk-docs/config.js +102 -0
  52. package/runtime/scripts/ae-sdk-docs/convert.js +430 -0
  53. package/runtime/scripts/ae-sdk-docs/index.js +314 -0
  54. package/runtime/scripts/ae-sdk-docs/source.js +205 -0
  55. package/runtime/scripts/ae-sdk-docs/split.js +170 -0
  56. package/runtime/scripts/ae-sdk-docs.js +72 -0
  57. package/runtime/src/host.js +1 -1
  58. package/runtime/src/updates.js +1 -1
  59. package/src/api/error-messages.js +1 -1
  60. package/src/build/build.js +7 -7
  61. package/src/build/html.js +1 -1
  62. package/src/build/jsx.js +2 -2
  63. package/src/build/runtime.js +3 -3
  64. package/src/check/check.js +3 -3
  65. package/src/check/native.js +14 -0
  66. package/src/check/pricing.js +1 -1
  67. package/src/check/types.js +2 -2
  68. package/src/cli.js +5 -5
  69. package/src/commands/add.js +8 -3
  70. package/src/commands/build.js +8 -2
  71. package/src/commands/check.js +11 -3
  72. package/src/commands/dev.js +9 -2
  73. package/src/commands/docs.js +5 -1
  74. package/src/commands/init.js +15 -3
  75. package/src/commands/push.js +5 -2
  76. package/src/commands/sdk.js +14 -0
  77. package/src/commands/testers.js +1 -1
  78. package/src/commands/upgrade.js +2 -2
  79. package/src/commands/whoami.js +1 -1
  80. package/src/config.js +1 -1
  81. package/src/dev/bridge.js +8 -0
  82. package/src/dev/cep-link.js +1 -1
  83. package/src/dev/debug-file.js +5 -5
  84. package/src/dev/native.js +3 -0
  85. package/src/dev/session.js +4 -3
  86. package/src/mcp/extendscript.js +10 -7
  87. package/src/mcp/host-target.js +1 -0
  88. package/src/mcp/panels.js +1 -1
  89. package/src/mcp/png.js +1 -0
  90. package/src/mcp/server.js +2 -2
  91. package/src/mcp/tools/capture.js +3 -0
  92. package/src/mcp/tools/host.js +1 -1
  93. package/src/mcp/tools/native.js +6 -0
  94. package/src/mcp/tools/panel.js +4 -4
  95. package/src/mcp/tools/project.js +47 -31
  96. package/src/mcp/tools/releases.js +23 -20
  97. package/src/native/build.js +27 -0
  98. package/src/native/cmake/CMakeLists.txt +88 -0
  99. package/src/native/cmake/zig-windows.cmake +19 -0
  100. package/src/native/effect.js +3 -0
  101. package/src/native/install.js +8 -0
  102. package/src/native/installers.js +4 -0
  103. package/src/native/pipl.js +20 -0
  104. package/src/native/restart.js +2 -0
  105. package/src/native/sdk.js +8 -0
  106. package/src/project/add-panel.js +1 -1
  107. package/src/project/docs.js +3 -1
  108. package/src/project/manifest.js +3 -2
  109. package/src/project/project.js +2 -2
  110. package/src/project/template.js +5 -1
  111. package/src/prompt.js +1 -1
  112. package/src/release/installers.js +4 -4
  113. package/src/release/native.js +3 -0
  114. package/src/release/package.js +1 -1
  115. package/src/release/publish.js +1 -1
  116. package/src/system/run-command.js +1 -1
  117. package/src/targets.js +1 -0
  118. package/src/user-config.js +2 -2
  119. package/template/cep/AGENTS.md +104 -0
  120. package/template/{README.md → cep/README.md} +16 -11
  121. package/template/cep/docs/code_guide.md +67 -0
  122. package/template/cep/docs/initial_setup.md +115 -0
  123. package/template/cep/docs/release_checklist.md +24 -0
  124. package/template/{src → cep/src}/features/home/home.html +4 -4
  125. package/template/{src → cep/src}/features/home/home.js +10 -11
  126. package/template/{src → cep/src}/shared/lib/host-theme.js +3 -3
  127. package/template/{src → cep/src}/shared/lib/view.js +1 -1
  128. package/template/{src → cep/src}/shared/ui/tokens.css +1 -1
  129. package/template/hosts/AGENTS.two-apps.md +12 -0
  130. package/template/hosts/aeft/AGENTS.host.md +10 -0
  131. package/template/hosts/aeft/jsx/aeft/home.jsx +16 -0
  132. package/template/hosts/ppro/AGENTS.host.md +15 -0
  133. package/template/hosts/ppro/jsx/ppro/home.jsx +20 -0
  134. package/template/native/.mcp.json +8 -0
  135. package/template/native/AGENTS.md +105 -0
  136. package/template/native/README.md +71 -0
  137. package/template/native/docs/code_guide.md +72 -0
  138. package/template/native/docs/initial_setup.md +179 -0
  139. package/template/native/docs/release_checklist.md +24 -0
  140. package/template/native/effect.json +16 -0
  141. package/template/native/gitignore +8 -0
  142. package/template/native/package.json +14 -0
  143. package/template/native/src/Effect.cpp +224 -0
  144. package/template/native/src/License.cpp +95 -0
  145. package/template/native/src/License.h +29 -0
  146. package/template/native/src/Render.h +68 -0
  147. package/vendor/afterpresets-additions/AfterEffects.d.ts +291 -0
  148. package/vendor/afterpresets-additions/Premiere.d.ts +179 -0
  149. package/vendor/types-for-adobe/Premiere/24.0/index.d.ts +2567 -0
  150. package/vendor/types-for-adobe/README.md +8 -4
  151. package/template/AGENTS.md +0 -168
  152. package/template/docs/initial_setup.md +0 -69
  153. package/template/jsx/home.jsx +0 -17
  154. /package/runtime/docs/{boot.md → common/boot.md} +0 -0
  155. /package/template/{.mcp.json → cep/.mcp.json} +0 -0
  156. /package/template/{gitignore → cep/gitignore} +0 -0
  157. /package/template/{package.json → cep/package.json} +0 -0
  158. /package/template/{src → cep/src}/features/home/home.css +0 -0
  159. /package/template/{src → cep/src}/features/license-screen/license-screen.css +0 -0
  160. /package/template/{src → cep/src}/features/license-screen/license-screen.html +0 -0
  161. /package/template/{src → cep/src}/features/license-screen/license-screen.js +0 -0
  162. /package/template/{src → cep/src}/features/update-screen/update-screen.css +0 -0
  163. /package/template/{src → cep/src}/features/update-screen/update-screen.html +0 -0
  164. /package/template/{src → cep/src}/features/update-screen/update-screen.js +0 -0
  165. /package/template/{src → cep/src}/panels/main/index.html +0 -0
  166. /package/template/{src → cep/src}/panels/main/main.js +0 -0
  167. /package/template/{src → cep/src}/shared/lib/dom.js +0 -0
  168. /package/template/{src → cep/src}/shared/lib/markdown.js +0 -0
  169. /package/template/{src → cep/src}/shared/ui/base.css +0 -0
  170. /package/template/{src → cep/src}/shared/ui/button.css +0 -0
  171. /package/template/{src → cep/src}/shared/ui/card.css +0 -0
  172. /package/template/{src → cep/src}/shared/ui/field.css +0 -0
  173. /package/template/{src → cep/src}/shared/ui/index.css +0 -0
@@ -0,0 +1,909 @@
1
+ #include "engine.hpp"
2
+
3
+ #include <algorithm>
4
+ #include <climits>
5
+ #include <cmath>
6
+ #include <regex>
7
+
8
+ #include "util.hpp"
9
+ #include "zip.hpp"
10
+
11
+ namespace apn {
12
+
13
+ namespace {
14
+
15
+ HttpResponse no_client() {
16
+ HttpResponse r;
17
+ r.network_error = true;
18
+ r.error = "no network client";
19
+ return r;
20
+ }
21
+
22
+ const char *const PROOF_PREFIX = "afterpresets-license-v1";
23
+ const char *const RELEASE_PREFIX = "afterpresets-release-v1";
24
+
25
+ std::string default_message(Status s) {
26
+ switch (s) {
27
+ case Status::Disabled: return "Licensing is off for this plugin.";
28
+ case Status::SignedOut: return "Sign in with your AfterPresets account to unlock this plugin.";
29
+ case Status::SigningIn: return "Finish signing in in your browser.";
30
+ case Status::Checking: return "Checking your license\xE2\x80\xA6";
31
+ case Status::Active: return "License active.";
32
+ case Status::NotOwned: return "This AfterPresets account does not own this plugin yet.";
33
+ case Status::Refunded: return "This purchase was refunded, so the license is no longer active.";
34
+ case Status::OfflineGrace:
35
+ return "You are offline. The plugin keeps working for a limited time; reconnect to verify your license.";
36
+ case Status::OfflineExpired: return "Connect to the internet to verify your license.";
37
+ case Status::Error: return "Something went wrong while checking your license.";
38
+ }
39
+ return "";
40
+ }
41
+
42
+ bool is_rejection_code(const std::string &code) {
43
+ return code == "invalid_token" || code == "not_owned" || code == "refunded" || code == "unknown_product";
44
+ }
45
+
46
+ std::string lower(std::string s) {
47
+ for (auto &c : s) c = (char)std::tolower((unsigned char)c);
48
+ return s;
49
+ }
50
+
51
+ std::string truncate(const std::string &s, size_t n) { return s.size() <= n ? s : s.substr(0, n); }
52
+
53
+ // Same rule as the JS runtime's timestampOf for strings: Date.parse.
54
+ std::optional<int64_t> timestamp_of(const Json &v) {
55
+ if (v.is_number() && std::isfinite(v.as_number())) return (int64_t)v.as_number();
56
+ if (v.is_string() && !v.as_string().empty()) return parse_iso8601(v.as_string());
57
+ return std::nullopt;
58
+ }
59
+
60
+ } // namespace
61
+
62
+ const char *status_name(Status s) {
63
+ switch (s) {
64
+ case Status::Disabled: return "disabled";
65
+ case Status::SignedOut: return "signed_out";
66
+ case Status::SigningIn: return "signing_in";
67
+ case Status::Checking: return "checking";
68
+ case Status::Active: return "active";
69
+ case Status::NotOwned: return "not_owned";
70
+ case Status::Refunded: return "refunded";
71
+ case Status::OfflineGrace: return "offline_grace";
72
+ case Status::OfflineExpired: return "offline_expired";
73
+ case Status::Error: return "error";
74
+ }
75
+ return "error";
76
+ }
77
+
78
+ const char *upd_status_name(UpdStatus s) {
79
+ switch (s) {
80
+ case UpdStatus::Idle: return "idle";
81
+ case UpdStatus::Checking: return "checking";
82
+ case UpdStatus::UpToDate: return "up_to_date";
83
+ case UpdStatus::Available: return "available";
84
+ case UpdStatus::Downloading: return "downloading";
85
+ case UpdStatus::Staged: return "staged";
86
+ case UpdStatus::Applied: return "applied";
87
+ case UpdStatus::Error: return "error";
88
+ }
89
+ return "error";
90
+ }
91
+
92
+ bool is_runnable(Status s) { return s == Status::Active || s == Status::OfflineGrace || s == Status::Disabled; }
93
+
94
+ Paths Paths::make(const std::string &data_root, const std::string &bundle_id) {
95
+ Paths p;
96
+ p.data_dir = fsu::join(fsu::join(fsu::join(data_root, "AfterPresets"), "extensions"), bundle_id);
97
+ p.license = fsu::join(p.data_dir, "license.json");
98
+ p.updates = fsu::join(p.data_dir, "updates.json");
99
+ p.lock = fsu::join(p.data_dir, "update.lock");
100
+ p.updates_dir = fsu::join(p.data_dir, "updates");
101
+ p.staged = fsu::join(p.updates_dir, "staged");
102
+ p.staged_meta = fsu::join(p.updates_dir, "staged.json");
103
+ return p;
104
+ }
105
+
106
+ Engine::Engine(Settings settings, Env env, std::function<void()> on_change)
107
+ : s_(std::move(settings)), env_(std::move(env)), paths_(Paths::make(env_.data_root, s_.bundle_id)),
108
+ on_change_(std::move(on_change)) {
109
+ license_.status = s_.license_enabled ? Status::Checking : Status::Disabled;
110
+ license_.message = default_message(license_.status);
111
+ if (s_.license_enabled) {
112
+ cache_stamp_ = fsu::stamp(paths_.license);
113
+ watch_at_ = now() + CACHE_WATCH_MS;
114
+ }
115
+ // An update staged by an earlier session that was never applied.
116
+ auto meta_text = fsu::read(paths_.staged_meta, 1 << 20);
117
+ auto meta = meta_text ? Json::parse(*meta_text) : std::nullopt;
118
+ if (meta && meta->is_object() && fsu::is_dir(paths_.staged)) {
119
+ std::string v = (*meta)["version"].str();
120
+ if (v == s_.version) {
121
+ // Already running it (applied, then After Effects restarted).
122
+ fsu::remove_all(paths_.staged);
123
+ fsu::remove_file(paths_.staged_meta);
124
+ } else if (!v.empty()) {
125
+ update_.status = UpdStatus::Staged;
126
+ update_.version = v;
127
+ update_.direction = (*meta)["direction"].str("upgrade");
128
+ update_.change_log = truncate((*meta)["changeLog"].str(), 8000);
129
+ }
130
+ }
131
+ schedule_first_update_check();
132
+ }
133
+
134
+ void Engine::log(int level, const std::string &message) {
135
+ if (env_.log) env_.log(level, message);
136
+ }
137
+
138
+ std::string Engine::computer() {
139
+ if (computer_id_.empty()) computer_id_ = computer_id(env_);
140
+ return computer_id_;
141
+ }
142
+
143
+ void Engine::set_state(Status status, const std::string &message, int grace_days, const std::string &url) {
144
+ LicenseView next;
145
+ next.status = status;
146
+ next.message = message.empty() ? default_message(status) : message;
147
+ next.grace_days_left = grace_days;
148
+ next.sign_in_url = url;
149
+ next.username = username_;
150
+ if (next == license_) return;
151
+ license_ = next;
152
+ if (on_change_) on_change_();
153
+ }
154
+
155
+ void Engine::set_update(const UpdateView &view) {
156
+ if (view == update_) return;
157
+ update_ = view;
158
+ if (on_change_) on_change_();
159
+ }
160
+
161
+ ApiResult Engine::api(const std::string &suffix, const Json &body) {
162
+ ApiResult out;
163
+ std::string base = s_.api_base;
164
+ while (!base.empty() && base.back() == '/') base.pop_back();
165
+ HttpRequest req;
166
+ req.method = "POST";
167
+ req.url = base + "/v1/api/extensions/" + s_.bundle_id + suffix;
168
+ req.body = body.dump();
169
+ req.headers.emplace_back("Content-Type", "application/json");
170
+ static const std::atomic<bool> never{false};
171
+ HttpResponse res = env_.http ? env_.http->send(req, cancel_ ? *cancel_ : never) : no_client();
172
+ if (res.network_error) {
173
+ // The status says "could not reach"; the log keeps the cause (no body, no token, no URL).
174
+ log(2, "Request to " + suffix + " failed: " + res.error);
175
+ out.kind = ApiResult::Network;
176
+ out.message = res.error;
177
+ return out;
178
+ }
179
+ out.status = res.status;
180
+ std::optional<Json> json;
181
+ if (res.content_type.find("json") != std::string::npos || (!res.body.empty() && res.body[0] == '{')) {
182
+ json = Json::parse(res.body);
183
+ }
184
+ bool has_json = json && !json->is_null() && !(json->is_bool() && !json->as_bool());
185
+ if (res.status >= 200 && res.status < 300 && has_json) {
186
+ out.kind = ApiResult::Ok;
187
+ out.json = *json;
188
+ return out;
189
+ }
190
+ std::string code;
191
+ std::string message;
192
+ if (json) {
193
+ const Json &e = (*json)["error"];
194
+ if (e.is_object()) {
195
+ code = e["code"].str();
196
+ message = e["message"].str();
197
+ } else if (e.is_string()) {
198
+ code = e.as_string();
199
+ }
200
+ }
201
+ if (res.status >= 400 && res.status < 500 && res.status != 408 && res.status != 429 && !code.empty()) {
202
+ out.kind = ApiResult::Rejected;
203
+ out.code = code;
204
+ out.message = message.empty() ? code : message;
205
+ if (json) out.json = *json;
206
+ return out;
207
+ }
208
+ out.kind = ApiResult::Network;
209
+ out.message = "Server unavailable (HTTP " + std::to_string(res.status) + ").";
210
+ return out;
211
+ }
212
+
213
+ // ---------------------------------------------------------------- cache
214
+
215
+ std::optional<Json> Engine::read_cache() {
216
+ auto text = fsu::read(paths_.license, 1 << 20);
217
+ if (!text) return std::nullopt;
218
+ auto data = Json::parse(*text);
219
+ if (!data || !data->is_object()) {
220
+ log(2, "Could not read license.json");
221
+ return std::nullopt;
222
+ }
223
+ const Json &token = (*data)["token"];
224
+ if (!token.is_string() || token.as_string().empty()) return std::nullopt;
225
+ return data;
226
+ }
227
+
228
+ bool Engine::write_cache(const Json &cache) {
229
+ bool ok = fsu::write_atomic(paths_.license, cache.dump(true), true);
230
+ if (!ok) log(3, "Could not save license.json");
231
+ remember_cache_stamp();
232
+ return ok;
233
+ }
234
+
235
+ void Engine::delete_cache() {
236
+ if (fsu::exists(paths_.license) && !fsu::remove_file(paths_.license)) log(2, "Could not delete license cache");
237
+ remember_cache_stamp();
238
+ }
239
+
240
+ void Engine::remember_cache_stamp() { cache_stamp_ = fsu::stamp(paths_.license); }
241
+
242
+ std::optional<std::string> Engine::stored_token() {
243
+ auto cache = read_cache();
244
+ if (!cache) return std::nullopt;
245
+ return (*cache)["token"].as_string();
246
+ }
247
+
248
+ Json Engine::token_body(const std::string &token) {
249
+ Json body = Json::object();
250
+ body.set("license_token", Json::string(token));
251
+ body.set("computer_id", Json::string(computer()));
252
+ return body;
253
+ }
254
+
255
+ // ---------------------------------------------------------------- proofs
256
+
257
+ std::optional<Engine::Check> Engine::trusted_check(const Json &cache) {
258
+ if (!s_.public_key) {
259
+ // Cannot verify (no key in this build): unsigned validatedAt for grace, never skip the online check.
260
+ auto legacy = timestamp_of(cache["validatedAt"]);
261
+ if (!legacy) return std::nullopt;
262
+ return Check{*legacy, std::nullopt};
263
+ }
264
+ const Json &proof = cache["proof"];
265
+ if (!proof.is_object() || !proof["signature"].is_string() || !proof["validatedAt"].is_string() ||
266
+ !proof["freshUntil"].is_string()) {
267
+ return std::nullopt;
268
+ }
269
+ auto validated_at = timestamp_of(proof["validatedAt"]);
270
+ auto fresh_until = timestamp_of(proof["freshUntil"]);
271
+ if (!validated_at || !fresh_until) return std::nullopt;
272
+ std::string payload = std::string(PROOF_PREFIX) + "\n" + s_.bundle_id + "\n" + computer() + "\n" +
273
+ sha256_hex(cache["token"].as_string()) + "\n" + proof["validatedAt"].as_string() + "\n" +
274
+ proof["freshUntil"].as_string();
275
+ if (!ed25519_verify(*s_.public_key, payload, proof["signature"].as_string())) return std::nullopt;
276
+ return Check{*validated_at, *fresh_until};
277
+ }
278
+
279
+ bool Engine::is_fresh(const std::optional<Check> &check) {
280
+ if (!check || !check->fresh_until) return false;
281
+ int64_t t = now();
282
+ return t >= check->validated_at - CLOCK_SKEW_MS && t < *check->fresh_until;
283
+ }
284
+
285
+ std::optional<int> Engine::grace_for(const Json &cache) {
286
+ auto check = trusted_check(cache);
287
+ if (!check) return std::nullopt;
288
+ int64_t elapsed = now() - check->validated_at;
289
+ int64_t limit = (int64_t)s_.grace_days * DAY_MS;
290
+ if (s_.grace_days <= 0 || elapsed < -CLOCK_SKEW_MS || elapsed >= limit) return std::nullopt;
291
+ int64_t remaining = limit - std::max<int64_t>(elapsed, 0);
292
+ int left = (int)((remaining + DAY_MS - 1) / DAY_MS);
293
+ return std::max(1, std::min(s_.grace_days, left));
294
+ }
295
+
296
+ // ---------------------------------------------------------------- outcomes
297
+
298
+ void Engine::apply_network_failure(const Json &cache, const std::string &error) {
299
+ auto days = grace_for(cache);
300
+ if (days) {
301
+ set_state(Status::OfflineGrace,
302
+ "You are offline. " + std::to_string(*days) + (*days == 1 ? " day" : " days") +
303
+ " left before this plugin needs to verify your license online.",
304
+ *days);
305
+ } else {
306
+ set_state(Status::OfflineExpired,
307
+ default_message(Status::OfflineExpired) + (error.empty() ? "" : " (" + error + ")"));
308
+ }
309
+ }
310
+
311
+ void Engine::apply_rejection(const ApiResult &err, const std::optional<Json> &cache) {
312
+ clear_recheck();
313
+ if (err.code == "invalid_token") {
314
+ delete_cache();
315
+ username_.clear();
316
+ set_state(Status::SignedOut, "This computer was signed out. Sign in again to keep using this plugin.");
317
+ return;
318
+ }
319
+ if (is_rejection_code(err.code) && cache) {
320
+ Json kept = Json::object();
321
+ kept.set("token", (*cache)["token"]);
322
+ kept.set("validatedAt", Json::null());
323
+ const Json &user = (*cache)["username"];
324
+ kept.set("username", user.is_string() ? user : Json::null());
325
+ kept.set("proof", Json::null());
326
+ write_cache(kept);
327
+ }
328
+ if (err.code == "not_owned") {
329
+ set_state(Status::NotOwned, err.message);
330
+ } else if (err.code == "refunded") {
331
+ set_state(Status::Refunded, err.message);
332
+ } else if (err.code == "unknown_product") {
333
+ set_state(Status::Error, "This plugin is not registered with AfterPresets (" + err.message + ").");
334
+ } else {
335
+ set_state(Status::Error, err.message.empty() ? default_message(Status::Error) : err.message);
336
+ }
337
+ }
338
+
339
+ void Engine::on_active(const std::string &token, const Json &json) {
340
+ if (!json.is_object() || json["status"].str() != "active") {
341
+ set_state(Status::Error, "Unexpected license response.");
342
+ return;
343
+ }
344
+ const Json &user = json["user"];
345
+ username_ = user.is_object() && user["username"].is_string() ? user["username"].as_string() : "";
346
+ Json cache = Json::object();
347
+ cache.set("token", Json::string(token));
348
+ cache.set("validatedAt", Json::string(format_iso8601(now())));
349
+ cache.set("username", username_.empty() ? Json::null() : Json::string(username_));
350
+ const Json &proof = json["proof"];
351
+ if (proof.is_object()) {
352
+ Json p = Json::object();
353
+ p.set("validatedAt", Json::string(proof["validatedAt"].str()));
354
+ p.set("freshUntil", Json::string(proof["freshUntil"].str()));
355
+ p.set("signature", Json::string(proof["signature"].str()));
356
+ cache.set("proof", p);
357
+ } else {
358
+ cache.set("proof", Json::null());
359
+ }
360
+ write_cache(cache);
361
+ set_state(Status::Active);
362
+ schedule_recheck(trusted_check(cache));
363
+ if (waiting_for_sign_in_ && s_.update_mode != UpdateMode::Off && s_.update_mode != UpdateMode::Manual) {
364
+ waiting_for_sign_in_ = false;
365
+ update_at_ = now() + 5000;
366
+ }
367
+ }
368
+
369
+ void Engine::unlock_from_cache(const Json &cache) {
370
+ const Json &user = cache["username"];
371
+ username_ = user.is_string() ? user.as_string() : "";
372
+ set_state(Status::Active);
373
+ }
374
+
375
+ void Engine::schedule_recheck(const std::optional<Check> &check, int64_t delay) {
376
+ clear_recheck();
377
+ if (!s_.license_enabled) return;
378
+ if (delay == -2) delay = check && check->fresh_until ? *check->fresh_until - now() : -1;
379
+ if (delay < 0) return;
380
+ recheck_at_ = now() + std::max<int64_t>(delay, 1000);
381
+ }
382
+
383
+ // ---------------------------------------------------------------- license flows
384
+
385
+ Engine::Launch Engine::launch_local() {
386
+ if (!s_.license_enabled) {
387
+ set_state(Status::Disabled);
388
+ return Launch::None;
389
+ }
390
+ if (pending_.active) return Launch::None;
391
+ auto cache = read_cache();
392
+ std::optional<Check> check = cache ? trusted_check(*cache) : std::nullopt;
393
+ if (!s_.background || !check || !check->fresh_until || is_fresh(check)) {
394
+ if (!cache) {
395
+ clear_recheck();
396
+ set_state(Status::SignedOut);
397
+ return Launch::None;
398
+ }
399
+ if (is_fresh(check)) {
400
+ unlock_from_cache(*cache);
401
+ schedule_recheck(check);
402
+ return Launch::None;
403
+ }
404
+ set_state(Status::Checking);
405
+ return Launch::Validate;
406
+ }
407
+ unlock_from_cache(*cache);
408
+ launch_token_ = (*cache)["token"].as_string();
409
+ return Launch::ValidateBehind;
410
+ }
411
+
412
+ void Engine::validate_stored() {
413
+ if (!s_.license_enabled || pending_.active) return;
414
+ auto cache = read_cache();
415
+ if (!cache) {
416
+ clear_recheck();
417
+ set_state(Status::SignedOut);
418
+ return;
419
+ }
420
+ auto check = trusted_check(*cache);
421
+ if (is_fresh(check)) {
422
+ unlock_from_cache(*cache);
423
+ schedule_recheck(check);
424
+ return;
425
+ }
426
+ set_state(Status::Checking);
427
+ std::string token = (*cache)["token"].as_string();
428
+ ApiResult r = api("/license/validate", token_body(token));
429
+ if (r.kind == ApiResult::Ok) on_active(token, r.json);
430
+ else if (r.kind == ApiResult::Network) apply_network_failure(*cache, r.message);
431
+ else apply_rejection(r, cache);
432
+ }
433
+
434
+ void Engine::validate_behind() {
435
+ if (!s_.license_enabled || pending_.active) return;
436
+ auto cache = read_cache();
437
+ if (!cache || launch_token_.empty()) return;
438
+ std::string token = launch_token_;
439
+ if ((*cache)["token"].as_string() != token) return; // signed in or out elsewhere meanwhile
440
+ ApiResult r = api("/license/validate", token_body(token));
441
+ // Ignore a result that arrives after the buyer signed out or in elsewhere.
442
+ auto current = read_cache();
443
+ if (pending_.active || license_.status != Status::Active || !current || (*current)["token"].as_string() != token) {
444
+ return;
445
+ }
446
+ if (r.kind == ApiResult::Ok) on_active(token, r.json);
447
+ else if (r.kind == ApiResult::Network) apply_network_failure(*cache, r.message);
448
+ else apply_rejection(r, cache);
449
+ }
450
+
451
+ void Engine::recheck_quietly() {
452
+ if (!s_.license_enabled || pending_.active || license_.status != Status::Active) return;
453
+ auto cache = read_cache();
454
+ if (!cache) {
455
+ set_state(Status::SignedOut);
456
+ return;
457
+ }
458
+ std::string token = (*cache)["token"].as_string();
459
+ ApiResult r = api("/license/validate", token_body(token));
460
+ if (r.kind == ApiResult::Ok) on_active(token, r.json);
461
+ else if (r.kind == ApiResult::Network) schedule_recheck(std::nullopt, QUIET_RETRY_MS);
462
+ else apply_rejection(r, cache);
463
+ }
464
+
465
+ void Engine::sign_in() {
466
+ if (!s_.license_enabled) {
467
+ log(2, "Licensing is off for this plugin; sign-in was ignored.");
468
+ return;
469
+ }
470
+ if (pending_.active) return;
471
+ set_state(Status::Checking, "Starting sign-in\xE2\x80\xA6");
472
+ Json body = Json::object();
473
+ body.set("computer_id", Json::string(computer()));
474
+ ApiResult r = api("/license/sign-in", body);
475
+ const Json &j = r.json;
476
+ if (r.kind != ApiResult::Ok || !j["deviceCode"].is_string() || j["deviceCode"].as_string().empty() ||
477
+ !j["verificationUrl"].is_string() || !is_http_url(j["verificationUrl"].as_string())) {
478
+ if (r.kind == ApiResult::Network) {
479
+ set_state(Status::Error, "Could not reach AfterPresets. Check your internet connection and try again.");
480
+ } else if (r.kind == ApiResult::Rejected && r.code == "unknown_product") {
481
+ set_state(Status::Error, "This plugin is not registered with AfterPresets (" + r.message + ").");
482
+ } else {
483
+ set_state(Status::Error, r.kind == ApiResult::Rejected && !r.message.empty() ? r.message : "Could not start sign-in.");
484
+ }
485
+ return;
486
+ }
487
+ double expires_in = j["expiresIn"].as_number(0);
488
+ double interval = j["interval"].as_number(0);
489
+ if (!(expires_in > 0)) expires_in = 600;
490
+ if (!(interval > 0)) interval = 3;
491
+ pending_.active = true;
492
+ pending_.device_code = j["deviceCode"].as_string();
493
+ pending_.url = j["verificationUrl"].as_string();
494
+ pending_.expires_at = now() + (int64_t)(std::max(expires_in, 1.0) * 1000);
495
+ pending_.interval_ms = std::max<int64_t>((int64_t)(interval * 1000), MIN_POLL_MS);
496
+ pending_.poll_at = now() + pending_.interval_ms;
497
+ set_state(Status::SigningIn, "", 0, pending_.url);
498
+ if (env_.open_url && !env_.open_url(pending_.url)) log(2, "Could not open the browser for sign-in.");
499
+ }
500
+
501
+ void Engine::sign_in_failed(const std::string &message) {
502
+ stop_polling();
503
+ set_state(Status::SignedOut, message);
504
+ }
505
+
506
+ void Engine::poll_sign_in() {
507
+ if (!pending_.active) return;
508
+ if (now() >= pending_.expires_at) {
509
+ sign_in_failed("The sign-in expired. Click Sign in to try again.");
510
+ return;
511
+ }
512
+ std::string device_code = pending_.device_code;
513
+ pending_.poll_at = INT64_MAX; // in flight
514
+ Json body = Json::object();
515
+ body.set("deviceCode", Json::string(device_code));
516
+ ApiResult r = api("/license/sign-in/poll", body);
517
+ if (!pending_.active || pending_.device_code != device_code) return;
518
+ if (r.kind == ApiResult::Ok) {
519
+ const Json &token = r.json["token"];
520
+ if (!token.is_string() || token.as_string().empty() || token.as_string().size() > 255) {
521
+ sign_in_failed("Unexpected sign-in response. Try again.");
522
+ return;
523
+ }
524
+ stop_polling();
525
+ on_active(token.as_string(), r.json);
526
+ return;
527
+ }
528
+ if (r.kind == ApiResult::Network || r.code == "authorization_pending") {
529
+ pending_.poll_at = now() + pending_.interval_ms;
530
+ return;
531
+ }
532
+ if (r.code == "expired_token") {
533
+ sign_in_failed("The sign-in was cancelled or expired. Click Sign in to try again.");
534
+ return;
535
+ }
536
+ sign_in_failed(r.message.empty() ? default_message(Status::Error) : r.message);
537
+ }
538
+
539
+ void Engine::cancel_sign_in() {
540
+ if (!s_.license_enabled || !pending_.active) return;
541
+ stop_polling();
542
+ set_state(Status::SignedOut);
543
+ }
544
+
545
+ void Engine::open_sign_in_page() {
546
+ if (pending_.active && !pending_.url.empty() && env_.open_url) env_.open_url(pending_.url);
547
+ }
548
+
549
+ void Engine::sign_out() {
550
+ if (!s_.license_enabled) {
551
+ log(2, "Licensing is off for this plugin; sign-out was ignored.");
552
+ return;
553
+ }
554
+ stop_polling();
555
+ clear_recheck();
556
+ auto cache = read_cache();
557
+ delete_cache();
558
+ username_.clear();
559
+ set_state(Status::SignedOut);
560
+ if (cache) api("/license/sign-out", token_body((*cache)["token"].as_string())); // best effort
561
+ }
562
+
563
+ void Engine::check_cache_file() {
564
+ if (!s_.license_enabled) return;
565
+ auto st = fsu::stamp(paths_.license);
566
+ if (st.has_value() == cache_stamp_.has_value() && (!st || *st == *cache_stamp_)) return;
567
+ cache_stamp_ = st;
568
+ auto cache = read_cache();
569
+ // Another process (another After Effects, aerender, a second plugin of this bundle) signed in or out.
570
+ if (cache && is_runnable(license_.status)) return;
571
+ if (cache) stop_polling(); // the other process finished first
572
+ validate_stored();
573
+ }
574
+
575
+ // ---------------------------------------------------------------- updates
576
+
577
+ Json Engine::read_update_settings() {
578
+ auto text = fsu::read(paths_.updates, 1 << 20);
579
+ auto json = text ? Json::parse(*text) : std::nullopt;
580
+ return json && json->is_object() ? *json : Json::object();
581
+ }
582
+
583
+ void Engine::write_update_settings(const Json &settings) {
584
+ if (!fsu::write_atomic(paths_.updates, settings.dump(true), false)) log(2, "Could not save updates.json");
585
+ }
586
+
587
+ void Engine::schedule_first_update_check() {
588
+ if (s_.update_mode != UpdateMode::Notify && s_.update_mode != UpdateMode::Background) return;
589
+ int64_t at = now() + s_.first_update_check_delay_ms;
590
+ // Several launches within 6 hours share one check, unless the last one found something.
591
+ Json st = read_update_settings();
592
+ auto last = timestamp_of(st["lastCheckAt"]);
593
+ if (last && st["lastResult"].str() == "up_to_date" && *last <= now() && *last + UPDATE_INTERVAL_MS > at) {
594
+ at = *last + UPDATE_INTERVAL_MS;
595
+ }
596
+ update_at_ = at;
597
+ }
598
+
599
+ bool Engine::acquire_lock() {
600
+ for (int attempt = 0; attempt < 2; attempt++) {
601
+ auto r = fsu::write_exclusive(paths_.lock, std::to_string(now()));
602
+ if (r == fsu::Excl::Ok) { lock_held_ = true; return true; }
603
+ if (r == fsu::Excl::Error) { log(2, "Update lock unavailable"); return false; }
604
+ int64_t age = fsu::age_ms(paths_.lock, now());
605
+ if (age >= 0 && age <= LOCK_STALE_MS) return false;
606
+ fsu::remove_file(paths_.lock);
607
+ }
608
+ return false;
609
+ }
610
+
611
+ void Engine::release_lock() {
612
+ if (lock_held_) fsu::remove_file(paths_.lock);
613
+ lock_held_ = false;
614
+ }
615
+
616
+ void Engine::check_updates(bool automatic) {
617
+ if (s_.update_mode == UpdateMode::Off) return;
618
+ if (automatic && s_.update_mode == UpdateMode::Manual) return;
619
+ if (update_.status == UpdStatus::Downloading) return;
620
+ UpdateView view = update_;
621
+ auto fail = [&](const std::string &code, const std::string &message) {
622
+ UpdateView v = update_;
623
+ // A staged or applied update stays visible; the error is reported alongside.
624
+ if (v.status != UpdStatus::Staged && v.status != UpdStatus::Applied) v.status = UpdStatus::Error;
625
+ v.error_code = code;
626
+ v.error_message = message;
627
+ set_update(v);
628
+ };
629
+ if (view.status != UpdStatus::Staged && view.status != UpdStatus::Applied) {
630
+ view.status = UpdStatus::Checking;
631
+ set_update(view);
632
+ }
633
+
634
+ Json body = Json::object();
635
+ body.set("currentVersion", Json::string(s_.version));
636
+ body.set("channel", Json::string(s_.channel));
637
+ // Native additions (ignored by the current API; see docs/native-runtime-design.md, "API additions").
638
+ body.set("platform", Json::string("native"));
639
+ body.set("os", Json::string(s_.os));
640
+ body.set("arch", Json::string(s_.arch));
641
+ if (s_.license_enabled) {
642
+ auto token = stored_token();
643
+ if (!token) {
644
+ waiting_for_sign_in_ = automatic;
645
+ fail("no_license", "Sign in to check for updates.");
646
+ return;
647
+ }
648
+ body.set("license_token", Json::string(*token));
649
+ body.set("computer_id", Json::string(computer()));
650
+ }
651
+ ApiResult r = api("/updates/check", body);
652
+
653
+ Json st = read_update_settings();
654
+ st.set("lastCheckAt", Json::string(format_iso8601(now())));
655
+ if (r.kind != ApiResult::Ok) {
656
+ st.set("lastResult", Json::string("error"));
657
+ write_update_settings(st);
658
+ if (s_.license_enabled && r.kind == ApiResult::Rejected && is_rejection_code(r.code)) {
659
+ apply_rejection(r, read_cache());
660
+ }
661
+ fail(r.kind == ApiResult::Network ? "network" : r.code, r.message);
662
+ return;
663
+ }
664
+ const Json &j = r.json;
665
+ if (!j["updateAvailable"].as_bool()) {
666
+ st.set("lastResult", Json::string("up_to_date"));
667
+ write_update_settings(st);
668
+ available_.reset();
669
+ if (update_.status != UpdStatus::Staged && update_.status != UpdStatus::Applied) {
670
+ UpdateView v;
671
+ v.status = UpdStatus::UpToDate;
672
+ set_update(v);
673
+ }
674
+ return;
675
+ }
676
+ st.set("lastResult", Json::string("available"));
677
+ write_update_settings(st);
678
+
679
+ UpdateInfo info;
680
+ info.version = j["targetVersion"].str(j["version"].str());
681
+ info.direction = j["direction"].str() == "downgrade" ? "downgrade" : "upgrade";
682
+ info.change_log = j["changeLog"].str();
683
+ info.download_url = j["downloadUrl"].str();
684
+ info.sha256 = lower(j["sha256"].str());
685
+ info.signature = j["signature"].str();
686
+ static const std::regex version_re("^[0-9A-Za-z.+-]{1,64}$");
687
+ static const std::regex sha_re("^[0-9a-f]{64}$");
688
+ if (!is_http_url(info.download_url) || !std::regex_match(info.sha256, sha_re) || info.signature.empty() ||
689
+ !std::regex_match(info.version, version_re)) {
690
+ fail("bad_update_info", "The update information is incomplete.");
691
+ return;
692
+ }
693
+ if ((update_.status == UpdStatus::Staged || update_.status == UpdStatus::Applied) && update_.version == info.version) {
694
+ return; // already staged or applied; waiting for a restart
695
+ }
696
+ available_ = info;
697
+ UpdateView v;
698
+ v.status = UpdStatus::Available;
699
+ v.version = info.version;
700
+ v.direction = info.direction;
701
+ v.change_log = truncate(info.change_log, 8000);
702
+ set_update(v);
703
+ if (s_.update_mode != UpdateMode::Background || s_.dev) return;
704
+ if (automatic && failed_version_ == info.version) return;
705
+ download_update();
706
+ }
707
+
708
+ void Engine::download_update() {
709
+ if (!available_) return;
710
+ UpdateInfo info = *available_;
711
+ std::string code, message;
712
+ if (stage(info, code, message)) {
713
+ available_.reset();
714
+ UpdateView v;
715
+ v.status = UpdStatus::Staged;
716
+ v.version = info.version;
717
+ v.direction = info.direction;
718
+ v.change_log = truncate(info.change_log, 8000);
719
+ set_update(v);
720
+ if (s_.apply_when_staged) apply_now();
721
+ return;
722
+ }
723
+ if (code != "busy") failed_version_ = info.version;
724
+ UpdateView v = update_;
725
+ v.status = UpdStatus::Error;
726
+ v.error_code = code;
727
+ v.error_message = message;
728
+ set_update(v);
729
+ }
730
+
731
+ bool Engine::stage(const UpdateInfo &info, std::string &code, std::string &message) {
732
+ if (s_.dev) {
733
+ code = "dev_build";
734
+ message = "Updates are not installed in dev builds.";
735
+ return false;
736
+ }
737
+ if (!s_.public_key) {
738
+ code = "no_public_key";
739
+ message = "This build has no update signing key.";
740
+ return false;
741
+ }
742
+ // 1. The signature covers bundle id, version and checksum: verify before downloading anything.
743
+ std::string payload = std::string(RELEASE_PREFIX) + "\n" + s_.bundle_id + "\n" + info.version + "\n" + info.sha256;
744
+ if (!ed25519_verify(*s_.public_key, payload, info.signature)) {
745
+ code = "bad_signature";
746
+ message = "The update signature is not valid for this plugin. The update was not installed.";
747
+ return false;
748
+ }
749
+ if (!acquire_lock()) {
750
+ code = "busy";
751
+ message = "Another After Effects is installing this update.";
752
+ return false;
753
+ }
754
+ UpdateView v = update_;
755
+ v.status = UpdStatus::Downloading;
756
+ v.error_code.clear();
757
+ v.error_message.clear();
758
+ set_update(v);
759
+
760
+ struct Cleanup {
761
+ Engine *e;
762
+ std::string download, staging;
763
+ ~Cleanup() {
764
+ if (!download.empty()) fsu::remove_file(download);
765
+ if (!staging.empty()) fsu::remove_all(staging);
766
+ e->release_lock();
767
+ }
768
+ } cleanup{this, "", ""};
769
+
770
+ fsu::mkdirs(paths_.updates_dir);
771
+ std::string rand = env_.random_hex ? env_.random_hex(6) : std::to_string(now());
772
+ cleanup.download = fsu::join(paths_.updates_dir, "download-" + rand + ".zip");
773
+ HttpRequest req;
774
+ req.method = "GET";
775
+ req.url = info.download_url;
776
+ req.download_path = cleanup.download;
777
+ req.max_bytes = ZipLimits::MAX_TOTAL_BYTES + 16ULL * 1024 * 1024; // the server caps releases at 400 MB unpacked
778
+ req.timeout_ms = 30 * 60 * 1000;
779
+ req.follow_redirects = true;
780
+ static const std::atomic<bool> never{false};
781
+ HttpResponse res = env_.http ? env_.http->send(req, cancel_ ? *cancel_ : never) : no_client();
782
+ if (res.network_error) {
783
+ code = "download_failed";
784
+ message = "Download failed: " + res.error;
785
+ return false;
786
+ }
787
+ if (res.status != 200) {
788
+ code = "download_failed";
789
+ message = "Download failed (HTTP " + std::to_string(res.status) + ").";
790
+ return false;
791
+ }
792
+ // 2. The file is the one that was signed.
793
+ auto sha = sha256_file_hex(cleanup.download);
794
+ if (!sha || !equal_ct(*sha, info.sha256)) {
795
+ code = "bad_checksum";
796
+ message = "The downloaded update is corrupted (checksum mismatch). Nothing was installed.";
797
+ return false;
798
+ }
799
+ auto archive = fsu::read(cleanup.download, req.max_bytes);
800
+ if (!archive) {
801
+ code = "install_failed";
802
+ message = "The downloaded update could not be read.";
803
+ return false;
804
+ }
805
+ // 3. Only this OS's plugin is extracted, every entry validated first.
806
+ cleanup.staging = fsu::join(paths_.updates_dir, "staging-" + rand);
807
+ ZipError zerr;
808
+ std::string plugin_name;
809
+ if (!zip_extract_plugin(*archive, s_.os, cleanup.staging, plugin_name, zerr)) {
810
+ code = "bad_zip";
811
+ message = zerr.message;
812
+ return false;
813
+ }
814
+ fsu::remove_all(paths_.staged);
815
+ fsu::remove_file(paths_.staged_meta);
816
+ if (!fsu::rename(cleanup.staging, paths_.staged)) {
817
+ code = "install_failed";
818
+ message = "The update could not be staged.";
819
+ return false;
820
+ }
821
+ cleanup.staging.clear();
822
+ Json meta = Json::object();
823
+ meta.set("version", Json::string(info.version));
824
+ meta.set("sha256", Json::string(info.sha256));
825
+ meta.set("plugin", Json::string(plugin_name));
826
+ meta.set("direction", Json::string(info.direction));
827
+ meta.set("changeLog", Json::string(truncate(info.change_log, 8000)));
828
+ meta.set("stagedAt", Json::string(format_iso8601(now())));
829
+ if (!fsu::write_atomic(paths_.staged_meta, meta.dump(true), false)) {
830
+ fsu::remove_all(paths_.staged);
831
+ code = "install_failed";
832
+ message = "The update could not be staged.";
833
+ return false;
834
+ }
835
+ log(1, "Update " + info.version + " verified and staged.");
836
+ return true;
837
+ }
838
+
839
+ void Engine::mark_applied(const std::string &version) {
840
+ UpdateView v = update_;
841
+ v.status = UpdStatus::Applied;
842
+ v.restart_required = true;
843
+ v.needs_manual_install = false;
844
+ v.version = version;
845
+ v.error_code.clear();
846
+ v.error_message.clear();
847
+ set_update(v);
848
+ }
849
+
850
+ ApplyResult Engine::apply_now() {
851
+ std::string version, error;
852
+ ApplyResult r = apply_staged_update(s_, env_, &version, &error);
853
+ if (r == ApplyResult::Ok) {
854
+ mark_applied(version);
855
+ } else if (r == ApplyResult::NotWritable || r == ApplyResult::NotInstalled) {
856
+ UpdateView v = update_;
857
+ v.needs_manual_install = true;
858
+ v.error_code = r == ApplyResult::NotWritable ? "not_writable" : "not_installed";
859
+ v.error_message = error;
860
+ set_update(v);
861
+ } else if (r == ApplyResult::Failed) {
862
+ UpdateView v = update_;
863
+ v.error_code = "install_failed";
864
+ v.error_message = error;
865
+ if (!fsu::exists(paths_.staged_meta)) v.status = UpdStatus::Error; // the staged copy was discarded
866
+ set_update(v);
867
+ }
868
+ return r;
869
+ }
870
+
871
+ std::string Engine::notice() const {
872
+ const UpdateView &u = update_;
873
+ if (u.status == UpdStatus::Applied) return "Update installed: restart After Effects to use v" + u.version;
874
+ if (u.status == UpdStatus::Staged && u.needs_manual_install) {
875
+ return "Update v" + u.version + " is ready, but this plugin's folder is not writable: download it again from AfterPresets";
876
+ }
877
+ if (u.status == UpdStatus::Staged) return "Update v" + u.version + " downloaded: it installs when After Effects quits";
878
+ if (u.status == UpdStatus::Available) return "Update v" + u.version + " available";
879
+ return "";
880
+ }
881
+
882
+ // ---------------------------------------------------------------- timers
883
+
884
+ void Engine::tick() {
885
+ if (pending_.active && now() >= pending_.poll_at) poll_sign_in();
886
+ if (recheck_at_ >= 0 && now() >= recheck_at_) {
887
+ recheck_at_ = -1;
888
+ recheck_quietly();
889
+ }
890
+ if (watch_at_ >= 0 && now() >= watch_at_) {
891
+ watch_at_ = now() + CACHE_WATCH_MS;
892
+ check_cache_file();
893
+ }
894
+ if (update_at_ >= 0 && now() >= update_at_) {
895
+ update_at_ = now() + UPDATE_INTERVAL_MS;
896
+ check_updates(true);
897
+ }
898
+ }
899
+
900
+ int64_t Engine::next_wake() const {
901
+ int64_t at = INT64_MAX;
902
+ if (pending_.active) at = std::min(at, pending_.poll_at);
903
+ if (recheck_at_ >= 0) at = std::min(at, recheck_at_);
904
+ if (watch_at_ >= 0) at = std::min(at, watch_at_);
905
+ if (update_at_ >= 0) at = std::min(at, update_at_);
906
+ return at;
907
+ }
908
+
909
+ } // namespace apn