@synopackageland/cli 0.35.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.md +15 -3
- package/assets/go/route-audit/main.go +181 -8
- package/dist/build-spk/install-wizard.d.ts +2 -0
- package/dist/build-spk/install-wizard.js +4 -0
- package/dist/build-spk/wizard-messages.d.ts +22 -0
- package/dist/build-spk/wizard-messages.js +22 -0
- package/dist/cli.js +33 -0
- package/dist/error-registry.js +15 -0
- package/dist/skill.js +5 -0
- package/dist/standalone/api-permissions.d.ts +4 -1
- package/dist/standalone/api-permissions.js +10 -4
- package/dist/standalone/build.js +5 -0
- package/dist/standalone/hyper-backup.d.ts +59 -0
- package/dist/standalone/hyper-backup.js +180 -0
- package/dist/standalone/inspect-spk.d.ts +5 -0
- package/dist/standalone/inspect-spk.js +28 -10
- package/dist/standalone/manifest.d.ts +9 -0
- package/dist/standalone/manifest.js +32 -1
- package/dist/standalone/shared-folder.d.ts +6 -1
- package/dist/standalone/shared-folder.js +13 -0
- package/dist/standalone/spk.d.ts +2 -0
- package/dist/standalone/spk.js +20 -6
- package/package.json +2 -2
- package/skills/SKILL.md +34 -2
- package/skills/reference/backup.md +70 -0
package/dist/standalone/build.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildInstallSettings } from './install-settings.js';
|
|
2
|
+
import { buildAppBackup } from './hyper-backup.js';
|
|
2
3
|
import { auditGoRoutes, httpRoutesDigest } from './route-audit.js';
|
|
3
4
|
import { PORTAL_REQUIRES_EXECUTOR } from "../portal/alias.js";
|
|
4
5
|
import { buildApiDescription } from "./api-description.js";
|
|
@@ -156,6 +157,7 @@ export async function buildStandalone(options) {
|
|
|
156
157
|
const identityGateway = await buildIdentityGateway(manifest, arch, archWork, run);
|
|
157
158
|
const composeControl = await buildComposeControl(manifest, arch, archWork, run);
|
|
158
159
|
const installSettingsControl = await buildInstallSettings(manifest, arch, archWork, run);
|
|
160
|
+
const appBackupControl = await buildAppBackup(manifest, arch, archWork, run, (w) => warnings.add(w));
|
|
159
161
|
const composeStorageInit = await buildComposeStorageInit(manifest, arch, archWork, run);
|
|
160
162
|
// A portal needs the executor, so the executor's SDK carries the panel text.
|
|
161
163
|
const portalText = manifest.portal && executor
|
|
@@ -171,6 +173,7 @@ export async function buildStandalone(options) {
|
|
|
171
173
|
composeControl,
|
|
172
174
|
composeStorageInit,
|
|
173
175
|
installSettingsControl,
|
|
176
|
+
appBackupControl,
|
|
174
177
|
backgroundWorker,
|
|
175
178
|
executor,
|
|
176
179
|
app,
|
|
@@ -254,6 +257,7 @@ async function buildGoCgi(manifest, backend, arch, work, run) {
|
|
|
254
257
|
`-X ${SDK_MODULE}/api.expectedPermissionsDigest=${manifest.apiPermissionsDigest}`,
|
|
255
258
|
`-X ${SDK_MODULE}/api.expectedHTTPRoutesDigest=${httpRoutesDigest(audit)}`,
|
|
256
259
|
`-X ${SDK_MODULE}/ipc.packageID=${manifest.package}`,
|
|
260
|
+
`-X ${SDK_MODULE}/ipc.executorTier=none`,
|
|
257
261
|
...(manifest.entry ? [`-X ${SDK_MODULE}/api.expectedEntryMode=${manifest.entry}`] : []),
|
|
258
262
|
...delegateBuildFlags(manifest),
|
|
259
263
|
...(manifest.executor
|
|
@@ -314,6 +318,7 @@ async function buildBackgroundWorker(manifest, arch, work, run) {
|
|
|
314
318
|
"-buildid=",
|
|
315
319
|
`-X main.packageID=${manifest.package}`,
|
|
316
320
|
`-X ${SDK_MODULE}/ipc.packageID=${manifest.package}`,
|
|
321
|
+
`-X ${SDK_MODULE}/ipc.executorTier=none`,
|
|
317
322
|
`-X ${SDK_MODULE}/api.expectedHTTPRoutesDigest=${httpRoutesDigest(audit)}`,
|
|
318
323
|
`-X ${SDK_MODULE}/api.expectedPermissionsDigest=${manifest.apiPermissionsDigest}`,
|
|
319
324
|
`-X ${SDK_MODULE}/api.expectedSDKPermissionsDigest=${manifest.apiSDKPermissionsDigest}`,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { StandaloneError, StandaloneManifest } from './manifest.js';
|
|
2
|
+
import type { NativeArchFamily } from '../build-spk/dsm-arch.js';
|
|
3
|
+
import type { CommandRunner } from './build.js';
|
|
4
|
+
/**
|
|
5
|
+
* `backup`: Hyper Backup application backup. On by default for native Apps.
|
|
6
|
+
* Hyper Backup runs a package's backup scripts as root, so the scripts are a
|
|
7
|
+
* fixed template that only switches to the package account and moves one
|
|
8
|
+
* archive; the SDK control does the rest as the package account.
|
|
9
|
+
*/
|
|
10
|
+
export interface HyperBackup {
|
|
11
|
+
/** Export while the App runs; SQLite databases are then snapshotted. */
|
|
12
|
+
online: boolean;
|
|
13
|
+
/** "major.minor"; a backup restores into the same major and an equal or newer minor. */
|
|
14
|
+
dataVersion: string;
|
|
15
|
+
/** A package-relative executable run as `<hook> freeze` and `<hook> thaw` around an export. */
|
|
16
|
+
hook?: string;
|
|
17
|
+
/** The App declared `backup`; an SDK without the control is then an error, not a skip. */
|
|
18
|
+
explicit?: true;
|
|
19
|
+
}
|
|
20
|
+
export declare const DEFAULT_HYPER_BACKUP: HyperBackup;
|
|
21
|
+
/** Hyper Backup's plugin folder inside the SPK. */
|
|
22
|
+
export declare const BACKUP_SCRIPTS_DIR = "scripts/backup";
|
|
23
|
+
/** The SDK control and its config inside the package payload. */
|
|
24
|
+
export declare const BACKUP_PAYLOAD_DIR = "backup";
|
|
25
|
+
export declare function readHyperBackup(value: unknown, context: {
|
|
26
|
+
compose?: boolean;
|
|
27
|
+
}, fail: (e: StandaloneError) => void): HyperBackup | undefined;
|
|
28
|
+
/** scripts/backup: framework 1.0, the offline/online choice and the two fixed scripts. */
|
|
29
|
+
export declare function renderBackupScripts(b: Pick<HyperBackup, 'online' | 'dataVersion' | 'hook'>): {
|
|
30
|
+
version: string;
|
|
31
|
+
info: string;
|
|
32
|
+
export: string;
|
|
33
|
+
import: string;
|
|
34
|
+
};
|
|
35
|
+
export declare const BACKUP_SCRIPT_MODES: Record<string, number>;
|
|
36
|
+
export declare function backupConfig(pkg: string, b: HyperBackup): string;
|
|
37
|
+
/** What sdk-package.json records, without the build-only `explicit`. */
|
|
38
|
+
export declare function backupRecord(b: HyperBackup): HyperBackup;
|
|
39
|
+
export declare function readBackupRecord(value: unknown): HyperBackup | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Builds the SDK backup control. An SDK release without it skips the default
|
|
42
|
+
* with a warning; an explicit `backup` declaration then fails.
|
|
43
|
+
*/
|
|
44
|
+
/** The Go package of the SDK backup control; tests use it to tell its build apart. */
|
|
45
|
+
export declare const APP_BACKUP_CONTROL_PACKAGE = "./appbackup/cmd/control";
|
|
46
|
+
export declare function buildAppBackup(m: StandaloneManifest, arch: NativeArchFamily, work: string, run: CommandRunner, warn: (w: string) => void): Promise<string | undefined>;
|
|
47
|
+
/** The hook must be a packaged executable. */
|
|
48
|
+
export declare function backupHookProblem(payload: string, b: HyperBackup): string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Inspect: the record, scripts/backup and payload backup/ must be exactly
|
|
51
|
+
* what build writes, or all absent. Any other file in scripts/backup — such
|
|
52
|
+
* as can_export or info.dynamic, which Hyper Backup runs as root while it only
|
|
53
|
+
* lists applications — is refused. The extracted scripts/backup is removed
|
|
54
|
+
* afterwards so later script checks see the lifecycle scripts alone.
|
|
55
|
+
*/
|
|
56
|
+
export declare function inspectHyperBackup(dir: string, payloadDir: () => Promise<string>, payload: Array<{
|
|
57
|
+
path: string;
|
|
58
|
+
mode: string;
|
|
59
|
+
}>, raw: unknown, pkg: string, arch: NativeArchFamily, findings: string[]): Promise<HyperBackup | undefined>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { sdkSource } from '../runtime/sdk-source.js';
|
|
4
|
+
import { goChildEnv } from '../runtime/go-env.js';
|
|
5
|
+
import { pinnedGoBuildEnv, pinnedGoProblem } from '../build-spk/go-toolchain.js';
|
|
6
|
+
import { checkCgiBinary } from './binary.js';
|
|
7
|
+
import { StandaloneBuildError } from './spk.js';
|
|
8
|
+
import { daemonSdkVersion, relativeExec } from './daemon.js';
|
|
9
|
+
export const DEFAULT_HYPER_BACKUP = Object.freeze({ online: false, dataVersion: '1.0' });
|
|
10
|
+
/** Hyper Backup's plugin folder inside the SPK. */
|
|
11
|
+
export const BACKUP_SCRIPTS_DIR = 'scripts/backup';
|
|
12
|
+
/** The SDK control and its config inside the package payload. */
|
|
13
|
+
export const BACKUP_PAYLOAD_DIR = 'backup';
|
|
14
|
+
const FIELDS = new Set(['hyper_backup', 'online', 'data_version', 'hook']);
|
|
15
|
+
const DATA_VERSION = /^(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})$/;
|
|
16
|
+
const INVALID = 'backup accepts hyper_backup: false (opt out) or online (boolean), data_version ("major.minor", default "1.0") and hook (a fixed executable inside the package).';
|
|
17
|
+
const record = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v);
|
|
18
|
+
export function readHyperBackup(value, context, fail) {
|
|
19
|
+
const bad = () => { fail({ code: 'BACKUP_INVALID', message: INVALID }); return undefined; };
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
return context.compose ? undefined : DEFAULT_HYPER_BACKUP;
|
|
22
|
+
if (!record(value) || Object.keys(value).some(k => !FIELDS.has(k)))
|
|
23
|
+
return bad();
|
|
24
|
+
if (value.hyper_backup !== undefined && typeof value.hyper_backup !== 'boolean')
|
|
25
|
+
return bad();
|
|
26
|
+
if (value.hyper_backup === false)
|
|
27
|
+
return Object.keys(value).length === 1 ? undefined : bad();
|
|
28
|
+
if ((value.online !== undefined && typeof value.online !== 'boolean') ||
|
|
29
|
+
(value.data_version !== undefined && (typeof value.data_version !== 'string' || !DATA_VERSION.test(value.data_version))) ||
|
|
30
|
+
(value.hook !== undefined && !relativeExec(value.hook)))
|
|
31
|
+
return bad();
|
|
32
|
+
if (context.compose) {
|
|
33
|
+
fail({ code: 'BACKUP_COMPOSE_UNSUPPORTED', message: 'Hyper Backup application backup covers the package data folder; Compose App data lives in container volumes, so remove backup or set hyper_backup: false.' });
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return { online: value.online === true, dataVersion: value.data_version ?? '1.0', ...(value.hook ? { hook: value.hook } : {}), explicit: true };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The root-run part. It reads Hyper Backup's temporary folder, refuses a
|
|
40
|
+
* root-owned data folder, switches to the data folder's owner (the package
|
|
41
|
+
* account) and lets that account write or read one archive. It never runs
|
|
42
|
+
* package content as root and never looks inside the archive. When Hyper
|
|
43
|
+
* Backup already runs it as another account, it skips the switch.
|
|
44
|
+
*/
|
|
45
|
+
function shim(action) {
|
|
46
|
+
const io = action === 'export' ? '> "$T/payload.tar"' : '< "$T/payload.tar"';
|
|
47
|
+
return `#!/bin/sh
|
|
48
|
+
# Generated by synopkgland. Hyper Backup runs this as root. It only switches
|
|
49
|
+
# to the package account and moves one archive; it never reads the archive.
|
|
50
|
+
T=$(printf '%s' "$SYNOPKG_BKP_INPUT" | /usr/bin/jq -er .temp_path) || exit 1
|
|
51
|
+
U=$(/usr/bin/stat -c %u:%g "$SYNOPKG_PKGVAR") || exit 1
|
|
52
|
+
case $U in 0:*|*:0) exit 1 ;; esac
|
|
53
|
+
if [ "$(/usr/bin/id -u)" = 0 ]; then RUN="/usr/bin/chroot --userspec=$U /"; else RUN=; fi
|
|
54
|
+
$RUN /bin/sh -c 'exec "$SYNOPKG_PKGDEST/backup/control" ${action}' ${io} || exit 1
|
|
55
|
+
printf '{"app_data_version":"1.0"}' > "$SYNOPKG_BKP_OUTPUT_PATH"
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
/** scripts/backup: framework 1.0, the offline/online choice and the two fixed scripts. */
|
|
59
|
+
export function renderBackupScripts(b) {
|
|
60
|
+
return { version: '1.0\n', info: `{"online_backup":${b.online}}\n`, export: shim('export'), import: shim('import') };
|
|
61
|
+
}
|
|
62
|
+
export const BACKUP_SCRIPT_MODES = { version: 0o644, info: 0o644, export: 0o755, import: 0o755 };
|
|
63
|
+
export function backupConfig(pkg, b) {
|
|
64
|
+
return JSON.stringify({ schema: 1, package: pkg, dataVersion: b.dataVersion, ...(b.online ? { online: true } : {}), ...(b.hook ? { hook: b.hook } : {}) }, null, 2) + '\n';
|
|
65
|
+
}
|
|
66
|
+
/** What sdk-package.json records, without the build-only `explicit`. */
|
|
67
|
+
export function backupRecord(b) {
|
|
68
|
+
return { online: b.online, dataVersion: b.dataVersion, ...(b.hook ? { hook: b.hook } : {}) };
|
|
69
|
+
}
|
|
70
|
+
export function readBackupRecord(value) {
|
|
71
|
+
if (!record(value) || Object.keys(value).some(k => !['online', 'dataVersion', 'hook'].includes(k)) || typeof value.online !== 'boolean' ||
|
|
72
|
+
typeof value.dataVersion !== 'string' || !DATA_VERSION.test(value.dataVersion) || (value.hook !== undefined && !relativeExec(value.hook)))
|
|
73
|
+
return undefined;
|
|
74
|
+
return backupRecord(value);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Builds the SDK backup control. An SDK release without it skips the default
|
|
78
|
+
* with a warning; an explicit `backup` declaration then fails.
|
|
79
|
+
*/
|
|
80
|
+
/** The Go package of the SDK backup control; tests use it to tell its build apart. */
|
|
81
|
+
export const APP_BACKUP_CONTROL_PACKAGE = './appbackup/cmd/control';
|
|
82
|
+
export async function buildAppBackup(m, arch, work, run, warn) {
|
|
83
|
+
if (!m.hyperBackup)
|
|
84
|
+
return;
|
|
85
|
+
const source = sdkSource(daemonSdkVersion(m));
|
|
86
|
+
if (!existsSync(join(source.goDir, 'appbackup/cmd/control/main.go'))) {
|
|
87
|
+
const message = 'The selected SDK has no Hyper Backup application backup; use an SDK release with appbackup.';
|
|
88
|
+
if (m.hyperBackup.explicit)
|
|
89
|
+
throw new StandaloneBuildError('SDK_APP_BACKUP_UNSUPPORTED', message);
|
|
90
|
+
warn(`SDK_APP_BACKUP_UNSUPPORTED ${message} The package is built without application backup.`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const out = join(work, `app-backup-${arch}`);
|
|
94
|
+
const result = await run('go', ['build', '-trimpath', '-buildvcs=false', '-ldflags', '-s -w -buildid=', '-o', out, APP_BACKUP_CONTROL_PACKAGE], { cwd: source.goDir, env: goChildEnv({ ...process.env, ...pinnedGoBuildEnv(arch) }) });
|
|
95
|
+
if (result.missing && !m.hyperBackup.explicit) {
|
|
96
|
+
warn('SDK_APP_BACKUP_BUILD_FAILED Go was not found, so the package is built without application backup; install Go to include it.');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (result.status !== 0)
|
|
100
|
+
throw new StandaloneBuildError('SDK_APP_BACKUP_BUILD_FAILED', `Cannot build the SDK backup control: ${result.stderr}`);
|
|
101
|
+
const error = checkCgiBinary(work, out, arch), toolchain = pinnedGoProblem(out);
|
|
102
|
+
if (error || toolchain)
|
|
103
|
+
throw new StandaloneBuildError('SDK_APP_BACKUP_BUILD_FAILED', error?.message ?? toolchain);
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/** The hook must be a packaged executable. */
|
|
107
|
+
export function backupHookProblem(payload, b) {
|
|
108
|
+
if (!b.hook)
|
|
109
|
+
return;
|
|
110
|
+
const path = join(payload, b.hook);
|
|
111
|
+
try {
|
|
112
|
+
const s = lstatSync(path);
|
|
113
|
+
if (s.isFile() && (s.mode & 0o111) !== 0)
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
catch { /* reported below */ }
|
|
117
|
+
return `backup.hook ${b.hook} is not an executable file in the package.`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Inspect: the record, scripts/backup and payload backup/ must be exactly
|
|
121
|
+
* what build writes, or all absent. Any other file in scripts/backup — such
|
|
122
|
+
* as can_export or info.dynamic, which Hyper Backup runs as root while it only
|
|
123
|
+
* lists applications — is refused. The extracted scripts/backup is removed
|
|
124
|
+
* afterwards so later script checks see the lifecycle scripts alone.
|
|
125
|
+
*/
|
|
126
|
+
export async function inspectHyperBackup(dir, payloadDir, payload, raw, pkg, arch, findings) {
|
|
127
|
+
const fail = (m) => { findings.push(`BACKUP_INVALID ${m}`); };
|
|
128
|
+
const scripts = join(dir, BACKUP_SCRIPTS_DIR);
|
|
129
|
+
const present = payload.filter(p => p.path.startsWith(`${BACKUP_PAYLOAD_DIR}/`));
|
|
130
|
+
const b = raw === undefined ? undefined : readBackupRecord(raw);
|
|
131
|
+
if (raw !== undefined && !b)
|
|
132
|
+
fail('the hyperBackup record in sdk-package.json is not the one synopkgland build writes');
|
|
133
|
+
if (!b) {
|
|
134
|
+
if (lstatExists(scripts))
|
|
135
|
+
fail('scripts/backup is present without a backup declaration');
|
|
136
|
+
if (present.length)
|
|
137
|
+
fail('the payload has a backup folder without a backup declaration');
|
|
138
|
+
rmSync(scripts, { recursive: true, force: true });
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
const expected = renderBackupScripts(b);
|
|
142
|
+
const names = lstatExists(scripts) && lstatSync(scripts).isDirectory() ? readdirSync(scripts).sort() : [];
|
|
143
|
+
if (names.join() !== Object.keys(expected).sort().join())
|
|
144
|
+
fail(`scripts/backup must hold exactly export, import, info and version, found: ${names.join(', ') || 'none'}`);
|
|
145
|
+
for (const [name, text] of Object.entries(expected)) {
|
|
146
|
+
const path = join(scripts, name);
|
|
147
|
+
let s;
|
|
148
|
+
try {
|
|
149
|
+
s = lstatSync(path);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!s.isFile() || (s.mode & 0o777) !== BACKUP_SCRIPT_MODES[name] || readFileSync(path, 'utf8') !== text)
|
|
155
|
+
fail(`scripts/backup/${name} differs from the fixed script synopkgland build writes`);
|
|
156
|
+
}
|
|
157
|
+
const files = present.map(p => `${p.path}:${p.mode}`).sort();
|
|
158
|
+
const wanted = [`${BACKUP_PAYLOAD_DIR}/config.json:0644`, `${BACKUP_PAYLOAD_DIR}/control:0755`];
|
|
159
|
+
if (files.join() !== wanted.join())
|
|
160
|
+
fail('the payload backup folder must hold exactly the SDK control and config.json');
|
|
161
|
+
else {
|
|
162
|
+
const root = await payloadDir();
|
|
163
|
+
if (readFileSync(join(root, BACKUP_PAYLOAD_DIR, 'config.json'), 'utf8') !== backupConfig(pkg, b))
|
|
164
|
+
fail('backup/config.json differs from the backup declaration');
|
|
165
|
+
const error = checkCgiBinary(root, join(root, BACKUP_PAYLOAD_DIR, 'control'), arch);
|
|
166
|
+
if (error)
|
|
167
|
+
fail(error.message);
|
|
168
|
+
}
|
|
169
|
+
rmSync(scripts, { recursive: true, force: true });
|
|
170
|
+
return b;
|
|
171
|
+
}
|
|
172
|
+
function lstatExists(path) {
|
|
173
|
+
try {
|
|
174
|
+
lstatSync(path);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -3,12 +3,15 @@ import { type WebLaunchMode } from "../contract/web-launch.js";
|
|
|
3
3
|
import { type ComposeApp } from "./compose.js";
|
|
4
4
|
import { type ExecutorInspection } from "../runtime/executor-inspect.js";
|
|
5
5
|
import { type AppScriptName } from "./app-scripts.js";
|
|
6
|
+
import { type HyperBackup } from "./hyper-backup.js";
|
|
6
7
|
import { type GatewayAdmissionReport } from "./identity-gateway.js";
|
|
7
8
|
import type { EntryMode } from "./manifest.js";
|
|
8
9
|
import { type UninstallCleanupItem } from "../runtime/uninstall-cleanup.js";
|
|
9
10
|
export type SpkKind = "native-go" | "native-python" | "native-node" | "native-rust" | "binary" | "container" | "native-layout";
|
|
10
11
|
export interface SpkInspection {
|
|
11
12
|
installSettings?: InstallSettings;
|
|
13
|
+
/** Hyper Backup application backup, as sdk-package.json records it. */
|
|
14
|
+
hyperBackup?: HyperBackup;
|
|
12
15
|
identityGateway?: GatewayAdmissionReport;
|
|
13
16
|
kind?: SpkKind;
|
|
14
17
|
package?: string;
|
|
@@ -50,6 +53,8 @@ export interface SpkInspection {
|
|
|
50
53
|
};
|
|
51
54
|
/** Declared `entry` mode; `admin-only` is the administrator floor of every route. */
|
|
52
55
|
entry?: EntryMode;
|
|
56
|
+
/** Service callbacks that skip the DSM session, as listed in the install and upgrade wizards. */
|
|
57
|
+
integrations?: string[];
|
|
53
58
|
/** The declared launch mode verified against the built launcher. */
|
|
54
59
|
launch?: WebLaunchMode;
|
|
55
60
|
/** The App's own scripts/app/<DSM script name>, each called from the generated script of that name. */
|
|
@@ -15,6 +15,7 @@ import { checkBridgeApp, checkBridgeExecutor } from "./inspect-bridge.js";
|
|
|
15
15
|
import { inspectExecutor, inspectExecutorAnchor, } from "../runtime/executor-inspect.js";
|
|
16
16
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
17
17
|
import { sdkScriptOf, unwrapAppScripts } from "./app-scripts.js";
|
|
18
|
+
import { inspectHyperBackup } from "./hyper-backup.js";
|
|
18
19
|
import { tmpdir } from "node:os";
|
|
19
20
|
import { join } from "node:path";
|
|
20
21
|
import * as tar from "tar";
|
|
@@ -31,8 +32,8 @@ import { readUninstallRecord, stateItemTexts } from "./uninstall.js";
|
|
|
31
32
|
import { DSM_LANGS } from "./i18n.js";
|
|
32
33
|
import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, BRIDGE_WORKER_ARGS, backgroundUnitName, renderBackgroundUnit, } from "./background.js";
|
|
33
34
|
import { renderStandaloneResource } from "./resource.js";
|
|
34
|
-
import { recoverSharedFolder,
|
|
35
|
-
import { renderInstallWizardFiles, renderNativeInstallWizard, } from "../build-spk/install-wizard.js";
|
|
35
|
+
import { recoverSharedFolder, renderUpgradeWizardFiles, sharedFolderInstallSteps, SHARED_FOLDER_MARKER, } from "./shared-folder.js";
|
|
36
|
+
import { renderInstallWizardFiles, integrationWizardSteps, renderNativeInstallWizard, } from "../build-spk/install-wizard.js";
|
|
36
37
|
import { renderDelegateScopeSteps, } from "../build-spk/delegate-scope.js";
|
|
37
38
|
import { renderUninstallWizardFiles } from "../build-spk/uninstall-wizard.js";
|
|
38
39
|
import { renderNativePrivilege } from "../build-spk/native-lifecycle.js";
|
|
@@ -125,6 +126,8 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
125
126
|
throw new Error();
|
|
126
127
|
if (raw.operationSeconds !== undefined && (!Number.isInteger(raw.operationSeconds) || raw.operationSeconds < 1 || raw.operationSeconds > 3600))
|
|
127
128
|
throw new Error();
|
|
129
|
+
if (raw.integrations !== undefined && (raw.backend !== 'go' || !Array.isArray(raw.integrations) || !raw.integrations.length || raw.integrations.length > 1024 || raw.integrations.some((m, i) => typeof m !== 'string' || m.length > 128 || !/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(m) || i > 0 && raw.integrations[i - 1] >= m)))
|
|
130
|
+
throw new Error();
|
|
128
131
|
metadata = raw;
|
|
129
132
|
}
|
|
130
133
|
catch {
|
|
@@ -138,10 +141,21 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
138
141
|
const identityGateway = await inspectProviderOnlyAdmission(dir, paths, metadata.provider_only_oidc, findings);
|
|
139
142
|
await inspectExternalGateway(dir, paths, payload, metadata.gateway_external, findings);
|
|
140
143
|
const installSettings = await inspectInstallSettings(dir, payload, metadata, findings);
|
|
144
|
+
const hyperBackup = metadataText === undefined ? undefined : await inspectHyperBackup(dir, async () => {
|
|
145
|
+
const root = join(dir, "backup-inspection");
|
|
146
|
+
if (!existsSync(root)) {
|
|
147
|
+
mkdirSync(root);
|
|
148
|
+
await tar.x({ file: join(dir, "package.tgz"), cwd: root });
|
|
149
|
+
}
|
|
150
|
+
return root;
|
|
151
|
+
}, payload, metadata.hyperBackup, /^package="([^"]*)"$/m.exec(readOptional(join(dir, "INFO")) ?? "")?.[1] ?? "", familyOfArchToken(/^arch="([^"]*)"$/m.exec(readOptional(join(dir, "INFO")) ?? "")?.[1]?.split(/\s+/)[0] ?? "") ?? "x86_64", findings);
|
|
152
|
+
const integrations = metadata.integrations ?? [];
|
|
141
153
|
if (metadata.compose !== undefined) {
|
|
142
154
|
const result = await inspectUnifiedCompose(dir, payload, { ...metadata, installSettings }, findings);
|
|
143
155
|
if (installSettings)
|
|
144
156
|
result.installSettings = installSettings;
|
|
157
|
+
if (hyperBackup)
|
|
158
|
+
result.hyperBackup = hyperBackup;
|
|
145
159
|
if (identityGateway)
|
|
146
160
|
result.identityGateway = identityGateway;
|
|
147
161
|
result.icon = inspectPackageIcons(dir, findings);
|
|
@@ -291,7 +305,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
291
305
|
? await inspectLoginStyle(dir, info.package, Boolean(portal || executor?.policy.portal), findings)
|
|
292
306
|
: undefined;
|
|
293
307
|
if (standalone)
|
|
294
|
-
checkSharedFolder(dir, findings, info.package, sharedFolder, executor?.policy.scope === "install-selected", portal, hasExecutor, nativeGateway, info.os_min_ver, installSettings);
|
|
308
|
+
checkSharedFolder(dir, findings, info.package, sharedFolder, executor?.policy.scope === "install-selected", portal, hasExecutor, nativeGateway, info.os_min_ver, installSettings, integrations);
|
|
295
309
|
const icon = inspectPackageIcons(dir, findings);
|
|
296
310
|
const cgiEntry = payload.find((entry) => entry.path === CGI_PAYLOAD_PATH);
|
|
297
311
|
if (standalone && !cgiEntry) {
|
|
@@ -304,6 +318,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
304
318
|
kind,
|
|
305
319
|
...(identityGateway ? { identityGateway } : {}),
|
|
306
320
|
...(installSettings ? { installSettings } : {}),
|
|
321
|
+
...(hyperBackup ? { hyperBackup } : {}),
|
|
307
322
|
...(launch ? { launch } : {}),
|
|
308
323
|
icon,
|
|
309
324
|
...(executor ? { executor } : {}),
|
|
@@ -319,6 +334,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
319
334
|
}
|
|
320
335
|
: {}),
|
|
321
336
|
...(metadata.entry ? { entry: metadata.entry } : {}),
|
|
337
|
+
...(integrations.length ? { integrations } : {}),
|
|
322
338
|
...(appScripts.length ? { appScripts } : {}),
|
|
323
339
|
...(loginStyle ? { loginStyle } : {}),
|
|
324
340
|
...(standalone
|
|
@@ -1365,7 +1381,7 @@ function checkBackground(dir, info, payload, findings, executor, daemon, bridge
|
|
|
1365
1381
|
* declaration (without one, any present file must be the default native
|
|
1366
1382
|
* install wizard), and no other install/upgrade wizard file may exist.
|
|
1367
1383
|
*/
|
|
1368
|
-
function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal, executor = false, nativeGateway, osMinVer, installSettings) {
|
|
1384
|
+
function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal, executor = false, nativeGateway, osMinVer, installSettings, integrations = []) {
|
|
1369
1385
|
const wizardDir = join(dir, "WIZARD_UIFILES");
|
|
1370
1386
|
const present = (existsSync(wizardDir) ? readdirSync(wizardDir) : [])
|
|
1371
1387
|
.filter((name) => /^(install|upgrade)_uifile(_|\.|$)/.test(name))
|
|
@@ -1391,16 +1407,18 @@ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal,
|
|
|
1391
1407
|
: {}),
|
|
1392
1408
|
...portalManifest(portal),
|
|
1393
1409
|
};
|
|
1394
|
-
const wizards = expectedWizardFiles(pkg, folder, selected, portal, installSettings);
|
|
1410
|
+
const wizards = expectedWizardFiles(pkg, folder, selected, portal, installSettings, integrations);
|
|
1395
1411
|
const source = selected
|
|
1396
1412
|
? "the install-selected policy"
|
|
1397
1413
|
: folder
|
|
1398
1414
|
? "the install.sharedFolder declaration"
|
|
1399
1415
|
: portal
|
|
1400
1416
|
? "the portal declaration"
|
|
1401
|
-
:
|
|
1417
|
+
: integrations.length
|
|
1418
|
+
? "the integrations declaration"
|
|
1419
|
+
: "the native-go default install wizard";
|
|
1402
1420
|
// The default native wizard is optional; a declared shared folder or portal needs all of its files.
|
|
1403
|
-
for (const name of folder || selected || portal || installSettings
|
|
1421
|
+
for (const name of folder || selected || portal || installSettings || integrations.length
|
|
1404
1422
|
? Object.keys(wizards)
|
|
1405
1423
|
: present.filter((n) => n in wizards)) {
|
|
1406
1424
|
if (readOptional(join(wizardDir, name)) !== wizards[name])
|
|
@@ -1461,7 +1479,7 @@ async function inspectInstallSettings(dir, payload, metadata, findings) {
|
|
|
1461
1479
|
}
|
|
1462
1480
|
return settings;
|
|
1463
1481
|
}
|
|
1464
|
-
function expectedWizardFiles(pkg, folder, selected = false, portal, installSettings) {
|
|
1482
|
+
function expectedWizardFiles(pkg, folder, selected = false, portal, installSettings, integrations = []) {
|
|
1465
1483
|
const steps = selected
|
|
1466
1484
|
? (t) => [
|
|
1467
1485
|
...renderDelegateScopeSteps(t),
|
|
@@ -1470,11 +1488,11 @@ function expectedWizardFiles(pkg, folder, selected = false, portal, installSetti
|
|
|
1470
1488
|
: folder
|
|
1471
1489
|
? sharedFolderInstallSteps(pkg, folder)
|
|
1472
1490
|
: renderNativeInstallWizard;
|
|
1473
|
-
const install = renderInstallWizardFiles(pkg, t => [...steps(t), ...settingsInstallSteps(installSettings)(t), ...(portal ? [renderPortalWizardStep(t, portal.alias)] : [])]);
|
|
1491
|
+
const install = renderInstallWizardFiles(pkg, t => [...steps(t), ...settingsInstallSteps(installSettings)(t), ...(portal ? [renderPortalWizardStep(t, portal.alias)] : []), ...integrationWizardSteps(t, integrations)]);
|
|
1474
1492
|
return {
|
|
1475
1493
|
...install.static,
|
|
1476
1494
|
...install.dynamic,
|
|
1477
|
-
...(
|
|
1495
|
+
...renderUpgradeWizardFiles(pkg, folder, integrations),
|
|
1478
1496
|
};
|
|
1479
1497
|
}
|
|
1480
1498
|
function portalManifest(portal) {
|
|
@@ -13,6 +13,7 @@ import { type ComposeApp } from "./compose.js";
|
|
|
13
13
|
import { type Daemon } from "./daemon.js";
|
|
14
14
|
import { type IdentityGateway } from "./identity-gateway.js";
|
|
15
15
|
import { type Uninstall } from "./uninstall.js";
|
|
16
|
+
import { type HyperBackup } from "./hyper-backup.js";
|
|
16
17
|
import { type AppScriptName } from "./app-scripts.js";
|
|
17
18
|
import { type Background } from "./background.js";
|
|
18
19
|
import { type SharedFolder } from "./shared-folder.js";
|
|
@@ -96,8 +97,16 @@ export interface StandaloneManifest {
|
|
|
96
97
|
/** `install.sharedFolder`: the Shared Folder the install wizard selects or `data-share` creates. */
|
|
97
98
|
sharedFolder?: SharedFolder;
|
|
98
99
|
installSettings?: InstallSettings;
|
|
100
|
+
/** `backup`: Hyper Backup application backup; on by default for native Apps, absent when opted out. */
|
|
101
|
+
hyperBackup?: HyperBackup;
|
|
99
102
|
/** Who may use the app at all; absent keeps the route policy alone. */
|
|
100
103
|
entry?: EntryMode;
|
|
104
|
+
/**
|
|
105
|
+
* `integrations`: Go `api.HandleIntegration` methods that arrive without a
|
|
106
|
+
* DSM session, sorted. Exactly the `integration` permissions; the install
|
|
107
|
+
* and upgrade wizards list them for the administrator.
|
|
108
|
+
*/
|
|
109
|
+
integrations?: string[];
|
|
101
110
|
/** `portal`: registers the DSM Application Portal entry; an administrator activates it. */
|
|
102
111
|
portal?: PortalDeclaration;
|
|
103
112
|
/**
|
|
@@ -17,11 +17,12 @@ import { daemonRestartSdkProblem } from "./daemon-restart.js";
|
|
|
17
17
|
import { daemonIngressMinDsmProblem } from "./daemon-ingress.js";
|
|
18
18
|
import { readIdentityGateway, } from "./identity-gateway.js";
|
|
19
19
|
import { readUninstall } from "./uninstall.js";
|
|
20
|
+
import { readHyperBackup } from "./hyper-backup.js";
|
|
20
21
|
import { readAppScripts } from "./app-scripts.js";
|
|
21
22
|
import { readBackground } from "./background.js";
|
|
22
23
|
import { DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, portalByExecutor, portalMinDsmMessage, } from "../build-spk/dsm-version.js";
|
|
23
24
|
import { readSharedFolder } from "./shared-folder.js";
|
|
24
|
-
import { API_PERMISSIONS_FILE, floorAtAdministrators, permitsOrdinaryUsers, readApiPermissions, } from "./api-permissions.js";
|
|
25
|
+
import { API_PERMISSIONS_FILE, floorAtAdministrators, integrationMethods, permitsOrdinaryUsers, readApiPermissions, } from "./api-permissions.js";
|
|
25
26
|
import { PORTAL_REQUIRES_EXECUTOR, portalEntryProblem, readPortalDeclaration, } from "../portal/alias.js";
|
|
26
27
|
import { renderPackageIcons } from "../build-spk/dsm-icons.js";
|
|
27
28
|
import { LOGIN_COLOR, loginImageProblem, } from "../build-spk/login-style.js";
|
|
@@ -58,7 +59,9 @@ export const KNOWN_FIELDS = new Set([
|
|
|
58
59
|
"compose",
|
|
59
60
|
"install",
|
|
60
61
|
"uninstall",
|
|
62
|
+
"backup",
|
|
61
63
|
"entry",
|
|
64
|
+
"integrations",
|
|
62
65
|
"portal",
|
|
63
66
|
"console",
|
|
64
67
|
"icon",
|
|
@@ -223,9 +226,11 @@ function readManifest(raw, c) {
|
|
|
223
226
|
c.fail("STANDALONE_REQUEST_TIMEOUT_SHORT", problem);
|
|
224
227
|
}
|
|
225
228
|
let permissions;
|
|
229
|
+
let integrations;
|
|
226
230
|
if (backend && backend.kind !== "binary") {
|
|
227
231
|
try {
|
|
228
232
|
permissions = readApiPermissions(backend, capabilities);
|
|
233
|
+
integrations = readIntegrations(raw.integrations, backend, permissions.policy, raw.compose !== undefined, c);
|
|
229
234
|
}
|
|
230
235
|
catch (error) {
|
|
231
236
|
// The SDK's own routes come from its module; a failed download or an
|
|
@@ -251,6 +256,7 @@ function readManifest(raw, c) {
|
|
|
251
256
|
? undefined
|
|
252
257
|
: readSharedFolder(raw.install, (e) => c.fail(e.code, e.message));
|
|
253
258
|
const installSettings = readInstallSettings(isRecord(raw.install) ? raw.install.settings : undefined, e => c.fail(e.code, e.message));
|
|
259
|
+
const hyperBackup = readHyperBackup(raw.backup, { compose: raw.compose !== undefined }, e => c.fail(e.code, e.message));
|
|
254
260
|
const compose = arches &&
|
|
255
261
|
readComposeApp(raw.compose, c.root, arches, (e) => c.fail(e.code, e.message), {
|
|
256
262
|
package: packageId,
|
|
@@ -507,7 +513,9 @@ function readManifest(raw, c) {
|
|
|
507
513
|
...(uninstall ? { uninstall } : {}),
|
|
508
514
|
...(sharedFolder ? { sharedFolder } : {}),
|
|
509
515
|
...(installSettings ? { installSettings } : {}),
|
|
516
|
+
...(hyperBackup ? { hyperBackup } : {}),
|
|
510
517
|
...(entry ? { entry } : {}),
|
|
518
|
+
...(integrations?.length ? { integrations } : {}),
|
|
511
519
|
...(portal ? { portal } : {}),
|
|
512
520
|
...(dsmConsole ? { console: dsmConsole } : {}),
|
|
513
521
|
...(noWindow ? { sdkWindow: false } : {}),
|
|
@@ -667,6 +675,29 @@ function sdkVersionOrUndefined(backend) {
|
|
|
667
675
|
* SDK gate and main wires it; `-X main.entry` on an older main would be
|
|
668
676
|
* silently dropped and leave a registered app ungated.
|
|
669
677
|
*/
|
|
678
|
+
/**
|
|
679
|
+
* `integrations` repeats, in dsm-package.yaml, every permissions.json method
|
|
680
|
+
* declared `integration`, so a callback that skips the DSM session is never
|
|
681
|
+
* added by the policy file alone. Only Go SDK servers implement it.
|
|
682
|
+
*/
|
|
683
|
+
function readIntegrations(value, backend, policy, compose, c) {
|
|
684
|
+
const declared = integrationMethods(policy);
|
|
685
|
+
if (value === undefined && declared.length === 0)
|
|
686
|
+
return undefined;
|
|
687
|
+
if (value === undefined)
|
|
688
|
+
value = [];
|
|
689
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== "string") || new Set(value).size !== value.length) {
|
|
690
|
+
return c.fail("STANDALONE_INTEGRATIONS_INVALID", "integrations must be a list of unique permissions.json method names.");
|
|
691
|
+
}
|
|
692
|
+
const listed = [...value].sort();
|
|
693
|
+
if (backend.kind !== "go" || compose) {
|
|
694
|
+
return c.fail("STANDALONE_INTEGRATIONS_INVALID", "integrations need a Go backend built with api.HandleIntegration.");
|
|
695
|
+
}
|
|
696
|
+
if (listed.join("\n") !== declared.join("\n")) {
|
|
697
|
+
return c.fail("STANDALONE_INTEGRATIONS_MISMATCH", `integrations must list exactly the permissions.json methods declared "integration" (${declared.join(", ") || "none"}); found ${listed.join(", ") || "none"}.`);
|
|
698
|
+
}
|
|
699
|
+
return listed;
|
|
700
|
+
}
|
|
670
701
|
function readEntry(value, backend, policy, c) {
|
|
671
702
|
if (value !== "admin-only" && value !== "app-privilege") {
|
|
672
703
|
return c.fail("STANDALONE_ENTRY_INVALID", "entry must be admin-only or app-privilege; omit it to keep the existing permissions.json behavior.");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { StandaloneError } from "./manifest.js";
|
|
2
2
|
import { type Translate } from "../build-spk/wizard-i18n.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type InstallWizardStep } from "../build-spk/install-wizard.js";
|
|
4
4
|
/**
|
|
5
5
|
* `install.sharedFolder`: the Shared Folder a package keeps its files in.
|
|
6
6
|
*
|
|
@@ -33,6 +33,11 @@ export declare function renderSharedFolderWizardFiles(pkg: string, folder: Share
|
|
|
33
33
|
export declare function sharedFolderInstallSteps(pkg: string, folder: SharedFolder): (t: Translate) => InstallWizardStep[];
|
|
34
34
|
/** The upgrade wizard files that let the administrator keep or change the shared folder. */
|
|
35
35
|
export declare function renderSharedFolderUpgradeWizardFiles(pkg: string, folder: SharedFolder): Record<string, string>;
|
|
36
|
+
/**
|
|
37
|
+
* The upgrade wizard: the shared folder choice, then the service callbacks an
|
|
38
|
+
* administrator re-approves on every upgrade. Empty without either.
|
|
39
|
+
*/
|
|
40
|
+
export declare function renderUpgradeWizardFiles(pkg: string, folder: SharedFolder | undefined, integrations?: readonly string[]): Record<string, string>;
|
|
36
41
|
/** `conf/resource` `data-share` entry: create mode only; DSM never deletes the share on uninstall. */
|
|
37
42
|
export declare function sharedFolderResource(pkg: string, folder: SharedFolder): Record<string, unknown> | undefined;
|
|
38
43
|
/** The declaration an SPK's postinst carries, or undefined when it has none or it is no longer valid. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sharedFolderCombobox } from "../build-spk/share-combobox.js";
|
|
2
2
|
import { renderWizardFiles } from "../build-spk/wizard-i18n.js";
|
|
3
|
+
import { integrationWizardSteps } from "../build-spk/install-wizard.js";
|
|
3
4
|
const FIELDS = new Set(["mode", "key", "name", "permission"]);
|
|
4
5
|
const KEY = /^[a-z][a-z0-9_]{0,31}$/;
|
|
5
6
|
const RESERVED_KEY = /^(pkgwizard_|synopkg|change_)/;
|
|
@@ -123,6 +124,18 @@ export function sharedFolderInstallSteps(pkg, folder) {
|
|
|
123
124
|
export function renderSharedFolderUpgradeWizardFiles(pkg, folder) {
|
|
124
125
|
return renderWizardFiles("upgrade_uifile", t => [{ step_title: t("upgrade.step"), items: upgradeItems(t, pkg, folder) }]);
|
|
125
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* The upgrade wizard: the shared folder choice, then the service callbacks an
|
|
129
|
+
* administrator re-approves on every upgrade. Empty without either.
|
|
130
|
+
*/
|
|
131
|
+
export function renderUpgradeWizardFiles(pkg, folder, integrations = []) {
|
|
132
|
+
if (!folder && !integrations.length)
|
|
133
|
+
return {};
|
|
134
|
+
return renderWizardFiles("upgrade_uifile", t => [
|
|
135
|
+
...(folder ? [{ step_title: t("upgrade.step"), items: upgradeItems(t, pkg, folder) }] : []),
|
|
136
|
+
...integrationWizardSteps(t, integrations),
|
|
137
|
+
]);
|
|
138
|
+
}
|
|
126
139
|
/** `conf/resource` `data-share` entry: create mode only; DSM never deletes the share on uninstall. */
|
|
127
140
|
export function sharedFolderResource(pkg, folder) {
|
|
128
141
|
if (folder.mode !== "create")
|
package/dist/standalone/spk.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface AssembleOptions {
|
|
|
18
18
|
composeControl?: string;
|
|
19
19
|
composeStorageInit?: string;
|
|
20
20
|
installSettingsControl?: string;
|
|
21
|
+
/** The SDK backup control; absent builds without Hyper Backup application backup. */
|
|
22
|
+
appBackupControl?: string;
|
|
21
23
|
/** The SDK identity gateway, required exactly when the manifest declares identity_gateway. */
|
|
22
24
|
identityGateway?: string;
|
|
23
25
|
/** The compiled background worker, required exactly when capabilities.jobs is enabled. */
|