@calimero-network/mero-js 27.0.2 → 27.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/README.md CHANGED
@@ -74,11 +74,14 @@ Pinned measurements go through `policy.verifyQuote` (`createQuoteVerifier`).
74
74
  - **Calls**: `call`, `callConfirmed`, `view`, `events`. A call always carries an idempotency key, and is sent again with it while the node answers `503`.
75
75
  - **Blobs and presence**: `uploadBlob`, `downloadBlob`, `listBlobs`, `blobInfo`, `deleteBlob`; `publishPresence`, `presence`, `presenceStream`.
76
76
  - **Organizations**: `createOrg`, `getOrg`, `deleteOrg`, `setOrgMetadata`, `setMember`, `removeMember`, `setMemberMetadata`, `setCapabilities` (`ORG_CAPABILITIES`), teams, `invite`, `redeemInvite`.
77
- - **Owner decisions** (passkey approval): `upgradeOrg`, `upgradeRun`, `retryUpgradeRun`, `approveRelease`, `approveOutbound`.
77
+ - **Owner decisions** (passkey approval, never from an app's frontend): `upgradeOrg`, `upgradeRun`, `retryUpgradeRun`, `approveRelease`, `approveOutbound`, `bindPublisher`, `upgradePublished`.
78
+ - **Approvals from an app**: `appsReport`, `releaseRules`, `pendingApprovals`, then `requestConsent` / `openConsentPage` to send the user to the node's consent page, and `consentResult`. See [Ask your users to approve node images and app upgrades](https://calimero-network.github.io/mero-js/guides/user-approvals/).
79
+ - **Operator** (ops token): `opsApps`, `opsOrgs`, `setReleaseDeadline`, `liftReleaseDeadline`.
78
80
  - **Any route**: `get`, `post`, `put`, `delete`; `postConfirmed` and `deleteConfirmed` answer a `412 CONFIRM_REQUIRED` with a passkey.
79
81
 
80
82
  Refusals are typed (`LoginRequired`, `NotFound`, `ConfirmRequired`, `ConfirmCancelled`,
81
- `ArgsTooLarge`, `IdempotencyMismatch`, `AppError`, `TrapError`, `UnavailableError`),
83
+ `ArgsTooLarge`, `IdempotencyMismatch`, `ReleaseDeadlineError`, `ConsentStale`, `AppError`, `TrapError`,
84
+ `UnavailableError`),
82
85
  all extending `CalimeroError` with the node's `status` and `code`. See the
83
86
  [error model](https://calimero-network.github.io/mero-js/reference/error-model/).
84
87
 
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Release and upgrade approvals as core's node reports them, and the consent
3
+ * page an app sends its user to when one is theirs to make.
4
+ *
5
+ * An app never approves in its own page. A passkey assertion is blind (the
6
+ * authenticator shows a site name, never what the challenge commits to), so
7
+ * the page that runs the ceremony decides what the person believes they
8
+ * approve. The node therefore refuses every approval route to a session
9
+ * opened on an app's frontend, and serves its own **consent page** instead:
10
+ * the app asks for a request (`POST /consent/requests`), sends the person to
11
+ * the page's `url` on the node's own origin, and reads the decision when the
12
+ * page sends them back (`returnTo?request_id=…&result=…`) or from
13
+ * `GET /consent/{id}/result`. See core's
14
+ * [consent page](https://calimero-network.github.io/core/protocol/consent-page/)
15
+ * and [governance](https://calimero-network.github.io/core/protocol/governance/).
16
+ */
17
+ /**
18
+ * Who holds one decision on a cluster (core's `Authority`), fixed at genesis
19
+ * and attested by every replica. `owner`: the parties around each app, with a
20
+ * passkey (for release approvals the app's publisher, for upgrades whoever
21
+ * its package's manifest names); `operator`: the operator, for every app.
22
+ */
23
+ export type Authority = 'owner' | 'operator';
24
+ /** Core's `Governance`: which side holds each decision on this cluster. */
25
+ export interface Governance {
26
+ releases: Authority;
27
+ appUpgrades: Authority;
28
+ outbound: Authority;
29
+ }
30
+ /**
31
+ * Who upgrades a package's instances, as its signed manifest declares
32
+ * (`upgrades`; absent means `owner`), where the cluster leaves app upgrades to
33
+ * owners. `owner`: each instance's owner (an organization's
34
+ * `MANAGE_APPLICATION` holders); `publisher`: the `MANAGE_APPLICATION`
35
+ * holders of the organization the package's signing key is bound to, for
36
+ * every instance ({@link CalimeroClient.upgradePublished}). A package may move
37
+ * from `publisher` to `owner` in a later release, never back.
38
+ */
39
+ export type UpgradeAuthority = 'owner' | 'publisher';
40
+ /** Whose approvals a report covers: an organization's, or the account's own apps. */
41
+ export type ReportScope = {
42
+ kind: 'account';
43
+ } | {
44
+ kind: 'org';
45
+ org: string;
46
+ };
47
+ /** A package release as a report names it. */
48
+ export interface ReportedRelease {
49
+ package: string;
50
+ version: string;
51
+ /** The service, for a multi-service bundle (account reports only). */
52
+ service?: string | null;
53
+ }
54
+ /** A bundle as a report names it: its digest, and its release where the reader may see it. */
55
+ export interface ReportedBundle {
56
+ bundle: string;
57
+ package?: string;
58
+ version?: string;
59
+ }
60
+ /** Who made an upgrade, by kind. */
61
+ export type ActorKind = 'owner' | 'member' | 'operator';
62
+ /** An app's last upgrade. `actor`, `error` and `run` are left out of the operator's report. */
63
+ export interface LastUpgrade {
64
+ from: ReportedBundle;
65
+ to: ReportedBundle;
66
+ /** Unix seconds. */
67
+ at: number;
68
+ ok: boolean;
69
+ actorKind: ActorKind;
70
+ actor?: string;
71
+ error?: unknown;
72
+ run?: string;
73
+ }
74
+ /** One replica of an app: its node, the node's attested release, and whether the app's set approves it. */
75
+ export interface ReportedReplica {
76
+ node: number;
77
+ /** `null` while the node's release is not known yet (it then holds nothing). */
78
+ release: string | null;
79
+ approved: boolean;
80
+ }
81
+ /** One app in a report. `name`, `team` and `availableVersions` are in members' and accounts' reports only. */
82
+ export interface ReportedApp {
83
+ app: string;
84
+ group: number;
85
+ scope: ReportScope;
86
+ /** `hex(SHA-256)` of the WASM it runs. */
87
+ bundle: string;
88
+ package: ReportedRelease | null;
89
+ /** The package's pinned signer (`did:key`). */
90
+ publisher: {
91
+ signer: string;
92
+ } | null;
93
+ upgradesHeldBy: Authority;
94
+ lastUpgrade: LastUpgrade | null;
95
+ /** The releases approved to hold this app (its publisher's set, or its scope's); `null`: unrestricted. */
96
+ approvedReleases: string[] | null;
97
+ replicas: ReportedReplica[];
98
+ name?: string;
99
+ team?: string;
100
+ /** Newer releases of its package it may be upgraded to, oldest first (at most 16, the newest). */
101
+ availableVersions?: string[];
102
+ }
103
+ /** A mero-tee release the cluster's nodes run, with how many and whether the scope approved it. */
104
+ export interface ClusterRelease {
105
+ release: string;
106
+ nodes: number;
107
+ approved: boolean;
108
+ }
109
+ /** `GET /auth/apps` or `GET /orgs/{org}/apps`: one page. */
110
+ export interface AppsReport {
111
+ scope: ReportScope;
112
+ governance: Governance;
113
+ /** The scope's own approved set, newest last; `null`: none ever recorded (unrestricted). */
114
+ approvedReleases: string[] | null;
115
+ clusterReleases: ClusterRelease[];
116
+ apps: ReportedApp[];
117
+ /** Pass as `after` for the next page; `null` on the last. */
118
+ next: string | null;
119
+ }
120
+ /** `GET /ops/apps`: one page, for the operator. */
121
+ export interface OpsAppsReport {
122
+ /** This node had caught up with the system leader. */
123
+ fresh: boolean;
124
+ governance: Governance;
125
+ apps: ReportedApp[];
126
+ next: string | null;
127
+ }
128
+ /** `GET /ops/orgs`: one page, for the operator. */
129
+ export interface OpsOrgsReport {
130
+ fresh: boolean;
131
+ governance: Governance;
132
+ orgs: Array<{
133
+ org: string;
134
+ approvedReleases: string[] | null;
135
+ }>;
136
+ next: string | null;
137
+ }
138
+ /** A page of a report: entries after the id `after`, at most `limit` (1 to {@link MAX_REPORT_PAGE}). */
139
+ export interface PageOptions {
140
+ after?: string;
141
+ limit?: number;
142
+ }
143
+ /** The most entries one page of a report answers (core's `MAX_REPORT_PAGE`). */
144
+ export declare const MAX_REPORT_PAGE = 100;
145
+ /** `?after=&limit=` for a report route; `limit` outside 1 to 100 is refused before sending. */
146
+ export declare function pageQuery(page?: PageOptions): string;
147
+ /** A release's end-of-new-work deadline. */
148
+ export interface ReleaseDeadline {
149
+ version: string;
150
+ /** RFC 3339. */
151
+ after: string;
152
+ /** Unix seconds. */
153
+ afterSecs: number;
154
+ }
155
+ /** `GET /releases`: the cluster's release rules, public inside the sealed channel. */
156
+ export interface ReleaseRules {
157
+ governance: Governance;
158
+ /** Oldest release first. */
159
+ deadlines: ReleaseDeadline[];
160
+ }
161
+ /** Semver order (core's `compare_versions`); text order for versions that are not semver. */
162
+ export declare function compareVersions(a: string, b: string): number;
163
+ /**
164
+ * Whether a scope whose approved set is `approved` has lost new work at `now`
165
+ * (Unix seconds), as core's `SystemState::stalled` judges it: a release's
166
+ * deadline has passed and the newest approved release is older. Names the
167
+ * newest such release, since approving it lifts every one. `null` approved
168
+ * (unrestricted) is never stalled.
169
+ */
170
+ export declare function stalledBy(approved: string[] | null, deadlines: ReleaseDeadline[], now?: number): ReleaseDeadline | null;
171
+ /** `GET /publishers/{signerId}`: what a key signs to bind it here next. */
172
+ export interface PublisherBindingInfo {
173
+ signerId: string;
174
+ /** The cluster's name, as the binding message names it. */
175
+ cluster: string;
176
+ /** How many bindings the key has had here: a signature binds once. */
177
+ generation: number;
178
+ bound: boolean;
179
+ }
180
+ /**
181
+ * The text a package signing key signs to bind it to `org` as its publisher
182
+ * (core's `system::publisher_binding_message`). Take `cluster` and
183
+ * `generation` from {@link CalimeroClient.publisherBinding}; sign the UTF-8
184
+ * bytes with the key's Ed25519 secret and send
185
+ * {@link CalimeroClient.bindPublisher}.
186
+ */
187
+ export declare function publisherBindingMessage(cluster: string, org: string, signerId: string, generation: number): string;
188
+ /** {@link publisherBindingMessage} as the bytes to sign. */
189
+ export declare function publisherBindingBytes(cluster: string, org: string, signerId: string, generation: number): Uint8Array;
190
+ /** `POST /orgs/{org}/publishers`: the key's proof, base64url without padding. */
191
+ export interface PublisherProof {
192
+ signerId: string;
193
+ /** The Ed25519 public key the `did:key` names. */
194
+ publicKey: string;
195
+ /** Its signature over {@link publisherBindingMessage}. */
196
+ signature: string;
197
+ }
198
+ /** Whose approval a release or outbound request is for. */
199
+ export type ConsentScope = 'account' | {
200
+ org: string;
201
+ };
202
+ /** What the app asks its user to approve (`POST /consent/requests`). Unknown fields are refused. */
203
+ export type ConsentRequest = {
204
+ kind: 'release';
205
+ scope: ConsentScope;
206
+ /** `withdraw: true` asks to withdraw the release's approval instead of approving it. */
207
+ subject: {
208
+ version: string;
209
+ withdraw?: boolean;
210
+ };
211
+ /** Where the page sends the person back: the package's frontend origin, or one its manifest names. */
212
+ returnTo: string;
213
+ } | {
214
+ kind: 'upgrade';
215
+ subject: {
216
+ app: string;
217
+ toVersion: string;
218
+ };
219
+ returnTo: string;
220
+ } | {
221
+ kind: 'outbound';
222
+ scope: ConsentScope;
223
+ /** `package`: a package-bound session's own when left out; `prefixes`: every one the latest release declares. */
224
+ subject: {
225
+ package?: string;
226
+ prefixes?: string[];
227
+ };
228
+ returnTo: string;
229
+ };
230
+ /** The node's answer to a consent request: send the person to `url`. */
231
+ export interface ConsentStarted {
232
+ requestId: string;
233
+ /** The consent page, on the node's own origin. */
234
+ url: string;
235
+ /** Unix seconds; a request lives 10 minutes. */
236
+ expiresAt: number;
237
+ }
238
+ export type ConsentStatus = 'pending' | 'approved' | 'declined';
239
+ /** `GET /consent/{id}/result`. */
240
+ export interface ConsentResult {
241
+ requestId: string;
242
+ kind: ConsentRequest['kind'];
243
+ status: ConsentStatus;
244
+ /** Unix seconds, once decided. */
245
+ decidedAt: number | null;
246
+ }
247
+ /**
248
+ * What the consent page put on `returnTo` when it sent the person back, or
249
+ * `null` when `href` carries none. A hint only (anyone can type a URL): confirm
250
+ * with {@link CalimeroClient.consentResult} before acting on it.
251
+ */
252
+ export declare function consentReturn(href: string | URL): {
253
+ requestId: string;
254
+ result: 'approved' | 'declined';
255
+ } | null;
256
+ /** The public verifier for mero-tee releases (`attestation-verifier`). */
257
+ export declare const RELEASE_VERIFIER = "https://mero-tee.vercel.app";
258
+ /** A mero-tee release's notes on GitHub. */
259
+ export declare function releaseNotesUrl(version: string): string;
260
+ /** The public verifier's page for a mero-tee release. */
261
+ export declare function verifyReleaseUrl(version: string): string;
262
+ /** A mero-tee release the cluster's nodes run that the scope has not approved. */
263
+ export interface PendingRelease {
264
+ version: string;
265
+ /** How many of the cluster's nodes run it. */
266
+ nodes: number;
267
+ /** Whether the scope's side holds release approvals (else the operator does). */
268
+ actionable: boolean;
269
+ releaseNotesUrl: string;
270
+ verifyUrl: string;
271
+ /** Its deadline, when it carries one. */
272
+ deadline: ReleaseDeadline | null;
273
+ }
274
+ /** An app with newer releases of its package. */
275
+ export interface PendingUpgrade {
276
+ app: string;
277
+ name: string | null;
278
+ package: string;
279
+ from: string;
280
+ /** The newest it may move to. */
281
+ to: string;
282
+ /** Every newer release it may move to, oldest first. */
283
+ available: string[];
284
+ /** Whether owners hold app upgrades on this cluster; the node checks the package's own `upgrades` when asked. */
285
+ actionable: boolean;
286
+ }
287
+ /** What {@link CalimeroClient.pendingApprovals} answers. */
288
+ export interface PendingApprovals {
289
+ scope: ReportScope;
290
+ governance: Governance;
291
+ approvedReleases: string[] | null;
292
+ releases: PendingRelease[];
293
+ upgrades: PendingUpgrade[];
294
+ /** A passed deadline has stopped new work for the scope: approve `stalledBy.version` (or newer) to lift it. */
295
+ stalledBy: ReleaseDeadline | null;
296
+ /** Whether anything here is the user's to approve. */
297
+ actionable: boolean;
298
+ }
299
+ /**
300
+ * The pending list from a scope's report pages (every page's apps
301
+ * concatenated into `report.apps`) and the cluster's release rules.
302
+ */
303
+ export declare function pendingApprovalsFrom(report: AppsReport, rules: ReleaseRules, now?: number): PendingApprovals;
304
+ /**
305
+ * The consent request that approves `version` for `scope`, or with
306
+ * `{ withdraw: true }` withdraws its approval ({@link withdrawConsent}).
307
+ */
308
+ export declare function releaseConsent(scope: ReportScope, version: string, returnTo: string, options?: {
309
+ withdraw?: boolean;
310
+ }): ConsentRequest;
311
+ /**
312
+ * The consent request that withdraws `version`'s approval for `scope`: nodes
313
+ * on it are then sent none of the scope's apps' data (nothing already there
314
+ * moves). The page fetches no measurements for it. Withdrawing the scope's
315
+ * last approved release is refused on the page (`400 REJECTED`, the request
316
+ * stays pending), and the page warns before.
317
+ */
318
+ export declare function withdrawConsent(scope: ReportScope, version: string, returnTo: string): ConsentRequest;
319
+ /**
320
+ * The `release` part of a release request's model, as the consent page reads
321
+ * it (`GET /consent/{id}/model`, on the node's origin; the app never needs
322
+ * it). An approval carries the release's signed measurements; a withdrawal
323
+ * carries none, and says whether the release is approved now and whether it
324
+ * is the scope's last approved release (which cannot be withdrawn).
325
+ */
326
+ export type ConsentReleaseModel = {
327
+ version: string;
328
+ change: 'approve';
329
+ alreadyApproved: boolean;
330
+ approved: unknown;
331
+ nodes: unknown;
332
+ profile: string | null;
333
+ measurements: Record<string, unknown>;
334
+ } | {
335
+ version: string;
336
+ change: 'withdraw';
337
+ currentlyApproved: boolean;
338
+ lastApproved: boolean;
339
+ approved: unknown;
340
+ nodes: unknown;
341
+ profile: string | null;
342
+ };
343
+ /** The consent request that upgrades `app` to `toVersion`. */
344
+ export declare function upgradeConsent(app: string, toVersion: string, returnTo: string): ConsentRequest;
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Release and upgrade approvals as core's node reports them, and the consent
3
+ * page an app sends its user to when one is theirs to make.
4
+ *
5
+ * An app never approves in its own page. A passkey assertion is blind (the
6
+ * authenticator shows a site name, never what the challenge commits to), so
7
+ * the page that runs the ceremony decides what the person believes they
8
+ * approve. The node therefore refuses every approval route to a session
9
+ * opened on an app's frontend, and serves its own **consent page** instead:
10
+ * the app asks for a request (`POST /consent/requests`), sends the person to
11
+ * the page's `url` on the node's own origin, and reads the decision when the
12
+ * page sends them back (`returnTo?request_id=…&result=…`) or from
13
+ * `GET /consent/{id}/result`. See core's
14
+ * [consent page](https://calimero-network.github.io/core/protocol/consent-page/)
15
+ * and [governance](https://calimero-network.github.io/core/protocol/governance/).
16
+ */
17
+ import { compareReleaseVersions } from '../sealed/release.js';
18
+ /** The most entries one page of a report answers (core's `MAX_REPORT_PAGE`). */
19
+ export const MAX_REPORT_PAGE = 100;
20
+ /** `?after=&limit=` for a report route; `limit` outside 1 to 100 is refused before sending. */
21
+ export function pageQuery(page = {}) {
22
+ const q = new URLSearchParams();
23
+ if (page.after !== undefined)
24
+ q.set('after', page.after);
25
+ if (page.limit !== undefined) {
26
+ if (!Number.isInteger(page.limit) || page.limit < 1 || page.limit > MAX_REPORT_PAGE) {
27
+ throw new RangeError(`limit is 1 to ${MAX_REPORT_PAGE}`);
28
+ }
29
+ q.set('limit', String(page.limit));
30
+ }
31
+ const s = q.toString();
32
+ return s ? `?${s}` : '';
33
+ }
34
+ /** Semver order (core's `compare_versions`); text order for versions that are not semver. */
35
+ export function compareVersions(a, b) {
36
+ try {
37
+ return compareReleaseVersions(a, b);
38
+ }
39
+ catch {
40
+ return a < b ? -1 : a > b ? 1 : 0;
41
+ }
42
+ }
43
+ /**
44
+ * Whether a scope whose approved set is `approved` has lost new work at `now`
45
+ * (Unix seconds), as core's `SystemState::stalled` judges it: a release's
46
+ * deadline has passed and the newest approved release is older. Names the
47
+ * newest such release, since approving it lifts every one. `null` approved
48
+ * (unrestricted) is never stalled.
49
+ */
50
+ export function stalledBy(approved, deadlines, now = Date.now() / 1000) {
51
+ if (approved === null || approved.length === 0)
52
+ return null;
53
+ const newest = approved.reduce((a, b) => (compareVersions(a, b) >= 0 ? a : b));
54
+ let stall = null;
55
+ for (const d of deadlines) {
56
+ if (d.afterSecs <= now && compareVersions(newest, d.version) < 0) {
57
+ if (stall === null || compareVersions(d.version, stall.version) > 0)
58
+ stall = d;
59
+ }
60
+ }
61
+ return stall;
62
+ }
63
+ /**
64
+ * The text a package signing key signs to bind it to `org` as its publisher
65
+ * (core's `system::publisher_binding_message`). Take `cluster` and
66
+ * `generation` from {@link CalimeroClient.publisherBinding}; sign the UTF-8
67
+ * bytes with the key's Ed25519 secret and send
68
+ * {@link CalimeroClient.bindPublisher}.
69
+ */
70
+ export function publisherBindingMessage(cluster, org, signerId, generation) {
71
+ return `calimero-publisher-binding/v1\n${cluster}\n${org}\n${signerId}\n${generation}`;
72
+ }
73
+ /** {@link publisherBindingMessage} as the bytes to sign. */
74
+ export function publisherBindingBytes(cluster, org, signerId, generation) {
75
+ return new TextEncoder().encode(publisherBindingMessage(cluster, org, signerId, generation));
76
+ }
77
+ /**
78
+ * What the consent page put on `returnTo` when it sent the person back, or
79
+ * `null` when `href` carries none. A hint only (anyone can type a URL): confirm
80
+ * with {@link CalimeroClient.consentResult} before acting on it.
81
+ */
82
+ export function consentReturn(href) {
83
+ const url = typeof href === 'string' ? new URL(href) : href;
84
+ const requestId = url.searchParams.get('request_id');
85
+ const result = url.searchParams.get('result');
86
+ if (!requestId || (result !== 'approved' && result !== 'declined'))
87
+ return null;
88
+ return { requestId, result };
89
+ }
90
+ // ---------------------------------------------------------------- pending
91
+ /** The public verifier for mero-tee releases (`attestation-verifier`). */
92
+ export const RELEASE_VERIFIER = 'https://mero-tee.vercel.app';
93
+ /** A mero-tee release's notes on GitHub. */
94
+ export function releaseNotesUrl(version) {
95
+ return `https://github.com/calimero-network/mero-tee/releases/tag/mero-tee-v${encodeURIComponent(version)}`;
96
+ }
97
+ /** The public verifier's page for a mero-tee release. */
98
+ export function verifyReleaseUrl(version) {
99
+ return `${RELEASE_VERIFIER}/mero-tee?release_tag=${encodeURIComponent(`mero-tee-v${version}`)}`;
100
+ }
101
+ /**
102
+ * The pending list from a scope's report pages (every page's apps
103
+ * concatenated into `report.apps`) and the cluster's release rules.
104
+ */
105
+ export function pendingApprovalsFrom(report, rules, now = Date.now() / 1000) {
106
+ const deadlineOf = (v) => rules.deadlines.find((d) => d.version === v) ?? null;
107
+ const releasesActionable = report.governance.releases === 'owner';
108
+ const upgradesActionable = report.governance.appUpgrades === 'owner';
109
+ const releases = report.clusterReleases
110
+ .filter((r) => !r.approved)
111
+ .sort((a, b) => compareVersions(b.release, a.release))
112
+ .map((r) => ({
113
+ version: r.release,
114
+ nodes: r.nodes,
115
+ actionable: releasesActionable,
116
+ releaseNotesUrl: releaseNotesUrl(r.release),
117
+ verifyUrl: verifyReleaseUrl(r.release),
118
+ deadline: deadlineOf(r.release),
119
+ }));
120
+ const upgrades = [];
121
+ for (const a of report.apps) {
122
+ const available = a.availableVersions ?? [];
123
+ if (a.package === null || available.length === 0)
124
+ continue;
125
+ upgrades.push({
126
+ app: a.app,
127
+ name: a.name ?? null,
128
+ package: a.package.package,
129
+ from: a.package.version,
130
+ to: available[available.length - 1],
131
+ available,
132
+ actionable: upgradesActionable,
133
+ });
134
+ }
135
+ return {
136
+ scope: report.scope,
137
+ governance: report.governance,
138
+ approvedReleases: report.approvedReleases,
139
+ releases,
140
+ upgrades,
141
+ stalledBy: stalledBy(report.approvedReleases, rules.deadlines, now),
142
+ actionable: releases.some((r) => r.actionable) || upgrades.some((u) => u.actionable),
143
+ };
144
+ }
145
+ /**
146
+ * The consent request that approves `version` for `scope`, or with
147
+ * `{ withdraw: true }` withdraws its approval ({@link withdrawConsent}).
148
+ */
149
+ export function releaseConsent(scope, version, returnTo, options = {}) {
150
+ return {
151
+ kind: 'release',
152
+ scope: scope.kind === 'org' ? { org: scope.org } : 'account',
153
+ subject: options.withdraw ? { version, withdraw: true } : { version },
154
+ returnTo,
155
+ };
156
+ }
157
+ /**
158
+ * The consent request that withdraws `version`'s approval for `scope`: nodes
159
+ * on it are then sent none of the scope's apps' data (nothing already there
160
+ * moves). The page fetches no measurements for it. Withdrawing the scope's
161
+ * last approved release is refused on the page (`400 REJECTED`, the request
162
+ * stays pending), and the page warns before.
163
+ */
164
+ export function withdrawConsent(scope, version, returnTo) {
165
+ return releaseConsent(scope, version, returnTo, { withdraw: true });
166
+ }
167
+ /** The consent request that upgrades `app` to `toVersion`. */
168
+ export function upgradeConsent(app, toVersion, returnTo) {
169
+ return { kind: 'upgrade', subject: { app, toVersion }, returnTo };
170
+ }
@@ -12,7 +12,7 @@ export declare const ORG_CAPABILITIES: {
12
12
  readonly JOIN_OPEN_TEAMS: number;
13
13
  /** Add and remove a team's members. */
14
14
  readonly MANAGE_MEMBERS: number;
15
- /** Upgrade the organization's apps, start org upgrade runs, approve outbound hosts (org-wide). */
15
+ /** Upgrade the organization's apps (and, as a publisher, every instance of its `publisher` packages), start org upgrade runs, approve outbound hosts (org-wide). */
16
16
  readonly MANAGE_APPLICATION: number;
17
17
  /** Create a top-level team without being an admin. */
18
18
  readonly CREATE_TEAM: number;
@@ -22,6 +22,13 @@ export declare const ORG_CAPABILITIES: {
22
22
  readonly MANAGE_VISIBILITY: number;
23
23
  /** Set the organization's and its teams' metadata. */
24
24
  readonly MANAGE_METADATA: number;
25
+ /**
26
+ * Approve (or withdraw) the mero-tee releases that hold the apps the
27
+ * organization governs (those of the packages it publishes, and its own of
28
+ * unbound packages), and bind a package signing key to it
29
+ * (`bindPublisher`). Organization-wide only. Bit 9 is left unused.
30
+ */
31
+ readonly APPROVE_RELEASES: number;
25
32
  };
26
33
  export type OrgCapability = keyof typeof ORG_CAPABILITIES;
27
34
  /** What a member holds unless an admin says otherwise. */
@@ -12,7 +12,7 @@ export const ORG_CAPABILITIES = {
12
12
  JOIN_OPEN_TEAMS: 1 << 2,
13
13
  /** Add and remove a team's members. */
14
14
  MANAGE_MEMBERS: 1 << 3,
15
- /** Upgrade the organization's apps, start org upgrade runs, approve outbound hosts (org-wide). */
15
+ /** Upgrade the organization's apps (and, as a publisher, every instance of its `publisher` packages), start org upgrade runs, approve outbound hosts (org-wide). */
16
16
  MANAGE_APPLICATION: 1 << 4,
17
17
  /** Create a top-level team without being an admin. */
18
18
  CREATE_TEAM: 1 << 5,
@@ -22,6 +22,13 @@ export const ORG_CAPABILITIES = {
22
22
  MANAGE_VISIBILITY: 1 << 7,
23
23
  /** Set the organization's and its teams' metadata. */
24
24
  MANAGE_METADATA: 1 << 8,
25
+ /**
26
+ * Approve (or withdraw) the mero-tee releases that hold the apps the
27
+ * organization governs (those of the packages it publishes, and its own of
28
+ * unbound packages), and bind a package signing key to it
29
+ * (`bindPublisher`). Organization-wide only. Bit 9 is left unused.
30
+ */
31
+ APPROVE_RELEASES: 1 << 10,
25
32
  };
26
33
  /** What a member holds unless an admin says otherwise. */
27
34
  export const DEFAULT_ORG_CAPABILITIES = ORG_CAPABILITIES.JOIN_OPEN_TEAMS;