@voltro/plugin-releases 0.0.0-stage → 0.82.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.
package/dist/index.js ADDED
@@ -0,0 +1,798 @@
1
+ import { ReleaseNotFound as e, ReleaseRefused as t } from "./errors.js";
2
+ import { a as n, i as r, n as i, o as a, r as o, s, t as c } from "./rollout-CGRKMTfv.js";
3
+ import { _ as l, a as u, c as d, d as ee, f as te, g as f, h as p, l as ne, m as re, o as ie, p as ae, r as m, s as oe, t as h, u as se, v as g, y as _ } from "./rpc-U7ytx6SC.js";
4
+ import { Effect as v } from "effect";
5
+ import { randomBytes as y } from "node:crypto";
6
+ import { definePlugin as ce } from "@voltro/protocol";
7
+ import { boolean as b, eq as x, id as S, integer as C, json as w, queryFor as T, registerRetention as le, retentionTtlMsFromEnv as ue, table as E, text as D, timestamp as O } from "@voltro/database";
8
+ import { newTraceId as de } from "@voltro/runtime";
9
+ var fe = (e, t, n) => {
10
+ let r = /* @__PURE__ */ new Set();
11
+ for (let e of t) e.currentReleaseId !== null && r.add(e.currentReleaseId);
12
+ let i = [];
13
+ for (let a of t) {
14
+ let t = n(a.name), o = Number.isFinite(t) ? Math.max(1, Math.floor(t)) : 10, s = e.filter((e) => e.channelId === a.id).sort((e, t) => c(t.version, e.version));
15
+ for (let e of s.slice(o)) r.has(e.id) || i.push(e);
16
+ }
17
+ return i;
18
+ }, pe = [
19
+ "web",
20
+ "ios",
21
+ "android",
22
+ "macos",
23
+ "windows",
24
+ "linux"
25
+ ], me = [
26
+ "pwa",
27
+ "expo",
28
+ "tauri",
29
+ "electron"
30
+ ], k = E(f, {
31
+ id: S({ prefix: "relapp" }),
32
+ app: D(),
33
+ platform: D(),
34
+ shell: D(),
35
+ bundleId: D().nullable(),
36
+ updaterPublicKey: D().nullable(),
37
+ runtimeVersion: D().nullable(),
38
+ createdAt: O().default("now")
39
+ }).index("byReleaseAppTarget", [
40
+ "app",
41
+ "platform",
42
+ "shell"
43
+ ]), A = E(g, {
44
+ id: S({ prefix: "relch" }),
45
+ appId: D(),
46
+ name: D(),
47
+ currentReleaseId: D().nullable(),
48
+ minSupportedVersion: D().nullable(),
49
+ killSwitches: w().nullable(),
50
+ public: b().default(!1),
51
+ allowUnsigned: b().default(!1),
52
+ updatedAt: O().default("now").onUpdate("now")
53
+ }).index("byReleaseChannelApp", ["appId", "name"]), j = E(p, {
54
+ id: S({ prefix: "rel" }),
55
+ appId: D(),
56
+ channelId: D(),
57
+ version: D(),
58
+ buildId: D().nullable(),
59
+ notes: D().nullable(),
60
+ state: D().default("draft"),
61
+ rolloutPercent: C().default(0),
62
+ rolloutSalt: D(),
63
+ cohorts: w().nullable(),
64
+ createdBy: D().nullable(),
65
+ createdAt: O().default("now"),
66
+ updatedAt: O().default("now").onUpdate("now")
67
+ }).index("byReleaseAppChannel", ["appId", "channelId"]).index("byReleaseVersion", ["appId", "version"]), M = E(l, {
68
+ id: S({ prefix: "relart" }),
69
+ releaseId: D(),
70
+ kind: D(),
71
+ target: D().nullable(),
72
+ url: D(),
73
+ sha256: D(),
74
+ size: C(),
75
+ signature: D().nullable(),
76
+ notarized: b().default(!1),
77
+ assetKey: D().nullable(),
78
+ contentType: D().nullable(),
79
+ createdAt: O().default("now")
80
+ }).index("byReleaseArtifactRelease", ["releaseId"]), N = E(_, {
81
+ id: S({ prefix: "relev" }),
82
+ appId: D(),
83
+ channelId: D().nullable(),
84
+ releaseId: D().nullable(),
85
+ kind: D(),
86
+ reason: D().nullable(),
87
+ actorId: D().nullable(),
88
+ detail: w().nullable(),
89
+ at: O().default("now")
90
+ }).index("byReleaseEventApp", ["appId", "at"]), P = [
91
+ k,
92
+ A,
93
+ j,
94
+ M,
95
+ N
96
+ ], F = (e, t) => t.currentVersion !== null && c(e.version, t.currentVersion) <= 0 ? !1 : o({
97
+ installId: t.installId,
98
+ salt: e.rolloutSalt,
99
+ percent: e.rolloutPercent,
100
+ ...t.cohorts === void 0 ? {} : { installCohorts: t.cohorts },
101
+ releaseCohorts: e.cohorts
102
+ }), I = (e, t) => {
103
+ if (!F(e, t)) return null;
104
+ let n = e.artifacts.find((e) => e.target === t.target && e.signature !== null);
105
+ return !n || n.signature === null ? null : {
106
+ version: e.version,
107
+ notes: e.notes ?? "",
108
+ pub_date: e.createdAt.toISOString(),
109
+ platforms: { [t.target]: {
110
+ signature: n.signature,
111
+ url: n.url
112
+ } }
113
+ };
114
+ }, L = (e, t, n = {}) => {
115
+ let r = {
116
+ win: [
117
+ "nsis",
118
+ "msi",
119
+ "exe"
120
+ ],
121
+ mac: ["dmg", "zip"],
122
+ linux: [
123
+ "appimage",
124
+ "deb",
125
+ "rpm"
126
+ ]
127
+ }, i = e.artifacts.filter((e) => r[t].includes(e.kind));
128
+ if (i.length === 0) return null;
129
+ let a = [`version: ${e.version}`, "files:"];
130
+ for (let e of i) {
131
+ a.push(` - url: ${e.url}`);
132
+ let t = n[e.sha256];
133
+ a.push(t === void 0 ? ` sha256: ${e.sha256}` : ` sha512: ${t}`), a.push(` size: ${e.size}`);
134
+ }
135
+ let o = i[0];
136
+ a.push(`path: ${o.url}`);
137
+ let s = n[o.sha256];
138
+ return a.push(s === void 0 ? `sha256: ${o.sha256}` : `sha512: ${s}`), a.push(`releaseDate: '${e.createdAt.toISOString()}'`), e.notes && a.push(`releaseNotes: ${JSON.stringify(e.notes)}`), a.join("\n") + "\n";
139
+ }, R = (e, t, n) => {
140
+ if (n.rolledBackTo !== null) return {
141
+ kind: "rollback",
142
+ commitTime: n.rolledBackTo.commitTime.toISOString()
143
+ };
144
+ if (e === null || n.runtimeVersion !== null && n.runtimeVersion !== t.runtimeVersion) return { kind: "no-update" };
145
+ let r = e.buildId ?? e.id;
146
+ if (t.currentUpdateId === r || !F(e, {
147
+ installId: t.installId,
148
+ currentVersion: null,
149
+ ...t.cohorts === void 0 ? {} : { cohorts: t.cohorts }
150
+ })) return { kind: "no-update" };
151
+ let i = e.artifacts.find((e) => e.kind === "expo-bundle" && e.target === t.platform);
152
+ if (!i) return { kind: "no-update" };
153
+ let a = e.artifacts.filter((e) => e.kind === "expo-asset" && (e.target === null || e.target === t.platform)).map((e) => ({
154
+ hash: e.sha256,
155
+ key: e.assetKey ?? e.sha256,
156
+ contentType: e.contentType ?? "application/octet-stream",
157
+ url: e.url
158
+ }));
159
+ return {
160
+ kind: "manifest",
161
+ manifest: {
162
+ id: r,
163
+ createdAt: e.createdAt.toISOString(),
164
+ runtimeVersion: t.runtimeVersion,
165
+ launchAsset: {
166
+ hash: i.sha256,
167
+ key: i.assetKey ?? "bundle",
168
+ contentType: i.contentType ?? "application/javascript",
169
+ url: i.url
170
+ },
171
+ assets: a,
172
+ metadata: {},
173
+ extra: {
174
+ version: e.version,
175
+ ...e.notes ? { notes: e.notes } : {}
176
+ }
177
+ }
178
+ };
179
+ }, z = {
180
+ macos: ["dmg", "zip"],
181
+ windows: [
182
+ "nsis",
183
+ "msi",
184
+ "exe"
185
+ ],
186
+ linux: [
187
+ "appimage",
188
+ "deb",
189
+ "rpm"
190
+ ],
191
+ ios: ["ipa"],
192
+ android: ["apk", "aab"]
193
+ }, B = (e, t, n) => {
194
+ let r = z[t] ?? [], i = e.artifacts.filter((e) => r.includes(e.kind));
195
+ if (i.length === 0) return null;
196
+ if (n !== void 0) {
197
+ let e = i.find((e) => e.target !== null && e.target.endsWith(n));
198
+ if (e) return e;
199
+ }
200
+ return i[0];
201
+ }, V = (e) => /Windows/i.test(e) ? {
202
+ os: "windows",
203
+ arch: /ARM64|aarch64/i.test(e) ? "aarch64" : "x86_64"
204
+ } : /Macintosh|Mac OS X/i.test(e) ? { os: "macos" } : /Android/i.test(e) ? { os: "android" } : /iPhone|iPad|iPod/i.test(e) ? { os: "ios" } : /Linux/i.test(e) ? {
205
+ os: "linux",
206
+ arch: /aarch64|arm64/i.test(e) ? "aarch64" : "x86_64"
207
+ } : null, H = (e, t, n = {}) => ({
208
+ status: e,
209
+ body: JSON.stringify(t),
210
+ contentType: "application/json; charset=utf-8",
211
+ headers: {
212
+ "cache-control": "no-store",
213
+ ...n
214
+ }
215
+ }), he = (e, t, n) => ({
216
+ status: e,
217
+ body: t,
218
+ contentType: n,
219
+ headers: { "cache-control": "no-store" }
220
+ }), U = (e) => ({
221
+ status: e,
222
+ body: "",
223
+ headers: { "cache-control": "no-store" }
224
+ }), W = (e) => `${e}_${y(9).toString("base64url")}`, G = (n) => {
225
+ let r = (n.prefix ?? "/releases").replace(/\/+$/, ""), a = n.now ?? Date.now, o, s = () => {
226
+ if (!o) throw new t({ reason: "releases: the app store is not bound yet" });
227
+ return o;
228
+ }, m = null, y = async (e) => {
229
+ let t = (await s().query(T(k).where(x("app", e.app)).descriptor)).find((t) => t.platform === e.platform && t.shell === e.shell), n = !t;
230
+ t ||= await s().insert(f, {
231
+ id: W("relapp"),
232
+ app: e.app,
233
+ platform: e.platform,
234
+ shell: e.shell,
235
+ bundleId: e.bundleId ?? null,
236
+ updaterPublicKey: e.updaterPublicKey ?? null,
237
+ runtimeVersion: e.runtimeVersion ?? null,
238
+ createdAt: new Date(a())
239
+ });
240
+ let r = await s().query(T(A).where(x("appId", t.id)).descriptor);
241
+ for (let n of e.channels ?? [
242
+ "stable",
243
+ "beta",
244
+ "internal"
245
+ ]) r.some((e) => e.name === n) || await s().insert(g, {
246
+ id: W("relch"),
247
+ appId: t.id,
248
+ name: n,
249
+ currentReleaseId: null,
250
+ minSupportedVersion: null,
251
+ killSwitches: null,
252
+ public: n === "stable",
253
+ allowUnsigned: n === "internal",
254
+ updatedAt: new Date(a())
255
+ });
256
+ return {
257
+ row: t,
258
+ created: n
259
+ };
260
+ }, b = async () => {
261
+ for (let e of n.apps) await y(e);
262
+ }, S = () => m ?? Promise.resolve(), C = (t, n, r) => v.tryPromise({
263
+ try: async () => {
264
+ await S();
265
+ let i = (await s().query(T(k).where(x("app", t)).descriptor)).find((e) => (n === void 0 || e.platform === n) && (r === void 0 || e.shell === r));
266
+ if (!i) throw new e({ app: t });
267
+ return i;
268
+ },
269
+ catch: (n) => n instanceof e ? n : new e({ app: t })
270
+ }), w = (t, n, r) => v.tryPromise({
271
+ try: async () => {
272
+ let i = (await s().query(T(A).where(x("appId", t)).descriptor)).find((e) => e.name === n);
273
+ if (!i) throw new e({
274
+ app: r,
275
+ channel: n
276
+ });
277
+ return i;
278
+ },
279
+ catch: (t) => t instanceof e ? t : new e({
280
+ app: r,
281
+ channel: n
282
+ })
283
+ }), E = (e) => s().query(T(j).where(x("id", e)).descriptor).then((e) => e[0] ?? null), D = (e) => s().query(T(j).where(x("appId", e)).descriptor), O = (e) => s().query(T(M).where(x("releaseId", e)).descriptor), pe = (e) => s().query(T(A).where(x("appId", e)).descriptor), me = (e) => n.retention?.perChannel?.[e] ?? n.retention?.keep ?? 10, F = async (e, t) => {
284
+ let n = await pe(e), r = fe(await D(e), n, me);
285
+ if (r.length === 0) return [];
286
+ for (let e of r) await s().deleteMany(l, { where: x("releaseId", e.id) }), await s().delete(p, e.id);
287
+ return await K({
288
+ appId: e,
289
+ kind: "prune",
290
+ actorId: t,
291
+ detail: { removed: r.map((e) => ({
292
+ version: e.version,
293
+ channel: n.find((t) => t.id === e.channelId)?.name ?? null
294
+ })) }
295
+ }), r;
296
+ }, z = async (e) => {
297
+ let t = await O(e.id);
298
+ return {
299
+ id: e.id,
300
+ version: e.version,
301
+ buildId: e.buildId,
302
+ notes: e.notes,
303
+ createdAt: e.createdAt,
304
+ rolloutPercent: e.rolloutPercent,
305
+ rolloutSalt: e.rolloutSalt,
306
+ cohorts: e.cohorts ?? null,
307
+ artifacts: t.map((e) => ({
308
+ kind: e.kind,
309
+ target: e.target,
310
+ url: e.url,
311
+ sha256: e.sha256,
312
+ size: e.size,
313
+ signature: e.signature,
314
+ assetKey: e.assetKey,
315
+ contentType: e.contentType
316
+ }))
317
+ };
318
+ }, G = async (e, t) => {
319
+ let n = await v.runPromise(C(e)), r = await v.runPromise(w(n.id, t, e)), i = r.currentReleaseId ? await E(r.currentReleaseId) : null;
320
+ return {
321
+ app: e,
322
+ channel: t,
323
+ latestVersion: i?.version ?? null,
324
+ minSupportedVersion: r.minSupportedVersion,
325
+ killSwitches: r.killSwitches ?? {},
326
+ rolloutPercent: i?.rolloutPercent ?? 0,
327
+ rolloutSalt: i?.rolloutSalt ?? null,
328
+ cohorts: i?.cohorts ?? null
329
+ };
330
+ }, K = (e) => s().insert(_, {
331
+ id: W("relev"),
332
+ appId: e.appId,
333
+ channelId: e.channelId ?? null,
334
+ releaseId: e.releaseId ?? null,
335
+ kind: e.kind,
336
+ reason: e.reason ?? null,
337
+ actorId: e.actorId ?? null,
338
+ detail: e.detail ?? null,
339
+ at: new Date(a())
340
+ }), q = (e) => e.request.subject.type === "user" ? e.request.subject.id : null, ve = {
341
+ ...oe,
342
+ description: "The reactive release policy of a channel: latest version, minimum supported version, kill switches, rollout.",
343
+ execute: (e) => v.tryPromise({
344
+ try: () => G(e.app, e.channel),
345
+ catch: (e) => e
346
+ })
347
+ }, ye = {
348
+ ...ie,
349
+ description: "Every release of an app target, newest first.",
350
+ execute: (e) => v.gen(function* () {
351
+ let t = yield* C(e.app), n = yield* v.promise(() => D(t.id)), r = yield* v.promise(() => s().query(T(A).where(x("appId", t.id)).descriptor)), i = [];
352
+ for (let e of [...n].sort((e, t) => t.createdAt.getTime() - e.createdAt.getTime() || c(t.version, e.version))) {
353
+ let t = yield* v.promise(() => O(e.id));
354
+ i.push({
355
+ id: e.id,
356
+ version: e.version,
357
+ buildId: e.buildId,
358
+ state: e.state,
359
+ rolloutPercent: e.rolloutPercent,
360
+ channel: r.find((t) => t.id === e.channelId)?.name ?? "?",
361
+ createdAt: e.createdAt.toISOString(),
362
+ artifacts: t.length,
363
+ signed: t.length > 0 && t.every((e) => e.signature !== null)
364
+ });
365
+ }
366
+ return i;
367
+ })
368
+ }, be = {
369
+ ...ne,
370
+ description: "Register an app target at runtime, with its channels; idempotent.",
371
+ execute: (e) => v.tryPromise({
372
+ try: async () => {
373
+ await S();
374
+ let { row: t, created: n } = await y({
375
+ app: e.app,
376
+ platform: e.platform,
377
+ shell: e.shell,
378
+ ...e.bundleId === void 0 ? {} : { bundleId: e.bundleId },
379
+ ...e.updaterPublicKey === void 0 ? {} : { updaterPublicKey: e.updaterPublicKey },
380
+ ...e.runtimeVersion === void 0 ? {} : { runtimeVersion: e.runtimeVersion },
381
+ ...e.channels === void 0 ? {} : { channels: e.channels }
382
+ });
383
+ return {
384
+ id: t.id,
385
+ created: n
386
+ };
387
+ },
388
+ catch: (n) => new t({ reason: `releases: ${e.app} could not be registered: ${n instanceof Error ? n.message : String(n)}` })
389
+ })
390
+ }, xe = {
391
+ ...u,
392
+ description: "Create a draft release of a version on a channel.",
393
+ execute: (e, n) => v.gen(function* () {
394
+ let r = yield* C(e.app), i = yield* w(r.id, e.channel, e.app);
395
+ if ((yield* v.promise(() => D(r.id))).some((t) => t.version === e.version)) return yield* v.fail(new t({ reason: `version ${e.version} already exists for ${e.app}` }));
396
+ let o = W("rel");
397
+ return yield* v.promise(() => s().insert(p, {
398
+ id: o,
399
+ appId: r.id,
400
+ channelId: i.id,
401
+ version: e.version,
402
+ buildId: e.buildId ?? null,
403
+ notes: e.notes ?? null,
404
+ state: "draft",
405
+ rolloutPercent: 0,
406
+ rolloutSalt: W("salt"),
407
+ cohorts: e.cohorts ?? null,
408
+ createdBy: q(n),
409
+ createdAt: new Date(a()),
410
+ updatedAt: new Date(a())
411
+ })), yield* v.promise(() => K({
412
+ appId: r.id,
413
+ channelId: i.id,
414
+ releaseId: o,
415
+ kind: "create",
416
+ actorId: q(n),
417
+ detail: { version: e.version }
418
+ })), yield* v.promise(() => F(r.id, q(n))), { id: o };
419
+ })
420
+ }, Se = {
421
+ ...re,
422
+ description: "Attach an artefact (a file the client downloads) to a release.",
423
+ execute: (n, r) => v.gen(function* () {
424
+ let i = yield* v.promise(() => E(n.releaseId));
425
+ if (!i) return yield* v.fail(new e({
426
+ app: "?",
427
+ version: n.releaseId
428
+ }));
429
+ if (!/^[a-f0-9]{64}$/i.test(n.sha256)) return yield* v.fail(new t({ reason: "sha256 must be 64 hex characters" }));
430
+ let o = W("relart");
431
+ return yield* v.promise(() => s().insert(l, {
432
+ id: o,
433
+ releaseId: i.id,
434
+ kind: n.kind,
435
+ target: n.target ?? null,
436
+ url: n.url,
437
+ sha256: n.sha256.toLowerCase(),
438
+ size: n.size,
439
+ signature: n.signature ?? null,
440
+ notarized: n.notarized ?? !1,
441
+ assetKey: n.assetKey ?? null,
442
+ contentType: n.contentType ?? null,
443
+ createdAt: new Date(a())
444
+ })), yield* v.promise(() => K({
445
+ appId: i.appId,
446
+ releaseId: i.id,
447
+ kind: "upload",
448
+ actorId: q(r),
449
+ detail: {
450
+ kind: n.kind,
451
+ target: n.target ?? null
452
+ }
453
+ })), { id: o };
454
+ })
455
+ }, Ce = {
456
+ ...d,
457
+ description: "Point a channel at a release (no rebuild) and start its rollout.",
458
+ execute: (n, r) => v.gen(function* () {
459
+ let i = yield* C(n.app), o = yield* w(i.id, n.to, n.app), c = yield* v.promise(() => D(i.id)), l = c.find((e) => e.version === n.version);
460
+ if (!l) return yield* v.fail(new e({
461
+ app: n.app,
462
+ version: n.version
463
+ }));
464
+ let u = yield* v.promise(() => O(l.id));
465
+ if (!o.allowUnsigned && u.some((e) => e.signature === null)) return yield* v.fail(new t({ reason: `release ${n.version} has unsigned artefacts; channel ${n.to} refuses them` }));
466
+ let d = Math.max(0, Math.min(100, n.rolloutPercent ?? 100));
467
+ return yield* v.promise(() => s().update(p, l.id, {
468
+ channelId: o.id,
469
+ state: d >= 100 ? "complete" : "rolling",
470
+ rolloutPercent: d,
471
+ updatedAt: new Date(a())
472
+ })), yield* v.promise(() => s().update(g, o.id, {
473
+ currentReleaseId: l.id,
474
+ updatedAt: new Date(a())
475
+ })), yield* v.promise(() => K({
476
+ appId: i.id,
477
+ channelId: o.id,
478
+ releaseId: l.id,
479
+ kind: "promote",
480
+ actorId: q(r),
481
+ detail: {
482
+ from: c.find((e) => e.id === o.currentReleaseId)?.version ?? null,
483
+ to: n.version,
484
+ rolloutPercent: d
485
+ }
486
+ })), {
487
+ channel: n.to,
488
+ version: n.version,
489
+ rolloutPercent: d
490
+ };
491
+ })
492
+ }, we = {
493
+ ...te,
494
+ description: "Change the rollout percentage of the channel's current release.",
495
+ execute: (n, r) => v.gen(function* () {
496
+ let i = yield* C(n.app), o = yield* w(i.id, n.channel, n.app);
497
+ if (!o.currentReleaseId) return yield* v.fail(new t({ reason: `channel ${n.channel} has no current release` }));
498
+ let c = yield* v.promise(() => E(o.currentReleaseId));
499
+ if (!c) return yield* v.fail(new e({
500
+ app: n.app,
501
+ channel: n.channel
502
+ }));
503
+ let l = Math.max(0, Math.min(100, n.percent));
504
+ return yield* v.promise(() => s().update(p, c.id, {
505
+ rolloutPercent: l,
506
+ state: l >= 100 ? "complete" : "rolling",
507
+ updatedAt: new Date(a())
508
+ })), yield* v.promise(() => K({
509
+ appId: i.id,
510
+ channelId: o.id,
511
+ releaseId: c.id,
512
+ kind: "rollout",
513
+ actorId: q(r),
514
+ detail: { percent: l }
515
+ })), {
516
+ version: c.version,
517
+ rolloutPercent: l
518
+ };
519
+ })
520
+ }, Te = {
521
+ ...ee,
522
+ description: "Point the channel back at the previous release; the current one is marked rolled back.",
523
+ execute: (e, n) => v.gen(function* () {
524
+ let r = yield* C(e.app), i = yield* w(r.id, e.channel, e.app);
525
+ if (!i.currentReleaseId) return yield* v.fail(new t({ reason: `channel ${e.channel} has no current release` }));
526
+ let o = yield* v.promise(() => D(r.id)), l = o.find((e) => e.id === i.currentReleaseId), u = o.filter((e) => e.channelId === i.id && e.id !== i.currentReleaseId && e.state !== "rolledBack").sort((e, t) => c(t.version, e.version))[0];
527
+ return l && (yield* v.promise(() => s().update(p, l.id, {
528
+ state: "rolledBack",
529
+ updatedAt: new Date(a())
530
+ }))), yield* v.promise(() => s().update(g, i.id, {
531
+ currentReleaseId: u?.id ?? null,
532
+ updatedAt: new Date(a())
533
+ })), yield* v.promise(() => K({
534
+ appId: r.id,
535
+ channelId: i.id,
536
+ releaseId: l?.id ?? null,
537
+ kind: "rollback",
538
+ reason: e.reason,
539
+ actorId: q(n),
540
+ detail: {
541
+ from: l?.version ?? null,
542
+ to: u?.version ?? null
543
+ }
544
+ })), { version: u?.version ?? null };
545
+ })
546
+ }, J = {
547
+ ...ae,
548
+ description: "Set the minimum supported version and kill switches of a channel.",
549
+ execute: (e, t) => v.gen(function* () {
550
+ let n = yield* C(e.app), r = yield* w(n.id, e.channel, e.app);
551
+ return yield* v.promise(() => s().update(g, r.id, {
552
+ ...e.minSupportedVersion === void 0 ? {} : { minSupportedVersion: e.minSupportedVersion },
553
+ ...e.killSwitches === void 0 ? {} : { killSwitches: e.killSwitches },
554
+ updatedAt: new Date(a())
555
+ })), yield* v.promise(() => K({
556
+ appId: n.id,
557
+ channelId: r.id,
558
+ kind: "policy",
559
+ reason: e.reason,
560
+ actorId: q(t),
561
+ detail: {
562
+ minSupportedVersion: e.minSupportedVersion ?? null,
563
+ killSwitches: e.killSwitches ?? null
564
+ }
565
+ })), yield* v.promise(() => G(e.app, e.channel));
566
+ })
567
+ }, Y = async (e, t) => {
568
+ let n = await v.runPromise(C(e)), r = await v.runPromise(w(n.id, t, e)), i = r.currentReleaseId ? await E(r.currentReleaseId) : null;
569
+ return {
570
+ app: n,
571
+ channel: r,
572
+ release: i ? await z(i) : null
573
+ };
574
+ }, X = (e, t) => t.get("install") ?? e.headers["x-voltro-install-id"] ?? e.headers["expo-embedded-update-id"] ?? "anonymous", Ee = {
575
+ method: "GET",
576
+ path: r,
577
+ handle: async (t) => {
578
+ if (t.method !== "GET" && t.method !== "HEAD") return H(405, { error: "method_not_allowed" });
579
+ let n = t.path.slice(r.length).split("/").filter((e) => e.length > 0).map(decodeURIComponent), a = new URLSearchParams(t.query), [o, s] = n;
580
+ if (!o || !s) return H(404, { error: "not_found" });
581
+ try {
582
+ if (s === "tauri") {
583
+ let [, , e, r, i] = n;
584
+ if (!e || !r || !i) return H(400, { error: "expected /tauri/:channel/:target/:currentVersion" });
585
+ let { release: s } = await Y(o, e), c = s ? I(s, {
586
+ installId: X(t, a),
587
+ currentVersion: i,
588
+ target: r
589
+ }) : null;
590
+ return c ? H(200, c) : U(204);
591
+ }
592
+ if (s === "expo" && n[2] === "manifest") {
593
+ let e = t.headers["expo-channel-name"] ?? a.get("channel") ?? "stable", n = t.headers["expo-platform"] ?? a.get("platform"), r = t.headers["expo-runtime-version"] ?? a.get("runtimeVersion") ?? "";
594
+ if (n !== "ios" && n !== "android") return H(400, { error: "expo-platform must be ios or android" });
595
+ let { app: i, release: s } = await Y(o, e), c = R(s, {
596
+ installId: X(t, a),
597
+ currentVersion: null,
598
+ platform: n,
599
+ runtimeVersion: r,
600
+ currentUpdateId: t.headers["expo-current-update-id"] ?? null
601
+ }, {
602
+ runtimeVersion: i.runtimeVersion,
603
+ rolledBackTo: null
604
+ });
605
+ return c.kind === "no-update" ? H(200, { type: "noUpdateAvailable" }, {
606
+ "expo-protocol-version": "1",
607
+ "expo-sfv-version": "0"
608
+ }) : c.kind === "rollback" ? H(200, {
609
+ type: "rollBackToEmbedded",
610
+ parameters: { commitTime: c.commitTime }
611
+ }, {
612
+ "expo-protocol-version": "1",
613
+ "expo-sfv-version": "0"
614
+ }) : H(200, c.manifest, {
615
+ "expo-protocol-version": "1",
616
+ "expo-sfv-version": "0",
617
+ "expo-manifest-filters": ""
618
+ });
619
+ }
620
+ if (s === "electron") {
621
+ let [, , e, t] = n;
622
+ if (!e || !t) return H(400, { error: "expected /electron/:channel/latest.yml" });
623
+ let r = t === "latest-mac.yml" ? "mac" : t === "latest-linux.yml" ? "linux" : t === "latest.yml" ? "win" : null;
624
+ if (r === null) return H(404, { error: "not_found" });
625
+ let { release: i } = await Y(o, e), a = i ? L(i, r) : null;
626
+ return a ? he(200, a, "text/yaml; charset=utf-8") : U(204);
627
+ }
628
+ if (s === "download") {
629
+ let [, , e] = n;
630
+ if (!e) return H(400, { error: "expected /download/:channel" });
631
+ let { channel: r, release: i } = await Y(o, e);
632
+ if (!r.public) return H(403, { error: "channel_not_public" });
633
+ if (!i) return U(204);
634
+ let s = a.get("os") ?? V(t.headers["user-agent"] ?? "")?.os, c = a.get("arch") ?? V(t.headers["user-agent"] ?? "")?.arch;
635
+ if (!s) return H(400, { error: "os is required (?os=macos|windows|linux|ios|android)" });
636
+ let l = B(i, s, c);
637
+ return l ? {
638
+ status: 302,
639
+ body: "",
640
+ headers: {
641
+ location: l.url,
642
+ "cache-control": "no-store"
643
+ }
644
+ } : H(404, { error: "no_artifact_for_os" });
645
+ }
646
+ if (s === "policy") {
647
+ let [, , e] = n;
648
+ if (!e) return H(400, { error: "expected /policy/:channel" });
649
+ let r = await G(o, e), s = a.get("version"), c = s ? i(r, {
650
+ version: s,
651
+ installId: X(t, a)
652
+ }) : null;
653
+ return H(200, {
654
+ policy: r,
655
+ ...c ? { verdict: c } : {}
656
+ });
657
+ }
658
+ return H(404, { error: "not_found" });
659
+ } catch (t) {
660
+ if (t instanceof e) return H(404, {
661
+ error: "not_found",
662
+ app: t.app
663
+ });
664
+ throw t;
665
+ }
666
+ }
667
+ }, Z = (e) => ({
668
+ kind: "json",
669
+ data: e
670
+ }), De = [{
671
+ method: "GET",
672
+ path: "/overview",
673
+ scope: "shared-store",
674
+ description: "Every app target with its channels, current versions and rollout state.",
675
+ handler: () => v.promise(async () => {
676
+ let e = await s().query(T(k).descriptor), t = await s().query(T(A).descriptor), n = await s().query(T(j).descriptor);
677
+ return Z({ apps: e.map((e) => ({
678
+ id: e.id,
679
+ app: e.app,
680
+ platform: e.platform,
681
+ shell: e.shell,
682
+ channels: t.filter((t) => t.appId === e.id).map((e) => {
683
+ let t = n.find((t) => t.id === e.currentReleaseId);
684
+ return {
685
+ name: e.name,
686
+ current: t?.version ?? null,
687
+ rolloutPercent: t?.rolloutPercent ?? 0,
688
+ state: t?.state ?? null,
689
+ minSupportedVersion: e.minSupportedVersion,
690
+ public: e.public
691
+ };
692
+ }),
693
+ releases: n.filter((t) => t.appId === e.id).length
694
+ })) });
695
+ })
696
+ }, {
697
+ method: "GET",
698
+ path: "/events",
699
+ scope: "shared-store",
700
+ description: "The last 100 release events (promote, rollback, rollout, policy).",
701
+ handler: () => v.promise(async () => {
702
+ let e = await s().query(T(N).descriptor);
703
+ return Z({ events: [...e].sort((e, t) => t.at.getTime() - e.at.getTime()).slice(0, 100).map((e) => ({
704
+ ...e,
705
+ at: e.at.toISOString()
706
+ })) });
707
+ })
708
+ }], Oe = () => ({ request: {
709
+ subject: {
710
+ type: "anonymous",
711
+ id: null,
712
+ tenantId: null
713
+ },
714
+ traceId: de()
715
+ } }), Q = (e, t) => ({
716
+ kind: "json",
717
+ status: e,
718
+ data: { error: t }
719
+ }), $ = (n, r, i) => ({
720
+ method: "POST",
721
+ path: n,
722
+ description: i,
723
+ handler: (n) => v.gen(function* () {
724
+ let i;
725
+ try {
726
+ i = JSON.parse(n.body || "{}");
727
+ } catch {
728
+ return Q(400, "invalid JSON body");
729
+ }
730
+ if (typeof i != "object" || !i || Array.isArray(i)) return Q(400, "expected a JSON object body");
731
+ let a = yield* r.execute(i, Oe()).pipe(v.either);
732
+ if (a._tag === "Left") {
733
+ let n = a.left;
734
+ return n instanceof e ? Q(404, `not found: ${n.app}${n.version ? ` ${n.version}` : ""}${n.channel ? ` (${n.channel})` : ""}`) : n instanceof t ? Q(409, n.reason) : Q(400, n instanceof Error ? n.message : String(n));
735
+ }
736
+ return Z({ result: a.right });
737
+ })
738
+ }), ke = [
739
+ $("/apps", be, "Register an app target at runtime: { app, platform, shell, channels? }."),
740
+ $("/create", xe, "Create a draft release: { app, channel, version, buildId?, notes? }."),
741
+ $("/upload", Se, "Attach an artefact: { releaseId, kind, target?, url, sha256, size, signature? }."),
742
+ $("/promote", Ce, "Promote a version onto a channel: { app, version, to, rolloutPercent? }."),
743
+ $("/rollout", we, "Set the rollout percentage: { app, channel, percent }."),
744
+ $("/rollback", Te, "Roll a channel back: { app, channel, reason }."),
745
+ $("/policy", J, "Set the policy: { app, channel, minSupportedVersion?, killSwitches?, reason }.")
746
+ ];
747
+ return le({
748
+ source: "plugin",
749
+ table: _,
750
+ timeColumn: "at",
751
+ ttlMs: ue(process.env.VOLTRO_RELEASE_EVENTS_TTL_HOURS, 8760),
752
+ envVar: "VOLTRO_RELEASE_EVENTS_TTL_HOURS"
753
+ }), ce({
754
+ name: n.name ? `${h}#${n.name}` : h,
755
+ description: "Release channels, artefacts and rollouts — the self-hosted update server behind Tauri, Expo OTA, electron-updater and PWA feeds, with a reactive release policy.",
756
+ permissions: [
757
+ "inspect:read",
758
+ "inspect:write",
759
+ "store:write"
760
+ ],
761
+ routes: [
762
+ ve,
763
+ ye,
764
+ be,
765
+ xe,
766
+ Se,
767
+ Ce,
768
+ we,
769
+ Te,
770
+ J
771
+ ],
772
+ rpcClientDescriptors: se,
773
+ httpRoutes: [Ee],
774
+ inspectEndpoints: [...De, ...ke],
775
+ extendSchema: { tables: [...P] },
776
+ declaredEnv: _e,
777
+ privacy: ge,
778
+ bindDataStore: (e) => {
779
+ o = e, m = b(), m.catch(() => {});
780
+ },
781
+ onActivate: () => v.tryPromise({
782
+ catch: (e) => /* @__PURE__ */ Error(`@voltro/plugin-releases could not seed its targets and channels: ${e instanceof Error ? e.message : String(e)}`),
783
+ try: () => S()
784
+ })
785
+ });
786
+ }, ge = { collects: [{
787
+ category: "device-id",
788
+ linked: !1,
789
+ tracking: !1,
790
+ purposes: ["app-functionality"]
791
+ }] }, _e = [{
792
+ name: "VOLTRO_RELEASE_EVENTS_TTL_HOURS",
793
+ required: !1,
794
+ secret: !1,
795
+ description: "Hours a release event (create, upload, promote, rollout, rollback, policy, prune) is kept before the retention sweep removes it. Default 8760 (one year)."
796
+ }];
797
+ //#endregion
798
+ export { h as BASE_NAME, m as RELEASES_MANAGE_SCOPE, p as RELEASES_TABLE, f as RELEASE_APPS_TABLE, l as RELEASE_ARTIFACTS_TABLE, g as RELEASE_CHANNELS_TABLE, _ as RELEASE_EVENTS_TABLE, pe as RELEASE_PLATFORMS, me as RELEASE_SHELLS, e as ReleaseNotFound, t as ReleaseRefused, c as compareVersions, i as evaluatePolicy, o as inRollout, r as isNewerVersion, F as offeredTo, V as osFromUserAgent, n as parseVersion, B as pickDownload, k as releaseAppsTable, M as releaseArtifactsTable, A as releaseChannelsTable, N as releaseEventsTable, P as releaseTables, G as releasesPlugin, j as releasesTable, L as renderElectronLatestYml, R as renderExpoFeed, I as renderTauriFeed, a as rolloutBucket, s as sortVersionsDesc };