alpha-gate 0.1.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/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +138 -0
- package/bin/alpha-gate.mjs +75 -0
- package/deploy/backup.sh +45 -0
- package/deploy/deploy.sh +21 -0
- package/deploy/dev.sh +56 -0
- package/deploy/lib/statedir.sh +11 -0
- package/deploy/teardown.sh +17 -0
- package/docs/ONBOARDING.md +16 -0
- package/docs/PRINCIPLES.md +195 -0
- package/docs/README.md +54 -0
- package/docs/UPLOADING.md +14 -0
- package/docs/integrate/activation.md +56 -0
- package/docs/integrate/sparkle-go.md +106 -0
- package/docs/integrate/sparkle-swift.md +63 -0
- package/docs/maintain/backup.md +52 -0
- package/docs/maintain/migrate-account.md +95 -0
- package/docs/maintain/teardown.md +46 -0
- package/docs/maintain/troubleshooting.md +102 -0
- package/docs/maintain/updating.md +92 -0
- package/docs/operate/add-users.md +55 -0
- package/docs/operate/channels.md +54 -0
- package/docs/operate/email.md +51 -0
- package/docs/operate/monitoring.md +56 -0
- package/docs/operate/publish.md +90 -0
- package/docs/operate/remove-users.md +47 -0
- package/docs/setup/cloudflare-account.md +41 -0
- package/docs/setup/deploy.md +84 -0
- package/docs/setup/install.md +65 -0
- package/migrations/0001_clients.sql +12 -0
- package/migrations/0002_builds_streams.sql +30 -0
- package/migrations/0003_access_log.sql +14 -0
- package/migrations/0004_meta.sql +5 -0
- package/migrations/0005_admin_audit.sql +14 -0
- package/migrations/0006_build_dmg.sql +6 -0
- package/migrations/0007_access_requests.sql +12 -0
- package/migrations/0008_build_rollback_target.sql +7 -0
- package/migrations/0009_hidden.sql +6 -0
- package/migrations/0010_purged_at.sql +5 -0
- package/package.json +65 -0
- package/publish.sh +302 -0
- package/release.json +7 -0
- package/src/auth/access-jwt.ts +210 -0
- package/src/auth/token-gate.ts +27 -0
- package/src/core/appcast.ts +107 -0
- package/src/core/audit-chain.ts +145 -0
- package/src/core/invite-template.ts +127 -0
- package/src/core/no-build.ts +160 -0
- package/src/core/resolver.ts +60 -0
- package/src/core/tokens.ts +51 -0
- package/src/core/types.ts +76 -0
- package/src/core/validation.ts +60 -0
- package/src/core/verdict.ts +195 -0
- package/src/core/version.ts +113 -0
- package/src/cron.ts +37 -0
- package/src/db/access-log.ts +168 -0
- package/src/db/access-requests.ts +90 -0
- package/src/db/admin-audit.ts +101 -0
- package/src/db/builds.ts +169 -0
- package/src/db/client.ts +80 -0
- package/src/db/clients.ts +103 -0
- package/src/db/meta.ts +30 -0
- package/src/db/streams.ts +85 -0
- package/src/deploy/cli.ts +181 -0
- package/src/deploy/commands/deploy.ts +392 -0
- package/src/deploy/commands/dev.ts +190 -0
- package/src/deploy/commands/teardown.ts +159 -0
- package/src/deploy/core/args.ts +205 -0
- package/src/deploy/core/colors.ts +49 -0
- package/src/deploy/core/config.ts +65 -0
- package/src/deploy/core/parse.ts +51 -0
- package/src/deploy/core/paths.ts +27 -0
- package/src/deploy/core/plan.ts +138 -0
- package/src/deploy/core/result.ts +13 -0
- package/src/deploy/core/state.ts +64 -0
- package/src/deploy/core/table.ts +49 -0
- package/src/deploy/core/types.ts +39 -0
- package/src/deploy/core/ui.ts +107 -0
- package/src/deploy/seams/clock.ts +10 -0
- package/src/deploy/seams/files.ts +52 -0
- package/src/deploy/seams/io.ts +56 -0
- package/src/deploy/seams/wrangler.ts +100 -0
- package/src/deploy/ui-preview.ts +112 -0
- package/src/deps.ts +41 -0
- package/src/dev/admin-entry.ts +104 -0
- package/src/env.ts +47 -0
- package/src/lib/clock.ts +17 -0
- package/src/lib/hosts.ts +27 -0
- package/src/lib/text.ts +6 -0
- package/src/r2/builds-bucket.ts +50 -0
- package/src/r2/keys.ts +27 -0
- package/src/routes/admin/admin-context.ts +9 -0
- package/src/routes/admin/audit-fields.ts +22 -0
- package/src/routes/admin/branding.tsx +161 -0
- package/src/routes/admin/builds.tsx +388 -0
- package/src/routes/admin/clients.tsx +396 -0
- package/src/routes/admin/confirm.tsx +80 -0
- package/src/routes/admin/flash.ts +72 -0
- package/src/routes/admin/form.ts +43 -0
- package/src/routes/admin/index.ts +146 -0
- package/src/routes/admin/invite.ts +57 -0
- package/src/routes/admin/middleware.ts +52 -0
- package/src/routes/admin/negotiate.ts +13 -0
- package/src/routes/admin/pending.tsx +73 -0
- package/src/routes/admin/read-model.ts +518 -0
- package/src/routes/admin/streams.tsx +182 -0
- package/src/routes/admin/theme.ts +34 -0
- package/src/routes/admin/upload.tsx +320 -0
- package/src/routes/admin/views.tsx +293 -0
- package/src/routes/app/access.tsx +38 -0
- package/src/routes/app/app-context.ts +8 -0
- package/src/routes/app/appcast.ts +68 -0
- package/src/routes/app/assets.ts +25 -0
- package/src/routes/app/download.ts +45 -0
- package/src/routes/app/get.tsx +35 -0
- package/src/routes/app/index.ts +31 -0
- package/src/routes/app/resolve.ts +16 -0
- package/src/services/anchor.ts +54 -0
- package/src/services/audit.ts +19 -0
- package/src/services/branding.ts +51 -0
- package/src/services/email-cloudflare.ts +80 -0
- package/src/services/email.ts +68 -0
- package/src/services/self-update.ts +62 -0
- package/src/views/access-page.tsx +39 -0
- package/src/views/admin/ci-page.tsx +76 -0
- package/src/views/admin/combobox.tsx +191 -0
- package/src/views/admin/forms.tsx +37 -0
- package/src/views/admin/layout.tsx +496 -0
- package/src/views/admin/manage-pages.tsx +223 -0
- package/src/views/admin/manage.tsx +1120 -0
- package/src/views/admin/plist-extract.ts +233 -0
- package/src/views/admin/read-pages.tsx +1098 -0
- package/src/views/admin/setup-page.tsx +108 -0
- package/src/views/admin/table-enhance.ts +176 -0
- package/src/views/admin/ui.tsx +159 -0
- package/src/views/get-page.tsx +39 -0
- package/src/views/layout.tsx +57 -0
- package/src/worker.ts +23 -0
- package/tsconfig.json +35 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { FC } from "hono/jsx";
|
|
2
|
+
import { AdminLayout, type Chrome } from "./layout";
|
|
3
|
+
|
|
4
|
+
// §13/§20 — the CI-publishing help page. Pure: it documents the headless publish flow for this exact
|
|
5
|
+
// instance (the admin origin is interpolated into copy-paste-ready commands). The upload/register
|
|
6
|
+
// routes are the only ones that accept a Cloudflare Access service token (decision 0006).
|
|
7
|
+
|
|
8
|
+
export const CiPage: FC<{ adminOrigin: string; chrome?: Chrome }> = ({ adminOrigin, chrome }) => (
|
|
9
|
+
<AdminLayout title="CI publishing" chrome={chrome}>
|
|
10
|
+
<p class="sub" style="margin-top:6px">
|
|
11
|
+
Publish builds headlessly from CI over a Cloudflare Access <strong>service token</strong> —
|
|
12
|
+
the only credential accepted on the build upload/register routes.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<section>
|
|
16
|
+
<div class="slab">
|
|
17
|
+
<h2>1 · Create a service token</h2>
|
|
18
|
+
</div>
|
|
19
|
+
<p>
|
|
20
|
+
In Cloudflare Zero Trust → Access → Service Auth, create a service token and add a Service
|
|
21
|
+
Auth rule to this app's Access application. Store its Client ID and Secret as CI secrets:
|
|
22
|
+
</p>
|
|
23
|
+
<pre>
|
|
24
|
+
<code>CF_ACCESS_CLIENT_ID{"\n"}CF_ACCESS_CLIENT_SECRET</code>
|
|
25
|
+
</pre>
|
|
26
|
+
</section>
|
|
27
|
+
|
|
28
|
+
<section>
|
|
29
|
+
<div class="slab">
|
|
30
|
+
<h2>2 · Publish from CI</h2>
|
|
31
|
+
</div>
|
|
32
|
+
<p>
|
|
33
|
+
On a macOS runner, build → sign Developer ID → notarize → staple → produce the signed
|
|
34
|
+
artifact, then run the same publish command you use locally. It reads the version from the
|
|
35
|
+
app, signs with <code>sign_update</code> (the Worker never signs), links the channel by
|
|
36
|
+
name, and handles the {">"} 90 MB register path itself:
|
|
37
|
+
</p>
|
|
38
|
+
<pre>
|
|
39
|
+
<code>{`export CF_ACCESS_CLIENT_ID=... CF_ACCESS_CLIENT_SECRET=...
|
|
40
|
+
|
|
41
|
+
# from a clone:
|
|
42
|
+
./publish.sh dist/MyApp.zip --admin-url ${adminOrigin} --channel beta
|
|
43
|
+
# or from npm (no clone):
|
|
44
|
+
npx alpha-gate publish dist/MyApp.zip --admin-url ${adminOrigin} --channel beta`}</code>
|
|
45
|
+
</pre>
|
|
46
|
+
<p class="muted">
|
|
47
|
+
A runner with no readable app bundle (a bare zip): pass <code>--build-number</code> /{" "}
|
|
48
|
+
<code>--short-version</code> and set <code>ED_SIGNATURE</code> from your own{" "}
|
|
49
|
+
<code>sign_update</code> step.
|
|
50
|
+
</p>
|
|
51
|
+
</section>
|
|
52
|
+
|
|
53
|
+
<section>
|
|
54
|
+
<div class="slab">
|
|
55
|
+
<h2>Endpoints (service token accepted)</h2>
|
|
56
|
+
</div>
|
|
57
|
+
<table>
|
|
58
|
+
<tbody>
|
|
59
|
+
<tr>
|
|
60
|
+
<td>
|
|
61
|
+
<code>POST /admin/builds/upload</code>
|
|
62
|
+
</td>
|
|
63
|
+
<td class="muted">multipart archive + version fields</td>
|
|
64
|
+
</tr>
|
|
65
|
+
<tr>
|
|
66
|
+
<td>
|
|
67
|
+
<code>POST /admin/builds/register</code>
|
|
68
|
+
</td>
|
|
69
|
+
<td class="muted">metadata-only (object_key + size)</td>
|
|
70
|
+
</tr>
|
|
71
|
+
</tbody>
|
|
72
|
+
</table>
|
|
73
|
+
<p class="muted">Every other admin mutation requires a human Access session.</p>
|
|
74
|
+
</section>
|
|
75
|
+
</AdminLayout>
|
|
76
|
+
);
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { FC } from "hono/jsx";
|
|
2
|
+
|
|
3
|
+
// A searchable entity picker (users, builds) that keeps the form contract of a plain <select>.
|
|
4
|
+
// Without JavaScript the native select IS the control (single or multiple) and the form posts
|
|
5
|
+
// exactly what a select posts. With JavaScript, COMBOBOX_SCRIPT (injected once by AdminLayout)
|
|
6
|
+
// hides the select and drives it from a type-to-filter combobox: the select stays in the DOM as
|
|
7
|
+
// the value carrier, so the server never sees a difference. Multi-select renders removable chips
|
|
8
|
+
// and posts the name once per selection (handlers read repeated fields via idList).
|
|
9
|
+
|
|
10
|
+
export interface ComboOption {
|
|
11
|
+
value: string;
|
|
12
|
+
label: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const Combobox: FC<{
|
|
16
|
+
/** The form field name the (hidden) select posts — e.g. buildId, clientId. */
|
|
17
|
+
name: string;
|
|
18
|
+
options: ComboOption[];
|
|
19
|
+
/** Accessible name for the picker; rendered sr-only. */
|
|
20
|
+
label: string;
|
|
21
|
+
/** Combobox placeholder, e.g. "Type a build number…". */
|
|
22
|
+
placeholder: string;
|
|
23
|
+
/** Allow several selections (chips). The no-JS fallback is a native multi-select. */
|
|
24
|
+
multiple?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Required pickers get no empty option (the no-JS select then defaults to its first option,
|
|
27
|
+
* matching the old behavior); the enhancer refuses to submit with nothing picked instead.
|
|
28
|
+
*/
|
|
29
|
+
required?: boolean;
|
|
30
|
+
}> = ({ name, options, label, placeholder, multiple, required }) => (
|
|
31
|
+
<span
|
|
32
|
+
class="cbx"
|
|
33
|
+
data-combobox
|
|
34
|
+
data-placeholder={placeholder}
|
|
35
|
+
data-required={required ? "1" : undefined}
|
|
36
|
+
>
|
|
37
|
+
<label>
|
|
38
|
+
<span class="sr-only">{label}</span>
|
|
39
|
+
<select
|
|
40
|
+
name={name}
|
|
41
|
+
multiple={multiple}
|
|
42
|
+
size={multiple ? Math.min(options.length, 4) : undefined}
|
|
43
|
+
>
|
|
44
|
+
{required || multiple ? null : <option value="">— none —</option>}
|
|
45
|
+
{options.map((o) => (
|
|
46
|
+
<option value={o.value}>{o.label}</option>
|
|
47
|
+
))}
|
|
48
|
+
</select>
|
|
49
|
+
</label>
|
|
50
|
+
</span>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// The enhancer. Self-contained by design (a hand-written string — nothing serialized, no module
|
|
54
|
+
// references; see the PRINCIPLES client-side-scripts gotcha). Injected once per page by AdminLayout;
|
|
55
|
+
// no-ops when the page has no [data-combobox].
|
|
56
|
+
export const COMBOBOX_SCRIPT = `
|
|
57
|
+
(function () {
|
|
58
|
+
var uid = 0;
|
|
59
|
+
document.querySelectorAll("[data-combobox]").forEach(function (box) {
|
|
60
|
+
var select = box.querySelector("select");
|
|
61
|
+
if (!select) return;
|
|
62
|
+
var multiple = select.multiple;
|
|
63
|
+
var required = box.getAttribute("data-required") === "1";
|
|
64
|
+
var options = Array.prototype.slice.call(select.options).filter(function (o) { return o.value !== ""; });
|
|
65
|
+
var listId = "cbx-" + (++uid);
|
|
66
|
+
|
|
67
|
+
// A required single select natively preselects its first option; the combobox starts EMPTY and
|
|
68
|
+
// refuses to submit until a real pick, so nothing is chosen by accident.
|
|
69
|
+
if (!multiple) select.selectedIndex = required ? -1 : 0;
|
|
70
|
+
|
|
71
|
+
var input = document.createElement("input");
|
|
72
|
+
input.type = "text";
|
|
73
|
+
input.className = "cbx-input";
|
|
74
|
+
input.setAttribute("role", "combobox");
|
|
75
|
+
input.setAttribute("aria-expanded", "false");
|
|
76
|
+
input.setAttribute("aria-controls", listId);
|
|
77
|
+
input.setAttribute("aria-autocomplete", "list");
|
|
78
|
+
input.setAttribute("autocomplete", "off");
|
|
79
|
+
input.placeholder = box.getAttribute("data-placeholder") || "Type to search…";
|
|
80
|
+
|
|
81
|
+
var list = document.createElement("ul");
|
|
82
|
+
list.className = "cbx-list";
|
|
83
|
+
list.id = listId;
|
|
84
|
+
list.setAttribute("role", "listbox");
|
|
85
|
+
list.hidden = true;
|
|
86
|
+
|
|
87
|
+
var chips = null;
|
|
88
|
+
if (multiple) {
|
|
89
|
+
chips = document.createElement("span");
|
|
90
|
+
chips.className = "cbx-chips";
|
|
91
|
+
box.appendChild(chips);
|
|
92
|
+
}
|
|
93
|
+
box.appendChild(input);
|
|
94
|
+
box.appendChild(list);
|
|
95
|
+
box.classList.add("on");
|
|
96
|
+
|
|
97
|
+
var shown = [];
|
|
98
|
+
var active = -1;
|
|
99
|
+
|
|
100
|
+
function renderChips() {
|
|
101
|
+
if (!chips) return;
|
|
102
|
+
chips.textContent = "";
|
|
103
|
+
options.forEach(function (o) {
|
|
104
|
+
if (!o.selected) return;
|
|
105
|
+
var b = document.createElement("button");
|
|
106
|
+
b.type = "button";
|
|
107
|
+
b.className = "cbx-chip";
|
|
108
|
+
b.textContent = o.textContent.trim() + " ✕";
|
|
109
|
+
b.setAttribute("aria-label", "Remove " + o.textContent.trim());
|
|
110
|
+
b.addEventListener("click", function () { o.selected = false; renderChips(); });
|
|
111
|
+
chips.appendChild(b);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function close() {
|
|
116
|
+
list.hidden = true;
|
|
117
|
+
input.setAttribute("aria-expanded", "false");
|
|
118
|
+
input.removeAttribute("aria-activedescendant");
|
|
119
|
+
active = -1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function pick(option) {
|
|
123
|
+
if (multiple) {
|
|
124
|
+
option.selected = true;
|
|
125
|
+
input.value = "";
|
|
126
|
+
renderChips();
|
|
127
|
+
render("");
|
|
128
|
+
input.focus();
|
|
129
|
+
} else {
|
|
130
|
+
select.value = option.value;
|
|
131
|
+
input.value = option.textContent.trim();
|
|
132
|
+
close();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function render(query) {
|
|
137
|
+
var q = query.trim().toLowerCase();
|
|
138
|
+
shown = options.filter(function (o) {
|
|
139
|
+
if (multiple && o.selected) return false;
|
|
140
|
+
return o.textContent.toLowerCase().indexOf(q) !== -1;
|
|
141
|
+
});
|
|
142
|
+
list.textContent = "";
|
|
143
|
+
shown.forEach(function (o, i) {
|
|
144
|
+
var li = document.createElement("li");
|
|
145
|
+
li.id = listId + "-" + i;
|
|
146
|
+
li.setAttribute("role", "option");
|
|
147
|
+
li.textContent = o.textContent.trim();
|
|
148
|
+
if (i === active) { li.setAttribute("aria-selected", "true"); li.className = "act"; }
|
|
149
|
+
li.addEventListener("mousedown", function (e) { e.preventDefault(); pick(o); });
|
|
150
|
+
list.appendChild(li);
|
|
151
|
+
});
|
|
152
|
+
if (shown.length === 0) {
|
|
153
|
+
var none = document.createElement("li");
|
|
154
|
+
none.className = "none";
|
|
155
|
+
none.textContent = "No matches";
|
|
156
|
+
list.appendChild(none);
|
|
157
|
+
}
|
|
158
|
+
list.hidden = false;
|
|
159
|
+
input.setAttribute("aria-expanded", "true");
|
|
160
|
+
if (active >= 0 && shown[active]) input.setAttribute("aria-activedescendant", listId + "-" + active);
|
|
161
|
+
else input.removeAttribute("aria-activedescendant");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
input.addEventListener("input", function () {
|
|
165
|
+
active = -1;
|
|
166
|
+
if (!multiple) select.selectedIndex = -1; // typed text invalidates the previous pick
|
|
167
|
+
render(input.value);
|
|
168
|
+
});
|
|
169
|
+
input.addEventListener("focus", function () { render(input.value); });
|
|
170
|
+
input.addEventListener("blur", function () { close(); });
|
|
171
|
+
input.addEventListener("keydown", function (e) {
|
|
172
|
+
if (e.key === "ArrowDown") { e.preventDefault(); if (list.hidden) render(input.value); active = Math.min(active + 1, shown.length - 1); render(input.value); }
|
|
173
|
+
else if (e.key === "ArrowUp") { e.preventDefault(); active = Math.max(active - 1, 0); render(input.value); }
|
|
174
|
+
else if (e.key === "Enter") {
|
|
175
|
+
if (!list.hidden && shown.length > 0) { e.preventDefault(); pick(shown[active >= 0 ? active : 0]); }
|
|
176
|
+
}
|
|
177
|
+
else if (e.key === "Escape") { close(); }
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// A required picker refuses to submit with nothing chosen (the server would 400 anyway;
|
|
181
|
+
// this keeps the operator in place with focus on the field).
|
|
182
|
+
var form = box.closest("form");
|
|
183
|
+
if (form && required) {
|
|
184
|
+
form.addEventListener("submit", function (e) {
|
|
185
|
+
var any = options.some(function (o) { return o.selected; });
|
|
186
|
+
if (!any) { e.preventDefault(); input.focus(); box.classList.add("err"); setTimeout(function () { box.classList.remove("err"); }, 900); }
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
})();
|
|
191
|
+
`;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Child, FC } from "hono/jsx";
|
|
2
|
+
|
|
3
|
+
// A small inline POST form: optional hidden fields, optional children (inputs/selects), a submit
|
|
4
|
+
// button. The shared building block for the back-office action buttons.
|
|
5
|
+
export const Post: FC<{
|
|
6
|
+
action: string;
|
|
7
|
+
label: string;
|
|
8
|
+
hidden?: Record<string, string | number>;
|
|
9
|
+
children?: Child;
|
|
10
|
+
}> = ({ action, label, hidden = {}, children }) => (
|
|
11
|
+
<form method="post" action={action} class="inline">
|
|
12
|
+
{Object.entries(hidden).map(([name, value]) => (
|
|
13
|
+
<input type="hidden" name={name} value={String(value)} />
|
|
14
|
+
))}
|
|
15
|
+
{children}
|
|
16
|
+
<button type="submit">{label}</button>
|
|
17
|
+
</form>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Progressive enhancement for [data-copy] buttons: the target's text is selectable without JS; this
|
|
21
|
+
// only adds one-click copy with a brief "Copied" confirmation.
|
|
22
|
+
export const COPY_SCRIPT = `
|
|
23
|
+
(function () {
|
|
24
|
+
document.querySelectorAll("[data-copy]").forEach(function (btn) {
|
|
25
|
+
btn.addEventListener("click", function () {
|
|
26
|
+
var el = document.querySelector(btn.getAttribute("data-copy"));
|
|
27
|
+
var text = el ? el.textContent : "";
|
|
28
|
+
if (!navigator.clipboard || !text) return;
|
|
29
|
+
navigator.clipboard.writeText(text).then(function () {
|
|
30
|
+
var prev = btn.textContent;
|
|
31
|
+
btn.textContent = "Copied";
|
|
32
|
+
setTimeout(function () { btn.textContent = prev; }, 1500);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
})();
|
|
37
|
+
`;
|