@jwilleke/ngdpbase-types 5.35.0 → 5.36.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.
@@ -1,5 +1,13 @@
1
1
  import type { IceLedger, LedgerFileEntry } from './types.js';
2
+ /**
3
+ * The ledger file form (#2013). A scope is bound to its receiver and user
4
+ * (`boundScope`); a file without this form was written before that binding,
5
+ * under a scope any caller could name, so it belongs to nobody: it is read
6
+ * as empty, never as anyone's history, and its files are imported once more.
7
+ */
8
+ export declare const LEDGER_FORM = 2;
2
9
  interface ScopeFile {
10
+ form?: number;
3
11
  scope: string;
4
12
  files: LedgerFileEntry[];
5
13
  items: string[];
@@ -21,8 +21,24 @@ export interface IceRunDeps {
21
21
  }
22
22
  /** The id of the receiver that keeps an original when the chosen receiver does not keep its own (D11). */
23
23
  export declare const FILES_RECEIVER = "core.files";
24
- /** The scope a run's fingerprints are checked in, unless the caller names one. */
25
- export declare function defaultScope(receiverId: string, ctx: ActorContext): string;
24
+ /**
25
+ * The engine's key for an item of a file it stored, so a retry of that file
26
+ * skips it (#2010): the file's fingerprint, the chain, and a hash of the
27
+ * item's content (kind, text, data, bytes). Never its position: a shim that
28
+ * changes or orders its output differently cannot make a retry skip the
29
+ * wrong item. `seen` is how many identical items came before it in this file.
30
+ */
31
+ export declare function itemKeyOf(sha256: string, chain: string[], item: IceItem, seen: number): string;
32
+ /** The form of a bound scope (#2013). Scopes written before it never match one. */
33
+ export declare const SCOPE_FORM = "ice2";
34
+ /**
35
+ * The scope a run's fingerprints, per-file status and item keys live in
36
+ * (#2013): bound to the receiver and to the user the run is for, by their
37
+ * identity `@id` (else their username, #2041). The engine works it out from
38
+ * the caller's context; a request cannot name another user's. A caller's own
39
+ * `sub` (phr's source id) narrows it within that user's scope, never outside.
40
+ */
41
+ export declare function boundScope(receiverId: string, owner: string, sub?: string): string;
26
42
  export interface ChainLeaf {
27
43
  item: IceItem;
28
44
  chain: string[];
@@ -201,7 +201,13 @@ export interface IceReceiver {
201
201
  label?: string;
202
202
  /** The kinds it takes, e.g. `['ncm']`, `['file']`, `['phr.health-record']`. */
203
203
  accepts: IceKind[];
204
- /** May this context use this receiver? Its existing permission — the import screen shows only these (D5). */
204
+ /**
205
+ * May this context use this receiver? Its existing permission (D5). The
206
+ * import screen shows only these, and the engine asks it before any work
207
+ * of a run (#2012): a no refuses every file before anything is unpacked,
208
+ * read by a shim, kept or fetched. Answer no whenever `receive` would
209
+ * refuse this context outright; `receive` still checks each item.
210
+ */
205
211
  mayUse(ctx: ActorContext): Promise<boolean>;
206
212
  /** Hand one item to the receiver's own door. */
207
213
  receive(item: IceItem, ctx: ActorContext, info: ReceiveInfo): Promise<ReceiveOutcome>;
@@ -252,10 +258,13 @@ export interface IceRunRequest {
252
258
  /** D11. Default `ngdpbase.import.engine.default-original`. */
253
259
  original?: OriginalChoice;
254
260
  /**
255
- * Whose fingerprints and item keys this run is checked against. Default
256
- * `<receiver>/<username>`: the same file imported again by the same person
257
- * into the same receiver is skipped. A caller with its own notion of a
258
- * source (phr's source id) passes it.
261
+ * A narrower ledger scope within the caller's own (#2013). The engine binds
262
+ * every run's scope to the receiver and to the user it runs as (their
263
+ * identity `@id`, else their username): the same file imported again by
264
+ * the same user into the same receiver is skipped. A caller with its own
265
+ * notion of a source (phr's source id) passes it to keep those apart; it
266
+ * can never reach another user's or another receiver's scope. Never pass a
267
+ * value a client sent.
259
268
  */
260
269
  scope?: string;
261
270
  }
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { type ActorContext } from '../context/ActorContext.js';
14
14
  import type { BackupFolder } from './BaseManager.js';
15
+ import { type UpdateCheck, type UpdateOutcome } from '../utils/addonInstaller.js';
15
16
  import BaseManager from './BaseManager.js';
16
17
  import type { BackupData } from './BaseManager.js';
17
18
  import type { WikiEngine } from '../types/WikiEngine.js';
@@ -302,6 +303,9 @@ export interface AddonDashboardCard {
302
303
  /** URL of the add-on's admin page */
303
304
  adminUrl: string;
304
305
  }
306
+ /** #2100: the PDP refused an administrator's add-on Update or Check. */
307
+ export declare class AddonUpdateForbiddenError extends Error {
308
+ }
305
309
  /**
306
310
  * Status information returned by getStatus()
307
311
  */
@@ -344,6 +348,10 @@ declare class AddonsManager extends BaseManager {
344
348
  private installedDir;
345
349
  /** #1830: the installed add-ons verified this boot — the only ones loaded from `installedDir`. */
346
350
  private verifiedInstalled;
351
+ /** #2100: installed add-ons this start switched to an Update's release; settled after the load pass. */
352
+ private updatedThisStart;
353
+ /** #2100: Updates run one at a time, so two never write the lock file at once. */
354
+ private updateQueue;
347
355
  /** Stylesheets registered by add-ons via registerStylesheet() */
348
356
  private registeredStylesheets;
349
357
  /** Dashboard cards registered by add-ons via registerDashboardCard() */
@@ -374,6 +382,14 @@ declare class AddonsManager extends BaseManager {
374
382
  * this boot, not the next.
375
383
  */
376
384
  private installConfiguredAddons;
385
+ /**
386
+ * The installer's options for this site (#1830, #2100): the registry and the
387
+ * limits from configuration, and every request — registry, attestations, the
388
+ * Sigstore TUF repository — through the guarded client and the egress policy
389
+ * (#1139). `timeoutKey` is the `…install.<key>` that bounds each request: the
390
+ * boot install's, or the shorter one an admin page waits on.
391
+ */
392
+ private installerOptions;
377
393
  /**
378
394
  * `addon-install` (#1830): written after every check passed and before
379
395
  * anything is written to disk. on-failure: refuse — code does not join the
@@ -381,6 +397,47 @@ declare class AddonsManager extends BaseManager {
381
397
  * (recordSystemAction throws, and the installer writes nothing).
382
398
  */
383
399
  private recordAddonInstall;
400
+ /**
401
+ * #2100: after the load pass, settle each add-on this start switched to an
402
+ * Update's release. Loaded (or switched off, so there was nothing to try):
403
+ * the previous release's folder and tarball go. Not loaded: the previous
404
+ * release is restored on disk and in the lock, so the next start runs it.
405
+ * Core cannot unload an add-on that failed half-way (its modules, routes and
406
+ * jobs stay), so the restored release is not loaded in this process.
407
+ */
408
+ private settleUpdatedAddons;
409
+ /**
410
+ * `addon-update` (#2100) for a fall-back: core moved an installed add-on back
411
+ * from `from` to `to` because `from` did not load. on-failure: continue for
412
+ * this record — the fall-back has already happened on disk, and a lost
413
+ * record must not undo it.
414
+ */
415
+ private recordFallback;
416
+ /** #2100: the door for the admin's add-on questions. Throws when the PDP refuses. */
417
+ private requireAddonPermission;
418
+ /**
419
+ * #2100: for each add-on in `ngdpbase.addons.install`, the pinned release,
420
+ * the configured range, and the newest release in it that is newer — asked
421
+ * of the registry now. Called when an administrator opens the add-ons page
422
+ * or presses Check; never at start. Registry unreachable is an answer
423
+ * (`registryUnreachable`), not an error.
424
+ */
425
+ checkInstalledUpdates(ctx: ActorContext): Promise<UpdateCheck[]>;
426
+ /**
427
+ * #2100: an administrator's Update. Downloads `version` of the installed
428
+ * add-on `slug`, checks it exactly as a first install is checked (sha512,
429
+ * provenance, contents, `ngdpbase.minVersion`) and stages it: it runs from
430
+ * the next start. Asks the PDP for `config-manage`. Recorded as
431
+ * `addon-update` (who, the add-on, from and to) before anything is written;
432
+ * a record that cannot be written stops the Update.
433
+ */
434
+ updateInstalledAddon(ctx: ActorContext, slug: string, version: string): Promise<UpdateOutcome>;
435
+ /**
436
+ * `addon-update` (#2100): an administrator's Update, written after every
437
+ * check passed and before anything is written to disk. on-failure: refuse —
438
+ * a record that cannot be written stops the Update.
439
+ */
440
+ private recordAddonUpdate;
384
441
  /**
385
442
  * `addon-verify-failed` (#1830): an integrity, provenance, tarball, manifest,
386
443
  * lock or folder check refused (or, for a repaired folder, found it changed).
@@ -382,6 +382,18 @@ declare class ImportManager extends BaseManager {
382
382
  importRunsFor(ctx: ActorContext): IceRunReport[];
383
383
  /** Use this ledger instead of the file one: a test's, or a database table's. */
384
384
  setImportLedger(ledger: IceLedger): void;
385
+ /**
386
+ * The ledger scope a run uses (#2013): bound to the receiver and to the user
387
+ * `ctx` names, by their identity `@id`; their username only when the
388
+ * identity store answers that there is no record for them (#2041). Never a
389
+ * scope the caller names: a request's `scope` only narrows within this
390
+ * user's own.
391
+ *
392
+ * @throws IdentityStoreUnavailableError when the identity store cannot
393
+ * answer: the run does not start, and nothing is recorded, rather than
394
+ * land in a scope the user's other runs do not use
395
+ */
396
+ private importScope;
385
397
  private newRunReport;
386
398
  private executeRun;
387
399
  /** What a run, or keeping at upload, works with. */
@@ -85,6 +85,12 @@ export type InstallStatus =
85
85
  | 'verified'
86
86
  /** Already installed; the folder differed from its verified tarball and was rewritten. */
87
87
  | 'repaired'
88
+ /**
89
+ * #2100: switched to the release an administrator's Update staged; the
90
+ * previous release is kept until AddonsManager confirms this one loaded
91
+ * ({@link confirmUpdate}) or falls back to it ({@link rollBackUpdate}).
92
+ */
93
+ | 'updated'
88
94
  /** Not installed (and not loaded); `reason` says why. */
89
95
  | 'skipped';
90
96
  /** Which verification refused, for `addon-verify-failed`. */
@@ -99,6 +105,20 @@ export interface InstallOutcome {
99
105
  registryUnreachable?: boolean;
100
106
  /** Set when a verification refused, or (for `repaired`) found the folder changed. */
101
107
  failedCheck?: FailedCheck;
108
+ /** For `updated`: the release it replaced. */
109
+ previousVersion?: string;
110
+ /**
111
+ * #2100: a switch an earlier start made and never confirmed (the new release
112
+ * did not load, or the process stopped before it could) was undone at this
113
+ * start, before anything loaded: `from` is the release given up, `to` the
114
+ * one restored.
115
+ */
116
+ rolledBack?: {
117
+ from: string;
118
+ to: string;
119
+ };
120
+ /** #2100: something admins should hear about a staged Update, such as one discarded. */
121
+ notice?: string;
102
122
  }
103
123
  /** `sha512-<base64>` of `data`. */
104
124
  export declare function sha512Integrity(data: Buffer): string;
@@ -107,3 +127,73 @@ export declare function sha512Integrity(data: Buffer): string;
107
127
  * Never throws for one add-on's problem: each outcome says what happened.
108
128
  */
109
129
  export declare function installConfiguredAddons(specs: Record<string, unknown>, opts: InstallerOptions): Promise<InstallOutcome[]>;
130
+ /**
131
+ * The switched-to release loaded (or the add-on is off, so there is nothing to
132
+ * try): the previous release's folder and tarball go. No-op when nothing waits.
133
+ */
134
+ export declare function confirmUpdate(addonsDir: string, slug: string): Promise<void>;
135
+ /**
136
+ * The switched-to release did not load: restore the previous release on disk
137
+ * and in the lock, so the next start runs it. The new folder is set aside
138
+ * (`.rejected-…`) for inspection. Undefined when there is nothing to undo.
139
+ */
140
+ export declare function rollBackUpdate(addonsDir: string, slug: string, now?: Date): Promise<{
141
+ from: string;
142
+ to: string;
143
+ } | undefined>;
144
+ /** Why an Update is refused while the last one is not settled (`previous` still in the lock). */
145
+ export declare const FINISH_LAST_UPDATE = "finish the last update first: restart the site";
146
+ /** What the admin page shows for one add-on in `ngdpbase.addons.install`. */
147
+ export interface UpdateCheck {
148
+ slug: string;
149
+ package?: string;
150
+ /** The configured range (`version` in the entry). */
151
+ range?: string;
152
+ /** The release the lock pins: what runs. Absent when not installed. */
153
+ pinned?: string;
154
+ /** A release an Update downloaded, used from the next start. */
155
+ pending?: string;
156
+ /** The newest release in the range, whether or not this core can run it. */
157
+ newest?: string;
158
+ /** The newest release in the range, newer than what is pinned or staged, that this core can run: what Update installs. */
159
+ available?: string;
160
+ /** The newest release in the range, newer than what is pinned or staged, when it needs a newer ngdpbase. */
161
+ needsCore?: {
162
+ version: string;
163
+ minVersion: string;
164
+ };
165
+ registryUnreachable?: boolean;
166
+ /** Why there is nothing to compare: not installed, or a configuration problem. */
167
+ problem?: string;
168
+ /** The last update is not settled (the lock still names the release it replaced): no Update until a restart. */
169
+ blocked?: string;
170
+ }
171
+ /**
172
+ * Ask the registry, for every installed add-on, which releases in its range
173
+ * are newer than the one pinned. Reads the packument only; writes nothing.
174
+ * Called only when an administrator opens the add-ons page or presses Check,
175
+ * never at start.
176
+ */
177
+ export declare function checkForUpdates(specs: Record<string, unknown>, opts: InstallerOptions): Promise<UpdateCheck[]>;
178
+ /** What an Update did. */
179
+ export interface UpdateOutcome {
180
+ slug: string;
181
+ /** `staged`: verified and downloaded; it runs from the next start. `refused`: nothing changed; `reason` says why. */
182
+ status: 'staged' | 'refused';
183
+ package?: string;
184
+ version?: string;
185
+ /** The release that runs until the next start. */
186
+ previousVersion?: string;
187
+ reason?: string;
188
+ registryUnreachable?: boolean;
189
+ failedCheck?: FailedCheck;
190
+ }
191
+ /**
192
+ * Download `version` of an installed add-on and stage it for the next start
193
+ * (#2100). It must be a release in the configured range, newer than the
194
+ * pinned one, that this core can run, and it passes the same checks as a
195
+ * first install ({@link fetchVerified}): sha512, provenance, contents,
196
+ * package.json. `beforeInstall` (the audit record) runs after the checks and
197
+ * before anything is written. The running add-on's folder is not touched.
198
+ */
199
+ export declare function stageUpdate(slug: string, rawSpec: unknown, version: string, opts: InstallerOptions): Promise<UpdateOutcome>;
@@ -7,9 +7,8 @@
7
7
  * folders in `<data>/addons` are installed add-ons at all: a folder the lock
8
8
  * does not name is never an add-on, whatever it contains.
9
9
  */
10
- /** What the lock records for one installed add-on. */
11
- export interface LockEntry {
12
- package: string;
10
+ /** One verified release of an add-on: what it is and what it was accepted on. */
11
+ export interface PinnedVersion {
13
12
  version: string;
14
13
  integrity: string;
15
14
  installedAt: string;
@@ -19,6 +18,23 @@ export interface LockEntry {
19
18
  verifiedAt: string;
20
19
  };
21
20
  }
21
+ /** What the lock records for one installed add-on. */
22
+ export interface LockEntry extends PinnedVersion {
23
+ package: string;
24
+ /**
25
+ * #2100: a newer release an administrator's Update downloaded and verified.
26
+ * Its tarball waits in `.packages/`; the next start switches to it. Nothing
27
+ * else reads it, so until then the site runs `version`.
28
+ */
29
+ pending?: PinnedVersion;
30
+ /**
31
+ * #2100: the release this one replaced at the start that switched to it,
32
+ * kept (its folder as `.previous-<slug>`, its tarball in `.packages/`) until
33
+ * this one has loaded. Present at a later start only when that start never
34
+ * confirmed the switch, so the installer falls back to it.
35
+ */
36
+ previous?: PinnedVersion;
37
+ }
22
38
  export interface LockFile {
23
39
  lockfileVersion: 1;
24
40
  addons: Record<string, LockEntry>;
@@ -26,6 +42,8 @@ export interface LockFile {
26
42
  export declare const LOCK_FILE_NAME = "addons-lock.json";
27
43
  /** Verified tarballs, kept so a restart needs no registry. Dot-named: discovery skips it. */
28
44
  export declare const PACKAGES_DIR_NAME = ".packages";
45
+ /** #2100: the folder a switched-from release waits in until the new one has loaded. Dot-named: discovery skips it. */
46
+ export declare const PREVIOUS_DIR_PREFIX = ".previous-";
29
47
  export declare function lockFilePath(addonsDir: string): string;
30
48
  /** The lock file, or an empty one when there is none. Throws when it exists and is not JSON. */
31
49
  export declare function readLockSync(addonsDir: string): LockFile;
@@ -12,6 +12,8 @@ import path from 'path';
12
12
  export const LOCK_FILE_NAME = 'addons-lock.json';
13
13
  /** Verified tarballs, kept so a restart needs no registry. Dot-named: discovery skips it. */
14
14
  export const PACKAGES_DIR_NAME = '.packages';
15
+ /** #2100: the folder a switched-from release waits in until the new one has loaded. Dot-named: discovery skips it. */
16
+ export const PREVIOUS_DIR_PREFIX = '.previous-';
15
17
  export function lockFilePath(addonsDir) {
16
18
  return path.join(addonsDir, LOCK_FILE_NAME);
17
19
  }
@@ -4,7 +4,7 @@ export declare const AUDIT_EVENT_NAME_PATTERN: RegExp;
4
4
  export declare const AUDIT_EVENT: {
5
5
  /** An add-on named in ngdpbase.addons.install installed into the data folder: package, version, sha512 and the repository its provenance names; recorded before it is written, which cannot proceed without it (#1830) */
6
6
  readonly ADDON_INSTALL: "addon-install";
7
- /** An installed add-on moved to another version by an administrator's Update: who, and from which version to which (#1830; the Update action is not built yet) */
7
+ /** The release an installed add-on runs changed: an administrator's Update (who, the add-on, from and to version), recorded before the release is written, which cannot proceed without it; or core falling back to the previous release because the new one did not load (#2100) */
8
8
  readonly ADDON_UPDATE: "addon-update";
9
9
  /** An add-on refused or set aside by a check: its sha512, provenance, tarball contents, package.json, the lock file, or a changed installed folder (#1830) */
10
10
  readonly ADDON_VERIFY_FAILED: "addon-verify-failed";
@@ -11,7 +11,7 @@ export const AUDIT_EVENT_NAME_PATTERN = /^[a-z]+(-[a-z]+)+$/;
11
11
  export const AUDIT_EVENT = {
12
12
  /** An add-on named in ngdpbase.addons.install installed into the data folder: package, version, sha512 and the repository its provenance names; recorded before it is written, which cannot proceed without it (#1830) */
13
13
  ADDON_INSTALL: 'addon-install',
14
- /** An installed add-on moved to another version by an administrator's Update: who, and from which version to which (#1830; the Update action is not built yet) */
14
+ /** The release an installed add-on runs changed: an administrator's Update (who, the add-on, from and to version), recorded before the release is written, which cannot proceed without it; or core falling back to the previous release because the new one did not load (#2100) */
15
15
  ADDON_UPDATE: 'addon-update',
16
16
  /** An add-on refused or set aside by a check: its sha512, provenance, tarball contents, package.json, the lock file, or a changed installed folder (#1830) */
17
17
  ADDON_VERIFY_FAILED: 'addon-verify-failed',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwilleke/ngdpbase-types",
3
- "version": "5.35.0",
3
+ "version": "5.36.0",
4
4
  "description": "TypeScript types for ngdpbase add-ons, and a test kit of core's own values. A dev dependency, versioned with ngdpbase.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Jim Willeke",