@ham2k/extension-sdk 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.
- package/AGENTS.md +139 -0
- package/LICENSE +21 -0
- package/README.md +19 -8
- package/dist/activityExports.js +12 -6
- package/dist/activityScoring.js +2 -0
- package/dist/dxcc.js +15 -2
- package/dist/index.d.ts +71 -4
- package/dist/index.js +16 -14
- package/dist/modes.js +20 -0
- package/dist/refTransforms.js +34 -0
- package/dist/referenceActivity.js +89 -18
- package/dist/scoring.js +11 -4
- package/dist/segments.js +17 -0
- package/dist/templateContext.js +4 -0
- package/docs/distribution.md +445 -0
- package/docs/forms.md +279 -0
- package/docs/hooks.md +1379 -0
- package/docs/settings.md +524 -0
- package/docs/templates.md +206 -0
- package/package.json +22 -3
- package/samples/README.md +33 -0
- package/samples/k2hrc-cqww/build.mjs +6 -0
- package/samples/k2hrc-cqww/manifest.json +23 -0
- package/samples/k2hrc-cqww/src/index.ts +273 -0
- package/samples/k2hrc-hamqth/build.mjs +6 -0
- package/samples/k2hrc-hamqth/manifest.json +24 -0
- package/samples/k2hrc-hamqth/src/index.ts +202 -0
- package/samples/k2hrc-llota/build.mjs +6 -0
- package/samples/k2hrc-llota/manifest.json +25 -0
- package/samples/k2hrc-llota/src/index.ts +172 -0
- package/samples/k2hrc-radio/build.mjs +6 -0
- package/samples/k2hrc-radio/manifest.json +23 -0
- package/samples/k2hrc-radio/src/index.ts +145 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
# Distributing an extension — the `.h2kext` bundle
|
|
2
|
+
|
|
3
|
+
An extension can be handed to someone as a single file they install into the
|
|
4
|
+
app: no registry, no app-store release. This describes the format and the tool
|
|
5
|
+
that writes one.
|
|
6
|
+
|
|
7
|
+
Writing the extension in the first place is the dev loop's job —
|
|
8
|
+
[development.md](https://github.com/ham2k/halo/blob/main/docs/extensions/development.md).
|
|
9
|
+
|
|
10
|
+
## The format
|
|
11
|
+
|
|
12
|
+
A `.h2kext` is a **zip**:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
manifest.json required — identity and capabilities
|
|
16
|
+
index.js required — your extension, built
|
|
17
|
+
assets/ optional — icons, data, licence text
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Zip rather than tar.gz for one reason above the others: the host must read
|
|
21
|
+
`manifest.json` **without** running any JS and often without unpacking the
|
|
22
|
+
rest — to list an installed-but-disabled extension, and to show someone what a
|
|
23
|
+
file is asking for *before* they agree to install it. A zip's central
|
|
24
|
+
directory makes that a seek; a tar.gz is a solid stream that must be
|
|
25
|
+
decompressed from the beginning every time. Signed-archive precedent (`.vsix`,
|
|
26
|
+
`.xpi`, `.crx`, `.jar`, `.epub` are all zip) and being openable by
|
|
27
|
+
double-click on macOS and Windows settle the rest.
|
|
28
|
+
|
|
29
|
+
### manifest.json
|
|
30
|
+
|
|
31
|
+
The same manifest a built-in extension carries, plus `api`:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"key": "w7abc-notes",
|
|
36
|
+
"name": "W7ABC Notes",
|
|
37
|
+
"version": "1.2.3",
|
|
38
|
+
"description": "Notes about the stations I work",
|
|
39
|
+
"category": "dashboard",
|
|
40
|
+
"api": 1,
|
|
41
|
+
"domains": ["notes.example.org"],
|
|
42
|
+
"icon": "note-text"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`api` is the extension API the bundle was built against. The host refuses one
|
|
47
|
+
it does not speak, so an older app meeting a newer bundle says so instead of
|
|
48
|
+
failing somewhere deep in a hook.
|
|
49
|
+
|
|
50
|
+
Three fields are **refused** in a distributed bundle, and it is worth
|
|
51
|
+
understanding why:
|
|
52
|
+
|
|
53
|
+
- **No `category`** makes an extension *core* to the host: always enabled, and
|
|
54
|
+
never listed in the Extensions panel. That is a status the app grants its own
|
|
55
|
+
extensions. A distributed bundle claiming it would be one the user could
|
|
56
|
+
neither see nor turn off.
|
|
57
|
+
- **`experiments`** gates availability against the *app's* experiment catalog.
|
|
58
|
+
A key the host doesn't define hides the extension permanently, with nothing
|
|
59
|
+
in the UI to explain it.
|
|
60
|
+
- **`enabledByDefault`** is a shipping app's answer for the extensions it
|
|
61
|
+
carries whether the operator asked for them or not. An installed bundle is
|
|
62
|
+
one the operator went and got, so the answer is always yes, and a manifest
|
|
63
|
+
saying otherwise describes a state it cannot reach.
|
|
64
|
+
|
|
65
|
+
**`geo` is now `relevance`**, and a manifest still carrying the old name is
|
|
66
|
+
refused at packing and at publishing rather than ignored. Nothing reads
|
|
67
|
+
`geo`, and no check anywhere looks at unknown top-level keys, so accepting it
|
|
68
|
+
would publish a regional extension as a worldwide one and tell its author
|
|
69
|
+
nothing. Rename the key; the four geographic lists inside it are unchanged.
|
|
70
|
+
|
|
71
|
+
The app itself does NOT refuse either of these at install. The packer and the
|
|
72
|
+
catalog answer to the author, who can fix the manifest and publish again;
|
|
73
|
+
refusing at the door tells the operator instead, who can do nothing about a
|
|
74
|
+
published manifest but go without an extension that would have worked — and
|
|
75
|
+
every bundle published before these rules existed carries one. Neither costs
|
|
76
|
+
them anything: an installed extension is enabled because they installed it,
|
|
77
|
+
and an unread `geo` makes it worldwide rather than regional. What the app
|
|
78
|
+
refuses is what it cannot run.
|
|
79
|
+
|
|
80
|
+
### Relevance
|
|
81
|
+
|
|
82
|
+
`relevance` says where, when and for whom the extension matters. The catalog
|
|
83
|
+
ranks its listing by it, for the operators the extension was written for, and
|
|
84
|
+
the Extensions panel's Catalog section shows the catalog's rows in that
|
|
85
|
+
order; nothing hides an extension over it, and a manifest without one is
|
|
86
|
+
worldwide and undated. The build index carries `relevance` for the catalog
|
|
87
|
+
and for later use; the panel does not rank the app's own extensions by it.
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
"relevance": {
|
|
91
|
+
"countries": ["us"],
|
|
92
|
+
"dates": ["2026-09-19", "2027-09-18"],
|
|
93
|
+
"interests": ["cw"]
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Where.** Four lists, each an allowlist over the operator's own callsign:
|
|
98
|
+
`entities` (DXCC prefixes as the country file names them: `K`, `VE`, `CE`),
|
|
99
|
+
`countries` (ISO alpha-2: `us`, `cl`), `continents` (`AF AS EU NA OC SA AN`)
|
|
100
|
+
and `ituRegions` (`1`, `2`, `3`). A missing or empty list does not gate;
|
|
101
|
+
values within a list are ORed, the lists ANDed; case is the matcher's job.
|
|
102
|
+
These are the geo keys of HaLo's notices, rule for rule.
|
|
103
|
+
|
|
104
|
+
**When.** `dates` lists the UTC days the extension's events start, as
|
|
105
|
+
`YYYY-MM-DD`. Start days, not spans: a reader asking "is this soon" needs no
|
|
106
|
+
more, and the extension's own code stays the only place that knows when an
|
|
107
|
+
event opens and closes to the minute. An event on a fixed calendar lists the
|
|
108
|
+
occurrences it knows about; one that runs to a weekly rule lists none,
|
|
109
|
+
because a list that must be right every week is a list that goes stale. An
|
|
110
|
+
absent or empty `dates` says nothing about timing — never "never".
|
|
111
|
+
|
|
112
|
+
**For whom.** `interests` draws on a closed vocabulary: the modes `cw`,
|
|
113
|
+
`phone`, `digital`; the band groups `hf`, `vhf`, `uhf`, `microwave`; the
|
|
114
|
+
styles `satellite`, `portable`, `qrp`. Only for an extension *dedicated* to
|
|
115
|
+
one — a contest that permits CW is not a `cw` extension, while the CWops CWT
|
|
116
|
+
is. Listing what an extension merely allows makes every interest match
|
|
117
|
+
everything, which is the same as listing none. The list is closed because an
|
|
118
|
+
interest nothing else spells the same way matches nobody while looking like
|
|
119
|
+
it works.
|
|
120
|
+
|
|
121
|
+
The packer refuses:
|
|
122
|
+
|
|
123
|
+
- a `relevance` that is not an object, or a key inside it other than those six;
|
|
124
|
+
- a list that is not a list, or an element that is not a non-empty string;
|
|
125
|
+
- a continent outside `AF AS EU NA OC SA AN`, or an ITU region outside `1`–`3`;
|
|
126
|
+
- a country that is not two letters, or an entity prefix that is not letters,
|
|
127
|
+
digits and `/` (with an optional leading `*`);
|
|
128
|
+
- a date that is not `YYYY-MM-DD`, or that names a day which does not exist;
|
|
129
|
+
- an interest outside the vocabulary above.
|
|
130
|
+
|
|
131
|
+
The matcher reads a key it cannot parse as "does not gate", so each of the
|
|
132
|
+
shape errors would otherwise ship the bundle as worldwide.
|
|
133
|
+
|
|
134
|
+
## Using the host's libraries
|
|
135
|
+
|
|
136
|
+
The host carries a set of libraries and hands every extension the same
|
|
137
|
+
instances, so a bundle does not have to ship its own copies:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
@ham2k/lib-callsigns @ham2k/lib-qson-adif
|
|
141
|
+
@ham2k/lib-country-files @ham2k/lib-qson-cabrillo
|
|
142
|
+
@ham2k/lib-cqmag-data @ham2k/lib-qson-tools
|
|
143
|
+
@ham2k/lib-dxcc-data i18next
|
|
144
|
+
@ham2k/lib-format-tools liquidjs
|
|
145
|
+
@ham2k/lib-geo-tools
|
|
146
|
+
@ham2k/lib-operation-data
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
It matters more than it sounds. The same extension, built both ways:
|
|
150
|
+
|
|
151
|
+
| | bundle | packaged |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| dependencies inlined | 1430 KB | 218 KB |
|
|
154
|
+
| using the host's | 22 KB | **7 KB** |
|
|
155
|
+
|
|
156
|
+
**Declare what you need.** The host carries whatever version *that build*
|
|
157
|
+
shipped with, and you have no say in which app your bundle lands in. So the
|
|
158
|
+
manifest states the ranges you work with:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
"sharedDependencies": {
|
|
162
|
+
"@ham2k/lib-callsigns": "^1.0.0",
|
|
163
|
+
"liquidjs": "^10.0.0"
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The host checks those before loading, and refuses a bundle it cannot satisfy —
|
|
168
|
+
which is a message naming the library and both versions, instead of a call
|
|
169
|
+
failing somewhere deep in a hook against a major you never tested.
|
|
170
|
+
|
|
171
|
+
**Declare the version you actually build against, not the oldest that
|
|
172
|
+
compiles.** That check compares versions, and cannot see which *functions* a
|
|
173
|
+
version has. A library that gained an export — `qsonToCabrillo` arrived in
|
|
174
|
+
`@ham2k/lib-qson-cabrillo` 1.2.0, whose earlier releases only read Cabrillo —
|
|
175
|
+
satisfies a `^1.0.0` declaration on a host too old to have it, so the bundle
|
|
176
|
+
installs, loads, and throws the first time an operator asks for the feature
|
|
177
|
+
that needs it. The build's own message suggests the version installed beside
|
|
178
|
+
you for exactly this reason; take it rather than rounding down.
|
|
179
|
+
|
|
180
|
+
The packer refuses a bundle that reaches for a shared module **without**
|
|
181
|
+
declaring it, and one that declares a package the host doesn't carry. Anything
|
|
182
|
+
outside that list you bundle yourself, as normal.
|
|
183
|
+
|
|
184
|
+
Which versions a particular build carries are in its
|
|
185
|
+
`assets/extensions/index.json`, under `sharedModules`.
|
|
186
|
+
|
|
187
|
+
Emitting the lookups is the build preset's job — see **Building** below.
|
|
188
|
+
|
|
189
|
+
## Building
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
npm install @ham2k/extension-sdk
|
|
193
|
+
npm install --save-dev @ham2k/extension-tools esbuild
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`npx -p @ham2k/extension-tools h2kext-init <yourcallsign>-<name>` writes a working one of everything
|
|
197
|
+
below — manifest, source, build script — so the rest of this section is what to
|
|
198
|
+
change rather than what to type.
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
import { build } from 'esbuild'
|
|
202
|
+
import { buildExtension } from '@ham2k/extension-tools'
|
|
203
|
+
|
|
204
|
+
await buildExtension(build, { dir: import.meta.dirname })
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
That reads `manifest.json`, compiles `src/index.ts` into `build/index.js`, and
|
|
208
|
+
copies the manifest alongside it, ready for the packer. The esbuild settings it
|
|
209
|
+
applies are requirements of the runtime rather than preferences: one IIFE
|
|
210
|
+
because the sandbox evaluates a script and has no module loader, `neutral`
|
|
211
|
+
because there is no Node and no DOM, and `es2020` because that is what
|
|
212
|
+
QuickJS-NG speaks.
|
|
213
|
+
|
|
214
|
+
esbuild is yours to bring — the preset takes your `build` function rather than
|
|
215
|
+
importing its own, so there is only ever one copy of it.
|
|
216
|
+
|
|
217
|
+
Everything `sharedDependencies` names becomes a lookup on the host's instance.
|
|
218
|
+
Anything else you use is bundled, as normal.
|
|
219
|
+
|
|
220
|
+
**Using one of the host's libraries without declaring it fails the build**,
|
|
221
|
+
naming the library and the two ways out. That is deliberate: the alternative is
|
|
222
|
+
a bundle that silently carries its own copy — thirty times the size, with its
|
|
223
|
+
own module state — and nothing anywhere that says so. If the second copy is
|
|
224
|
+
what you want, which is how you use a version the host does not carry, say so:
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
await buildExtension(build, { dir: import.meta.dirname, inline: ['liquidjs'] })
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`extensions/samples/hello-world` is the shortest complete example. Declaring
|
|
231
|
+
what it uses rather than carrying it took the packaged bundle from 185 KB to
|
|
232
|
+
**4 KB**.
|
|
233
|
+
|
|
234
|
+
The preset also settles a trap every author would otherwise hit: the SDK
|
|
235
|
+
reaches `liquidjs`, whose `main`/`module` are its **Node** builds, and
|
|
236
|
+
`platform: 'neutral'` ignores the `browser` field that would pick the right
|
|
237
|
+
one, so a hand-written esbuild config fails on unresolvable Node built-ins
|
|
238
|
+
before it compiles anything — even for an extension that never renders a
|
|
239
|
+
template.
|
|
240
|
+
|
|
241
|
+
## Packaging
|
|
242
|
+
|
|
243
|
+
```sh
|
|
244
|
+
node extensions/tools/h2kext-pack.mjs <directory> [-o out.h2kext]
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The directory holds your built `index.js`, your `manifest.json`, and optionally
|
|
248
|
+
`assets/`. The packer **packages; it does not compile** — building is yours,
|
|
249
|
+
which is what lets this work outside this repo.
|
|
250
|
+
|
|
251
|
+
It reports every problem at once rather than one per run:
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
h2kext-pack: ./build is not ready to package:
|
|
255
|
+
manifest.json: missing required field 'name'
|
|
256
|
+
manifest.json: key 'my-clock' must start with your callsign — 'my' is not one (e.g. ki2d-my-clock)
|
|
257
|
+
manifest.json: missing 'api' — declare the extension API this was built against (currently 1)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`--force-name` waives the naming convention below. It exists for Ham2K's own
|
|
261
|
+
packaging, which builds the extensions that ship inside the app under bare keys
|
|
262
|
+
and the reserved prefix; it does not waive what the runtime needs of a key.
|
|
263
|
+
|
|
264
|
+
`extensions/tools/h2kext.mjs` holds the format and its rules, and
|
|
265
|
+
`h2kext-build.mjs` the build preset. Both are free of any dependency on the
|
|
266
|
+
rest of this repo, and ship together as **`@ham2k/extension-tools`** — the
|
|
267
|
+
toolchain half, separate from the **`@ham2k/extension-sdk`** an extension
|
|
268
|
+
imports and bundles.
|
|
269
|
+
|
|
270
|
+
Two things about the SDK package are load-bearing rather than incidental, and
|
|
271
|
+
`extensions/sdk/build.mjs` asserts both:
|
|
272
|
+
|
|
273
|
+
- It ships as **one file per module**, not one bundle. Rolled into a single
|
|
274
|
+
file, your bundler can no longer tell which parts your extension reaches, and
|
|
275
|
+
Hello World comes out five times larger carrying an ADIF importer it cannot
|
|
276
|
+
call.
|
|
277
|
+
- Its JavaScript keeps the shared libraries as **bare specifiers**. The build
|
|
278
|
+
preset can only rewrite an import it can see; resolved inside the SDK, they
|
|
279
|
+
would be gone before it ran, and every extension would carry private copies
|
|
280
|
+
again.
|
|
281
|
+
|
|
282
|
+
`kernel.ts` lives in the SDK's source directory and is never published. It is
|
|
283
|
+
the host — it defines the runtime and holds the one instance of every shared
|
|
284
|
+
library that extensions look up. This repo's own
|
|
285
|
+
build uses the same plugin, which is what keeps the path you take from being
|
|
286
|
+
the untested one.
|
|
287
|
+
|
|
288
|
+
## Keys
|
|
289
|
+
|
|
290
|
+
An extension key is **`<yourcallsign>-<name>`**:
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
ki2d-my-clock w7abc-notes 2e0abc-contest-helper
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Lowercase, letters digits and hyphens. The callsign is a namespace every author
|
|
297
|
+
of this software already holds — globally unique, free, and already meaningful
|
|
298
|
+
to everyone here — so two people who never met cannot ship the same key. The
|
|
299
|
+
packer refuses anything else.
|
|
300
|
+
|
|
301
|
+
`ham2k-` is reserved for Ham2K's own extensions, and refused to everyone else.
|
|
302
|
+
There is no signing yet, so anyone could claim it, and the key is what the app
|
|
303
|
+
shows when it says where a hook came from.
|
|
304
|
+
|
|
305
|
+
The extensions built into the app use bare keys (`pota`, `sota`, …). Those are
|
|
306
|
+
reserved too: hook identity has to stay unambiguous, and build-secret
|
|
307
|
+
visibility is by key prefix, so an installed extension calling itself `sota`
|
|
308
|
+
would be asking to read `SOTA_*`. The app refuses any collision at install —
|
|
309
|
+
the packer cannot, because a standalone tool has no way to know what the app it
|
|
310
|
+
will be installed into contains.
|
|
311
|
+
|
|
312
|
+
## What an installed extension does not get
|
|
313
|
+
|
|
314
|
+
**Build secrets.** `host.secret()` returns null for an installed extension
|
|
315
|
+
whatever key it claims, exactly as it does for a dev-served one: visibility
|
|
316
|
+
follows where the code came from, not what it calls itself. A feature that
|
|
317
|
+
needs one degrades the same way it does in a build without that value
|
|
318
|
+
configured.
|
|
319
|
+
|
|
320
|
+
The sandbox is otherwise the same one the built-in extensions run in — no
|
|
321
|
+
filesystem, no network beyond the `domains` the manifest declares and the user
|
|
322
|
+
approved, no timers.
|
|
323
|
+
|
|
324
|
+
## Installing one
|
|
325
|
+
|
|
326
|
+
**Settings → Extensions → Install from file…**, pick the `.h2kext`, and agree
|
|
327
|
+
to what it asks for. The runtime restarts with it loaded, and the extension
|
|
328
|
+
appears in its own category alongside the app's own, marked *Installed* and
|
|
329
|
+
with a way to remove it again.
|
|
330
|
+
|
|
331
|
+
What the operator is shown before anything is written is the whole point of
|
|
332
|
+
the format being a zip: the file is opened and read, and only then unpacked.
|
|
333
|
+
|
|
334
|
+
Installing a bundle whose key is already installed **updates it in place**.
|
|
335
|
+
What it stored survives — an update is not a reinstall, and re-entering
|
|
336
|
+
credentials every time is how people learn not to update.
|
|
337
|
+
|
|
338
|
+
**Every install asks, including an update that wants nothing new.** Comparing
|
|
339
|
+
capabilities and staying quiet when they had not grown would let a version
|
|
340
|
+
through on the strength of its key alone, and nothing about the *code* behind
|
|
341
|
+
that key is compared — a bundle can change completely and still ask for the
|
|
342
|
+
same things. That may be worth revisiting once a bundle can be shown to come
|
|
343
|
+
from whoever it claims; it is not worth it while a key is all there is.
|
|
344
|
+
|
|
345
|
+
Removing an extension removes what it stored with it.
|
|
346
|
+
|
|
347
|
+
### Installing from a link
|
|
348
|
+
|
|
349
|
+
A web page can hand the app a bundle to install:
|
|
350
|
+
|
|
351
|
+
```
|
|
352
|
+
com.ham2k.logger:///install_extension?url=https://example.org/my-ext.h2kext
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Every edition of Ham2K Logger answers that scheme, so on a device with more
|
|
356
|
+
than one installed the platform decides which one opens; the catalog keeps a
|
|
357
|
+
plain download link beside its install button for that reason. The
|
|
358
|
+
edition-specific `com.ham2k.logger.dev` / `.next` / `.prod` schemes exist
|
|
359
|
+
but a link should not need them. The URL must be `https`, redirects are followed by hand and refused the
|
|
360
|
+
moment one leaves `https`, and the consent screen names the host the bytes
|
|
361
|
+
came from. Nothing is written until the operator agrees.
|
|
362
|
+
|
|
363
|
+
### Pre-loaded extensions
|
|
364
|
+
|
|
365
|
+
The app ships eight of the catalog's own extensions already packaged, under
|
|
366
|
+
`app/assets/preloaded-extensions/`, and installs them into the ordinary store
|
|
367
|
+
on first run: `ham2k-pota`, `ham2k-sota`, `ham2k-wwff`, and the five lookups.
|
|
368
|
+
Afterwards they are ordinary installed extensions — the same rows, the same
|
|
369
|
+
update check, the same Update and uninstall — so an operator starts with a
|
|
370
|
+
working app and the catalog can move it forward from there.
|
|
371
|
+
|
|
372
|
+
They are installed once, not every launch. A record of the highest version
|
|
373
|
+
ever pre-loaded per key is what makes that true: an app update carrying a
|
|
374
|
+
newer bundle installs it, a catalog release already ahead of it is left
|
|
375
|
+
alone, and a key the operator uninstalled stays gone.
|
|
376
|
+
|
|
377
|
+
Each one that renames a built-in — `ham2k-pota` is the built-in `pota` under
|
|
378
|
+
a name the catalog can serve — carries the operator's settings, panel values
|
|
379
|
+
and account credentials across on its first install. Copied, not moved: the
|
|
380
|
+
built-in's own state stays where it is.
|
|
381
|
+
|
|
382
|
+
One rule bends for these, and only for them:
|
|
383
|
+
|
|
384
|
+
- **Built-in collisions.** An installed bundle may not claim a key the app
|
|
385
|
+
ships — judged against what the app is currently OFFERING, which under the
|
|
386
|
+
catalog experiment is the core extensions and nothing else. `ham2k-lookup`
|
|
387
|
+
is both a built-in and a pre-load, and could otherwise never install.
|
|
388
|
+
|
|
389
|
+
**Build secrets are not among them, and used to be.** A pre-load holding a
|
|
390
|
+
`ham2k-` key could once read the values its name granted. Nothing needs that
|
|
391
|
+
now: every value it carried — SOTA's OAuth client id, WWFF's API key — ships
|
|
392
|
+
as a constant inside the extension that uses it, because these bundles are
|
|
393
|
+
served to anyone who asks and a `.h2kext` is a zip. An installed extension
|
|
394
|
+
sees no build secret, whatever key it claims and wherever it came from.
|
|
395
|
+
|
|
396
|
+
### Installing from the catalog
|
|
397
|
+
|
|
398
|
+
**Settings → Extensions → Catalog** lists what [catalog.ham2k.net](https://catalog.ham2k.net)
|
|
399
|
+
publishes on its `stable` channel, in the catalog's own order for the
|
|
400
|
+
operator's callsign (`relevance`, above). Install and Update go through the same consent screen as a
|
|
401
|
+
file, over bytes that must hash to the sha256 the listing gives — a mismatch
|
|
402
|
+
is refused before the zip is opened. A bundle that arrived this way is
|
|
403
|
+
recorded as such (`extensionInstalledFrom`: host, sha256, version), and that
|
|
404
|
+
record is what lets it carry a `ham2k-` key: the rule that refuses the prefix
|
|
405
|
+
to a file stands, waived on every read only for a key whose record names
|
|
406
|
+
`catalog.ham2k.net`, and cleared by an uninstall or by a file install of the
|
|
407
|
+
same key. A key the app ships stays refused whatever the catalog says. The
|
|
408
|
+
catalog's own bundle links (`…/api/v1/extensions/<key>/versions/<version>/bundle`)
|
|
409
|
+
take this verified path when they arrive as a deep link; every other host
|
|
410
|
+
takes the plain one above.
|
|
411
|
+
|
|
412
|
+
At most once every six hours — on launch and on resume — the app asks the
|
|
413
|
+
catalog whether anything installed has a newer release, and says so in the
|
|
414
|
+
status bar and on the row; the notice comes down with the last update taken.
|
|
415
|
+
It never installs unasked. A release the catalog has revoked is put on record
|
|
416
|
+
(`extensionRevoked`: version and note) and stops loading on the spot,
|
|
417
|
+
whatever the operator's own switch says — the row shows "Revoked" with the
|
|
418
|
+
note and no switch, and the record dies with an install of another version
|
|
419
|
+
or an uninstall.
|
|
420
|
+
|
|
421
|
+
## What is checked at install
|
|
422
|
+
|
|
423
|
+
Beyond what the packer already refused:
|
|
424
|
+
|
|
425
|
+
- the key is not one the app ships, and does not begin `ham2k-`;
|
|
426
|
+
- the `api` version is one this build speaks;
|
|
427
|
+
- every `sharedDependencies` range is satisfied by what this build carries;
|
|
428
|
+
- `relevance`, if present, passes the packer's rules — the shape the matcher
|
|
429
|
+
reads and its vocabulary — applied again by `ExtensionRelevance.validate`;
|
|
430
|
+
- entry names are sanitised on extract, so `../` in one writes nothing.
|
|
431
|
+
|
|
432
|
+
Each has its own message. A bundle that cannot be installed says why.
|
|
433
|
+
|
|
434
|
+
## Native only, for now
|
|
435
|
+
|
|
436
|
+
Installed bundles are files on disk. On web the app runs from its own assets
|
|
437
|
+
and, for authors, the dev server — [development.md](https://github.com/ham2k/halo/blob/main/docs/extensions/development.md). So on
|
|
438
|
+
web there is nothing to install into: no install from a file, no install or
|
|
439
|
+
update from the catalog, and none of the pre-loaded extensions above.
|
|
440
|
+
|
|
441
|
+
"For now" is a deferral, not a fact about the platform. Almost everything
|
|
442
|
+
here is already platform-neutral — the zip walk, the manifest rules, the
|
|
443
|
+
consent screen, the hash check — and what is left is a persistence seam of
|
|
444
|
+
about six operations. `HALO-583` is the card that picks it up, and says what
|
|
445
|
+
has to be decided first.
|