@ham2k/extension-sdk 0.1.0 → 0.2.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/README.md +19 -4
- package/dist/index.d.ts +1 -1
- package/docs/distribution.md +279 -0
- package/docs/forms.md +271 -0
- package/docs/hooks.md +1282 -0
- package/docs/settings.md +523 -0
- package/docs/templates.md +204 -0
- package/package.json +12 -2
- package/samples/README.md +33 -0
- package/samples/k2hrc-cqww/build.mjs +6 -0
- package/samples/k2hrc-cqww/manifest.json +24 -0
- package/samples/k2hrc-cqww/src/index.ts +273 -0
- package/samples/k2hrc-hamqth/build.mjs +6 -0
- package/samples/k2hrc-hamqth/manifest.json +25 -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 +24 -0
- package/samples/k2hrc-radio/src/index.ts +145 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Writing a Ham2K Logger extension
|
|
2
|
+
|
|
3
|
+
*This file describes the published `@ham2k/extension-sdk` package, as installed
|
|
4
|
+
in an extension project's `node_modules`. Working inside the Ham2K Logger
|
|
5
|
+
repository itself is a different setup — see `docs/extensions/README.md` there.*
|
|
6
|
+
|
|
7
|
+
You are building an extension for the Ham2K Logger — a TypeScript module the
|
|
8
|
+
app loads into a sandbox and calls through **hooks**. Extensions are where the
|
|
9
|
+
app's domain knowledge lives: award programs, contests, callsign lookups, spot
|
|
10
|
+
sources, exporters, panels.
|
|
11
|
+
|
|
12
|
+
Everything you need is in this package. `samples/` holds one worked extension
|
|
13
|
+
per shape — start there, with the one whose shape matches what you are
|
|
14
|
+
building. `docs/` is the reference, pinned to the version of the SDK installed
|
|
15
|
+
here; `dist/index.d.ts` is the typed surface, and is the final word wherever
|
|
16
|
+
prose and types disagree.
|
|
17
|
+
|
|
18
|
+
Paths like `app/lib/config/secret_policy.dart` or `app/experiments.yaml`,
|
|
19
|
+
mentioned in those docs, are files in the Ham2K Logger's own repository
|
|
20
|
+
(<https://github.com/ham2k/halo>). They are not part of your project and you
|
|
21
|
+
cannot read them here.
|
|
22
|
+
|
|
23
|
+
## The shape of a project
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
manifest.json your extension's public identity — read without running code
|
|
27
|
+
src/index.ts imports the manifest, calls defineExtension
|
|
28
|
+
build.mjs runs @ham2k/extension-tools' build preset
|
|
29
|
+
build/ generated: index.js + manifest.json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`npx -p @ham2k/extension-tools h2kext-init <yourcallsign>-<name>` writes all of it.
|
|
33
|
+
|
|
34
|
+
## The smallest whole extension
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { defineExtension } from '@ham2k/extension-sdk'
|
|
38
|
+
import type { PanelHook } from '@ham2k/extension-sdk'
|
|
39
|
+
|
|
40
|
+
import manifest from '../manifest.json'
|
|
41
|
+
|
|
42
|
+
const HelloPanel: PanelHook = {
|
|
43
|
+
async getPanels() {
|
|
44
|
+
return [{ key: 'hello', title: 'Hello World', icon: 'hand-wave-outline' }]
|
|
45
|
+
},
|
|
46
|
+
async render() {
|
|
47
|
+
return { kind: 'markdown', content: '# Hello, World!' }
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
defineExtension({
|
|
52
|
+
...manifest,
|
|
53
|
+
onActivation({ registerHook }) {
|
|
54
|
+
registerHook('panel', { key: manifest.key, hook: HelloPanel })
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`manifest.json` needs at least `key`, `name`, `version`, `description`,
|
|
60
|
+
`category`, `api`, and the `sharedDependencies` below. The manifest is spread
|
|
61
|
+
into `defineExtension` rather than retyped: two sources for the same identity
|
|
62
|
+
drift, and the packer reads the file.
|
|
63
|
+
|
|
64
|
+
## Building and shipping
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
node build.mjs # → build/index.js
|
|
68
|
+
npx -p @ham2k/extension-tools h2kext-pack build -o my-extension.h2kext
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The packer is also the checker. It reports every problem at once and refuses
|
|
72
|
+
anything that would install cleanly and then misbehave where you cannot watch
|
|
73
|
+
it, so a clean pack is the strongest evidence available outside the app itself.
|
|
74
|
+
Run it whenever you have changed the manifest or the hooks you register.
|
|
75
|
+
|
|
76
|
+
`tsc --noEmit` is the other half: the hook interfaces are typed, and a hook that
|
|
77
|
+
typechecks is registered under a category the host knows, with the methods it
|
|
78
|
+
will actually call.
|
|
79
|
+
|
|
80
|
+
## Where to read next
|
|
81
|
+
|
|
82
|
+
| Doing this | Read |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| Anything, before the reference — a whole worked extension of that shape | `samples/`: `k2hrc-hamqth` (lookup + credentials), `k2hrc-llota` (award program), `k2hrc-cqww` (contest + scoring), `k2hrc-radio` (HTML panel) |
|
|
85
|
+
| Any hook at all — what the category is, what it's handed, what it must return | `docs/hooks.md`, the section named for the category |
|
|
86
|
+
| A pane in a view: `getPanels`, `render`, panel content kinds | `docs/hooks.md` §`panel` |
|
|
87
|
+
| Callsign lookups, spot sources, exporters, reference types (POTA-style) | `docs/hooks.md` §`lookup`, §`spots`, §`export`, §`ref:<type>` |
|
|
88
|
+
| Contest or award scoring, and the UI an activity contributes | `docs/hooks.md` §`scoring`, §`activity` |
|
|
89
|
+
| A settings screen for your extension | `docs/settings.md` |
|
|
90
|
+
| Any form, dialog or field the user fills in | `docs/forms.md` |
|
|
91
|
+
| Panel text, export filenames, ADIF fields — anything with `{{ }}` in it | `docs/templates.md` |
|
|
92
|
+
| Packaging, the `.h2kext` format, keys, using the host's libraries | `docs/distribution.md` |
|
|
93
|
+
|
|
94
|
+
`docs/hooks.md` is long. Read the section for the category you are registering,
|
|
95
|
+
not the file.
|
|
96
|
+
|
|
97
|
+
## What the sandbox is
|
|
98
|
+
|
|
99
|
+
No Node, no DOM, no `fetch`, no `window`, no filesystem. One ES2020 engine
|
|
100
|
+
evaluating one script. Reach the outside world through `host`:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { host } from '@ham2k/extension-sdk'
|
|
104
|
+
|
|
105
|
+
await host.fetch(url) // https only, and only domains your manifest lists
|
|
106
|
+
await host.kvGet('key') // storage, namespaced to your extension
|
|
107
|
+
await host.kvSet('key', value)
|
|
108
|
+
await host.showForm(form) // ask the user something
|
|
109
|
+
await host.showMessage({ … }) // tell them something
|
|
110
|
+
host.log('…') // the app's console
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`dist/index.d.ts` has the rest, with the types.
|
|
114
|
+
|
|
115
|
+
## Traps worth knowing before you write anything
|
|
116
|
+
|
|
117
|
+
- **A panel returns content, not widgets.** `{ kind: 'markdown', content }` and
|
|
118
|
+
its siblings. There is no way to reach the app's UI toolkit, by design.
|
|
119
|
+
- **`host.fetch` only reaches domains your manifest's `domains` lists**, over
|
|
120
|
+
https. An undeclared host is refused at the call, not at build.
|
|
121
|
+
- **Using a shared library without declaring it fails the build.** The app
|
|
122
|
+
carries `@ham2k/lib-*`, `i18next` and `liquidjs`; naming one in
|
|
123
|
+
`sharedDependencies` rewrites your imports into lookups on the app's single
|
|
124
|
+
copy, which is worth roughly thirty times the bundle size. Don't install them.
|
|
125
|
+
`inline: ['liquidjs']` in your build ships your own copy, for when you need a
|
|
126
|
+
version the app does not carry. A scaffolded manifest already declares the
|
|
127
|
+
eight this SDK's own modules reach — importing anything from it pulls them
|
|
128
|
+
in — so deleting one from the manifest breaks a build that was working.
|
|
129
|
+
- **Your key is `<callsign>-<name>`**, lowercase. It namespaces your storage and
|
|
130
|
+
is what the app shows when it says where something came from. `ham2k-` is
|
|
131
|
+
reserved.
|
|
132
|
+
- **`host.secret()` returns null** for anything not built into the app. Degrade;
|
|
133
|
+
don't throw. A hook that throws takes its whole feature down.
|
|
134
|
+
- **Hook method names are routed by string.** The host finds your hook by its
|
|
135
|
+
registered key and then calls the method by name, so `renderPanel` where
|
|
136
|
+
`render` was meant is an extension that installs, activates, and does nothing.
|
|
137
|
+
This is what typing your hook as `PanelHook` (or its siblings) catches.
|
|
138
|
+
- **Every hook is async and crosses a bridge.** Some context reads carry a whole
|
|
139
|
+
log — fine when exporting, not on a per-keystroke path.
|
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ const HelloPanel: PanelHook = {
|
|
|
26
26
|
defineExtension({
|
|
27
27
|
...manifest,
|
|
28
28
|
onActivation({ registerHook }) {
|
|
29
|
-
registerHook('panel', { key:
|
|
29
|
+
registerHook('panel', { key: manifest.key, hook: HelloPanel })
|
|
30
30
|
},
|
|
31
31
|
})
|
|
32
32
|
```
|
|
@@ -34,9 +34,24 @@ defineExtension({
|
|
|
34
34
|
A panel returns *content*, not widgets — which is what keeps extensions out of
|
|
35
35
|
the app's UI toolkit.
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
`npx -p @ham2k/extension-tools h2kext-init <yourcallsign>-<name>` writes
|
|
38
|
+
all of that and the build script; the same package packages the result into a
|
|
39
|
+
`.h2kext` the app can install.
|
|
40
|
+
|
|
41
|
+
## The reference is in this package
|
|
42
|
+
|
|
43
|
+
`samples/` holds one worked extension per shape — a callsign lookup with the
|
|
44
|
+
credentials it needs, an award program, a contest and its scorer, an HTML
|
|
45
|
+
panel. Each builds and packs; each says in its header what it leaves out.
|
|
46
|
+
|
|
47
|
+
`AGENTS.md` beside this file is the map — the manifest, the sandbox's limits,
|
|
48
|
+
and which of `docs/` answers what. `docs/` holds the references themselves:
|
|
49
|
+
every hook category, forms, settings panels, templates, and the bundle format.
|
|
50
|
+
|
|
51
|
+
They travel in the package rather than being linked to, so they are readable
|
|
52
|
+
offline and describe the version installed here rather than whatever the
|
|
53
|
+
repository says today. If an agent is writing your extension, point it at
|
|
54
|
+
`node_modules/@ham2k/extension-sdk/AGENTS.md` — `h2kext-init` does that for you.
|
|
40
55
|
|
|
41
56
|
## About the peer dependencies
|
|
42
57
|
|
package/dist/index.d.ts
CHANGED
|
@@ -0,0 +1,279 @@
|
|
|
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
|
+
Two 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
|
+
|
|
61
|
+
## Using the host's libraries
|
|
62
|
+
|
|
63
|
+
The host carries a set of libraries and hands every extension the same
|
|
64
|
+
instances, so a bundle does not have to ship its own copies:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
@ham2k/lib-callsigns @ham2k/lib-qson-adif
|
|
68
|
+
@ham2k/lib-country-files @ham2k/lib-qson-cabrillo
|
|
69
|
+
@ham2k/lib-cqmag-data @ham2k/lib-qson-tools
|
|
70
|
+
@ham2k/lib-dxcc-data i18next
|
|
71
|
+
@ham2k/lib-format-tools liquidjs
|
|
72
|
+
@ham2k/lib-geo-tools
|
|
73
|
+
@ham2k/lib-operation-data
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
It matters more than it sounds. The same extension, built both ways:
|
|
77
|
+
|
|
78
|
+
| | bundle | packaged |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| dependencies inlined | 1430 KB | 218 KB |
|
|
81
|
+
| using the host's | 22 KB | **7 KB** |
|
|
82
|
+
|
|
83
|
+
**Declare what you need.** The host carries whatever version *that build*
|
|
84
|
+
shipped with, and you have no say in which app your bundle lands in. So the
|
|
85
|
+
manifest states the ranges you work with:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
"sharedDependencies": {
|
|
89
|
+
"@ham2k/lib-callsigns": "^1.0.0",
|
|
90
|
+
"liquidjs": "^10.0.0"
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The host checks those before loading, and refuses a bundle it cannot satisfy —
|
|
95
|
+
which is a message naming the library and both versions, instead of a call
|
|
96
|
+
failing somewhere deep in a hook against a major you never tested.
|
|
97
|
+
|
|
98
|
+
The packer refuses a bundle that reaches for a shared module **without**
|
|
99
|
+
declaring it, and one that declares a package the host doesn't carry. Anything
|
|
100
|
+
outside that list you bundle yourself, as normal.
|
|
101
|
+
|
|
102
|
+
Which versions a particular build carries are in its
|
|
103
|
+
`assets/extensions/index.json`, under `sharedModules`.
|
|
104
|
+
|
|
105
|
+
Emitting the lookups is the build preset's job — see **Building** below.
|
|
106
|
+
|
|
107
|
+
## Building
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
npm install @ham2k/extension-sdk
|
|
111
|
+
npm install --save-dev @ham2k/extension-tools esbuild
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`npx -p @ham2k/extension-tools h2kext-init <yourcallsign>-<name>` writes a working one of everything
|
|
115
|
+
below — manifest, source, build script — so the rest of this section is what to
|
|
116
|
+
change rather than what to type.
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
import { build } from 'esbuild'
|
|
120
|
+
import { buildExtension } from '@ham2k/extension-tools'
|
|
121
|
+
|
|
122
|
+
await buildExtension(build, { dir: import.meta.dirname })
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
That reads `manifest.json`, compiles `src/index.ts` into `build/index.js`, and
|
|
126
|
+
copies the manifest alongside it, ready for the packer. The esbuild settings it
|
|
127
|
+
applies are requirements of the runtime rather than preferences: one IIFE
|
|
128
|
+
because the sandbox evaluates a script and has no module loader, `neutral`
|
|
129
|
+
because there is no Node and no DOM, and `es2020` because that is what
|
|
130
|
+
QuickJS-NG speaks.
|
|
131
|
+
|
|
132
|
+
esbuild is yours to bring — the preset takes your `build` function rather than
|
|
133
|
+
importing its own, so there is only ever one copy of it.
|
|
134
|
+
|
|
135
|
+
Everything `sharedDependencies` names becomes a lookup on the host's instance.
|
|
136
|
+
Anything else you use is bundled, as normal.
|
|
137
|
+
|
|
138
|
+
**Using one of the host's libraries without declaring it fails the build**,
|
|
139
|
+
naming the library and the two ways out. That is deliberate: the alternative is
|
|
140
|
+
a bundle that silently carries its own copy — thirty times the size, with its
|
|
141
|
+
own module state — and nothing anywhere that says so. If the second copy is
|
|
142
|
+
what you want, which is how you use a version the host does not carry, say so:
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
await buildExtension(build, { dir: import.meta.dirname, inline: ['liquidjs'] })
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`extensions/samples/hello-world` is the shortest complete example. Declaring
|
|
149
|
+
what it uses rather than carrying it took the packaged bundle from 185 KB to
|
|
150
|
+
**4 KB**.
|
|
151
|
+
|
|
152
|
+
The preset also settles a trap every author would otherwise hit: the SDK
|
|
153
|
+
reaches `liquidjs`, whose `main`/`module` are its **Node** builds, and
|
|
154
|
+
`platform: 'neutral'` ignores the `browser` field that would pick the right
|
|
155
|
+
one, so a hand-written esbuild config fails on unresolvable Node built-ins
|
|
156
|
+
before it compiles anything — even for an extension that never renders a
|
|
157
|
+
template.
|
|
158
|
+
|
|
159
|
+
## Packaging
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
node extensions/tools/h2kext-pack.mjs <directory> [-o out.h2kext]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The directory holds your built `index.js`, your `manifest.json`, and optionally
|
|
166
|
+
`assets/`. The packer **packages; it does not compile** — building is yours,
|
|
167
|
+
which is what lets this work outside this repo.
|
|
168
|
+
|
|
169
|
+
It reports every problem at once rather than one per run:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
h2kext-pack: ./build is not ready to package:
|
|
173
|
+
manifest.json: missing required field 'name'
|
|
174
|
+
manifest.json: key 'my-clock' must start with your callsign — 'my' is not one (e.g. ki2d-my-clock)
|
|
175
|
+
manifest.json: missing 'api' — declare the extension API this was built against (currently 1)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`--force-name` waives the naming convention below. It exists for Ham2K's own
|
|
179
|
+
packaging, which builds the extensions that ship inside the app under bare keys
|
|
180
|
+
and the reserved prefix; it does not waive what the runtime needs of a key.
|
|
181
|
+
|
|
182
|
+
`extensions/tools/h2kext.mjs` holds the format and its rules, and
|
|
183
|
+
`h2kext-build.mjs` the build preset. Both are free of any dependency on the
|
|
184
|
+
rest of this repo, and ship together as **`@ham2k/extension-tools`** — the
|
|
185
|
+
toolchain half, separate from the **`@ham2k/extension-sdk`** an extension
|
|
186
|
+
imports and bundles.
|
|
187
|
+
|
|
188
|
+
Two things about the SDK package are load-bearing rather than incidental, and
|
|
189
|
+
`extensions/sdk/build.mjs` asserts both:
|
|
190
|
+
|
|
191
|
+
- It ships as **one file per module**, not one bundle. Rolled into a single
|
|
192
|
+
file, your bundler can no longer tell which parts your extension reaches, and
|
|
193
|
+
Hello World comes out five times larger carrying an ADIF importer it cannot
|
|
194
|
+
call.
|
|
195
|
+
- Its JavaScript keeps the shared libraries as **bare specifiers**. The build
|
|
196
|
+
preset can only rewrite an import it can see; resolved inside the SDK, they
|
|
197
|
+
would be gone before it ran, and every extension would carry private copies
|
|
198
|
+
again.
|
|
199
|
+
|
|
200
|
+
`kernel.ts` lives in the SDK's source directory and is never published. It is
|
|
201
|
+
the host — it defines the runtime and holds the one instance of every shared
|
|
202
|
+
library that extensions look up. This repo's own
|
|
203
|
+
build uses the same plugin, which is what keeps the path you take from being
|
|
204
|
+
the untested one.
|
|
205
|
+
|
|
206
|
+
## Keys
|
|
207
|
+
|
|
208
|
+
An extension key is **`<yourcallsign>-<name>`**:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
ki2d-my-clock w7abc-notes 2e0abc-contest-helper
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Lowercase, letters digits and hyphens. The callsign is a namespace every author
|
|
215
|
+
of this software already holds — globally unique, free, and already meaningful
|
|
216
|
+
to everyone here — so two people who never met cannot ship the same key. The
|
|
217
|
+
packer refuses anything else.
|
|
218
|
+
|
|
219
|
+
`ham2k-` is reserved for Ham2K's own extensions, and refused to everyone else.
|
|
220
|
+
There is no signing yet, so anyone could claim it, and the key is what the app
|
|
221
|
+
shows when it says where a hook came from.
|
|
222
|
+
|
|
223
|
+
The extensions built into the app use bare keys (`pota`, `sota`, …). Those are
|
|
224
|
+
reserved too: hook identity has to stay unambiguous, and build-secret
|
|
225
|
+
visibility is by key prefix, so an installed extension calling itself `sota`
|
|
226
|
+
would be asking to read `SOTA_*`. The app refuses any collision at install —
|
|
227
|
+
the packer cannot, because a standalone tool has no way to know what the app it
|
|
228
|
+
will be installed into contains.
|
|
229
|
+
|
|
230
|
+
## What an installed extension does not get
|
|
231
|
+
|
|
232
|
+
**Build secrets.** `host.secret()` returns null for an installed extension
|
|
233
|
+
whatever key it claims, exactly as it does for a dev-served one: visibility
|
|
234
|
+
follows where the code came from, not what it calls itself. A feature that
|
|
235
|
+
needs one degrades the same way it does in a build without that value
|
|
236
|
+
configured.
|
|
237
|
+
|
|
238
|
+
The sandbox is otherwise the same one the built-in extensions run in — no
|
|
239
|
+
filesystem, no network beyond the `domains` the manifest declares and the user
|
|
240
|
+
approved, no timers.
|
|
241
|
+
|
|
242
|
+
## Installing one
|
|
243
|
+
|
|
244
|
+
**Settings → Extensions → Install from file…**, pick the `.h2kext`, and agree
|
|
245
|
+
to what it asks for. The runtime restarts with it loaded, and the extension
|
|
246
|
+
appears in its own category alongside the app's own, marked *Installed* and
|
|
247
|
+
with a way to remove it again.
|
|
248
|
+
|
|
249
|
+
What the operator is shown before anything is written is the whole point of
|
|
250
|
+
the format being a zip: the file is opened and read, and only then unpacked.
|
|
251
|
+
|
|
252
|
+
Installing a bundle whose key is already installed **updates it in place**.
|
|
253
|
+
What it stored survives — an update is not a reinstall, and re-entering
|
|
254
|
+
credentials every time is how people learn not to update.
|
|
255
|
+
|
|
256
|
+
**Every install asks, including an update that wants nothing new.** Comparing
|
|
257
|
+
capabilities and staying quiet when they had not grown would let a version
|
|
258
|
+
through on the strength of its key alone, and nothing about the *code* behind
|
|
259
|
+
that key is compared — a bundle can change completely and still ask for the
|
|
260
|
+
same things. That may be worth revisiting once a bundle can be shown to come
|
|
261
|
+
from whoever it claims; it is not worth it while a key is all there is.
|
|
262
|
+
|
|
263
|
+
Removing an extension removes what it stored with it.
|
|
264
|
+
|
|
265
|
+
## What is checked at install
|
|
266
|
+
|
|
267
|
+
Beyond what the packer already refused:
|
|
268
|
+
|
|
269
|
+
- the key is not one the app ships, and does not begin `ham2k-`;
|
|
270
|
+
- the `api` version is one this build speaks;
|
|
271
|
+
- every `sharedDependencies` range is satisfied by what this build carries;
|
|
272
|
+
- entry names are sanitised on extract, so `../` in one writes nothing.
|
|
273
|
+
|
|
274
|
+
Each has its own message. A bundle that cannot be installed says why.
|
|
275
|
+
|
|
276
|
+
## Native only, for now
|
|
277
|
+
|
|
278
|
+
Installed bundles are files on disk. On web the app runs from its own assets
|
|
279
|
+
and, for authors, the dev server — [development.md](https://github.com/ham2k/halo/blob/main/docs/extensions/development.md).
|