@synopackageland/cli 0.9.6 → 0.10.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 +144 -5
- package/README.md +5 -0
- package/dist/add.js +7 -8
- package/dist/build-spk/dsm-arch.js +1 -1
- package/dist/build-spk/dsm-icons.d.ts +8 -1
- package/dist/build-spk/dsm-icons.js +26 -12
- package/dist/build-spk/dsm-version.d.ts +1 -1
- package/dist/build-spk/dsm-version.js +4 -4
- package/dist/build-spk/gateway.js +2 -2
- package/dist/build-spk/generator.js +1 -1
- package/dist/build-spk/png.js +16 -16
- package/dist/capabilities.js +1 -1
- package/dist/capability-overlays.d.ts +16 -5
- package/dist/capability-overlays.js +30 -16
- package/dist/cli.js +107 -107
- package/dist/compose/analyze.js +23 -23
- package/dist/compose/privilege.js +12 -12
- package/dist/contract/i18n.js +3 -3
- package/dist/contract/icon.js +2 -2
- package/dist/contract/posix-all-shares.js +2 -2
- package/dist/contract/schema-validate.js +3 -3
- package/dist/contract/validate.js +16 -16
- package/dist/contract/web-launch.d.ts +2 -1
- package/dist/contract/web-launch.js +3 -2
- package/dist/deploy/deploy.js +8 -8
- package/dist/deploy/homes-values.js +1 -1
- package/dist/deploy/identity-shared.js +1 -1
- package/dist/deploy/native-install-values.js +1 -1
- package/dist/deploy/param-values.js +4 -4
- package/dist/deploy/plan.d.ts +1 -1
- package/dist/deploy/port-preflight.js +3 -3
- package/dist/deploy/run-as-values.js +2 -2
- package/dist/deploy/share-permission-preflight.js +6 -6
- package/dist/deploy/share-values.js +3 -3
- package/dist/error-registry.js +369 -346
- package/dist/errors.js +1 -1
- package/dist/host/dsm-port-profile.d.ts +1 -1
- package/dist/host/dsm-port-profile.js +1 -1
- package/dist/info.js +11 -11
- package/dist/init.d.ts +2 -0
- package/dist/init.js +85 -38
- package/dist/inspect.js +5 -5
- package/dist/portal/alias.d.ts +1 -1
- package/dist/portal/alias.js +4 -4
- package/dist/runtime/executor-build.js +12 -1
- package/dist/runtime/executor-digests.json +8 -85
- package/dist/runtime/executor-inspect.js +11 -4
- package/dist/runtime/executor-policy.d.ts +9 -2
- package/dist/runtime/executor-policy.js +27 -8
- package/dist/runtime/root-programs.d.ts +47 -0
- package/dist/runtime/root-programs.js +153 -0
- package/dist/runtime/sdk-source.d.ts +5 -4
- package/dist/runtime/sdk-source.js +8 -4
- package/dist/skill.js +14 -14
- package/dist/spk/check-info.js +17 -17
- package/dist/spk/check-metadata.js +18 -18
- package/dist/spk/check-payload.js +10 -10
- package/dist/spk/rules.d.ts +57 -57
- package/dist/spk/rules.js +50 -50
- package/dist/spk/validate-spk.js +5 -5
- package/dist/standalone/api-permissions.js +1 -1
- package/dist/standalone/background.d.ts +3 -1
- package/dist/standalone/background.js +8 -4
- package/dist/standalone/binary.js +7 -7
- package/dist/standalone/bridge-app.d.ts +23 -0
- package/dist/standalone/bridge-app.js +141 -0
- package/dist/standalone/bridge-backend.d.ts +40 -0
- package/dist/standalone/bridge-backend.js +95 -0
- package/dist/standalone/bridge-build.d.ts +21 -0
- package/dist/standalone/bridge-build.js +62 -0
- package/dist/standalone/bridge-json.d.ts +16 -0
- package/dist/standalone/bridge-json.js +59 -0
- package/dist/standalone/bridge-runtime.d.ts +16 -0
- package/dist/standalone/bridge-runtime.js +46 -0
- package/dist/standalone/bridge-rust.d.ts +16 -0
- package/dist/standalone/bridge-rust.js +89 -0
- package/dist/standalone/build.js +18 -14
- package/dist/standalone/daemon.js +6 -6
- package/dist/standalone/dsm-window.d.ts +10 -5
- package/dist/standalone/dsm-window.js +64 -16
- package/dist/standalone/i18n.js +18 -18
- package/dist/standalone/inspect-bridge.d.ts +31 -0
- package/dist/standalone/inspect-bridge.js +111 -0
- package/dist/standalone/inspect-spk.d.ts +1 -1
- package/dist/standalone/inspect-spk.js +77 -69
- package/dist/standalone/lifecycle.js +22 -3
- package/dist/standalone/manifest.d.ts +17 -9
- package/dist/standalone/manifest.js +143 -65
- package/dist/standalone/portal-window.js +14 -7
- package/dist/standalone/request-timeout.d.ts +9 -0
- package/dist/standalone/request-timeout.js +13 -0
- package/dist/standalone/root-ops.d.ts +23 -2
- package/dist/standalone/root-ops.js +118 -13
- package/dist/standalone/sdk-check.js +4 -4
- package/dist/standalone/shared-folder.js +7 -7
- package/dist/standalone/spk.d.ts +2 -0
- package/dist/standalone/spk.js +30 -11
- package/dist/standalone/uninstall-hook.js +22 -4
- package/dist/standalone/uninstall.js +7 -7
- package/dist/standalone/window-size.d.ts +17 -0
- package/dist/standalone/window-size.js +31 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +13 -13
- package/package.json +2 -2
- package/schema/bridge-manifest.v1.schema.json +86 -0
- package/skills/reference/create.md +76 -2
- package/skills/reference/ui-json.md +2 -1
- package/skills/reference/ui-vue.md +9 -3
- package/skills/reference/wrap.md +1 -1
- package/dist/standalone/rust-build.d.ts +0 -35
- package/dist/standalone/rust-build.js +0 -127
|
@@ -5,13 +5,16 @@ import { type NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
|
5
5
|
import { type Catalogs, type InfoTranslations } from "./i18n.js";
|
|
6
6
|
export type { InfoText } from "./i18n.js";
|
|
7
7
|
import { type RootOp } from "./root-ops.js";
|
|
8
|
+
import type { RootSettings } from "../runtime/root-programs.js";
|
|
8
9
|
import { type Daemon } from "./daemon.js";
|
|
9
10
|
import { type Uninstall } from "./uninstall.js";
|
|
10
11
|
import { type Background } from "./background.js";
|
|
11
12
|
import { type SharedFolder } from "./shared-folder.js";
|
|
12
|
-
import { type ApiPermissions } from "./api-permissions.js";
|
|
13
|
+
import { readApiPermissions, type ApiPermissions } from "./api-permissions.js";
|
|
13
14
|
import { type PortalDeclaration } from "../portal/alias.js";
|
|
14
15
|
import { type SdkBackend } from "../runtime/sdk-source.js";
|
|
16
|
+
import { type WindowSize } from "./window-size.js";
|
|
17
|
+
import { type BridgeBackend } from "./bridge-backend.js";
|
|
15
18
|
/**
|
|
16
19
|
* `dsm-package.yaml`: a Standalone Package, i.e. an unsigned native SPK that
|
|
17
20
|
* runs as its own DSM package account with a CGI backend and a DSM window UI.
|
|
@@ -21,16 +24,11 @@ export type StandaloneBackend = {
|
|
|
21
24
|
kind: "go";
|
|
22
25
|
dir: string;
|
|
23
26
|
main: string;
|
|
24
|
-
|
|
25
|
-
kind: "rust";
|
|
26
|
-
dir: string;
|
|
27
|
-
package: string;
|
|
28
|
-
bin: string;
|
|
29
|
-
worker: string;
|
|
27
|
+
timeoutSeconds?: number;
|
|
30
28
|
} | {
|
|
31
29
|
kind: "binary";
|
|
32
30
|
files: Partial<Record<NativeArchFamily, string>>;
|
|
33
|
-
};
|
|
31
|
+
} | BridgeBackend;
|
|
34
32
|
export interface StandaloneI18n {
|
|
35
33
|
/** UI messages per locale, embedded into the DSM window. */
|
|
36
34
|
catalogs?: Catalogs;
|
|
@@ -55,17 +53,21 @@ export interface StandaloneManifest {
|
|
|
55
53
|
apiPermissions?: ApiPermissions;
|
|
56
54
|
apiPermissionsDigest?: string;
|
|
57
55
|
apiSDKPermissionsDigest?: string;
|
|
56
|
+
/** `ui.window`: the DSM window's size over the defaults; absent keeps 960×640, at least 700×420. */
|
|
58
57
|
ui: {
|
|
59
58
|
dir: string;
|
|
60
59
|
build?: {
|
|
61
60
|
cwd: string;
|
|
62
61
|
command: string[];
|
|
63
62
|
};
|
|
63
|
+
window?: WindowSize;
|
|
64
64
|
};
|
|
65
65
|
i18n?: StandaloneI18n;
|
|
66
66
|
capabilities: Capabilities;
|
|
67
67
|
/** Fixed root/<name>.sh operations, enabled by capabilities.rootOps. */
|
|
68
68
|
rootOps?: RootOp[];
|
|
69
|
+
/** `root`: the root programs' settings, the stop step and the root state directory. */
|
|
70
|
+
rootPrograms?: RootSettings;
|
|
69
71
|
/** Set by build: the pins and the root tasks generated from the App's SDK executor. */
|
|
70
72
|
executor?: ExecutorPins & {
|
|
71
73
|
tasks: ExecutorTask[];
|
|
@@ -107,7 +109,13 @@ export type ManifestResult = {
|
|
|
107
109
|
};
|
|
108
110
|
export declare const KNOWN_FIELDS: Set<string>;
|
|
109
111
|
export declare function loadStandaloneManifest(root: string): ManifestResult;
|
|
110
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* The runtime registers routes from the manifest's methods: a policy for
|
|
114
|
+
* another method, or an App method with an SDK name, would refuse to start.
|
|
115
|
+
* Without methods only the SDK names of the policy can be checked.
|
|
116
|
+
*/
|
|
117
|
+
export declare function bridgeMethodProblems(methods: readonly string[] | undefined, permissions: Pick<ReturnType<typeof readApiPermissions>, "policy" | "sdk">, file: string): string[];
|
|
118
|
+
/** Where an App's SDK version is declared: its Go backend; a prebuilt binary declares it in the project's go.mod. */
|
|
111
119
|
export declare function sdkBackend(m: Pick<StandaloneManifest, "backend" | "root">): SdkBackend;
|
|
112
120
|
/**
|
|
113
121
|
* The nearest directory at or above `start` that holds dsm-package.yaml,
|
|
@@ -5,7 +5,7 @@ import { isAbsolute, join, relative, resolve } from "node:path";
|
|
|
5
5
|
import { parse } from "yaml";
|
|
6
6
|
import { isNativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
7
7
|
import { checkGlossary, loadCatalogs, readInfoTranslations } from "./i18n.js";
|
|
8
|
-
import { loadRootOps, ROOT_OPS_DIR } from "./root-ops.js";
|
|
8
|
+
import { loadRootOps, readRootSettings, rootBinaryProblem, ROOT_OPS_DIR } from "./root-ops.js";
|
|
9
9
|
import { readDaemon } from "./daemon.js";
|
|
10
10
|
import { readUninstall } from "./uninstall.js";
|
|
11
11
|
import { readBackground } from "./background.js";
|
|
@@ -15,8 +15,11 @@ import { API_PERMISSIONS_FILE, floorAtAdministrators, permitsOrdinaryUsers, read
|
|
|
15
15
|
import { PORTAL_REQUIRES_EXECUTOR, portalEntryProblem, readPortalDeclaration } from "../portal/alias.js";
|
|
16
16
|
import { renderPackageIcons } from "../build-spk/dsm-icons.js";
|
|
17
17
|
import { validateReproducible } from "../reproducible-config.js";
|
|
18
|
-
import { appSdkVersion,
|
|
18
|
+
import { appSdkVersion, SDK_MODULE } from "../runtime/sdk-source.js";
|
|
19
19
|
import { sdkProblems } from "./sdk-check.js";
|
|
20
|
+
import { readWindowSize } from "./window-size.js";
|
|
21
|
+
import { TIMEOUT_USAGE, validTimeoutSeconds } from "./request-timeout.js";
|
|
22
|
+
import { BRIDGE_RUNTIMES, isBridgeBackend, overlaps, readBridgeBackend } from "./bridge-backend.js";
|
|
20
23
|
/**
|
|
21
24
|
* `dsm-package.yaml`: a Standalone Package, i.e. an unsigned native SPK that
|
|
22
25
|
* runs as its own DSM package account with a CGI backend and a DSM window UI.
|
|
@@ -24,15 +27,13 @@ import { sdkProblems } from "./sdk-check.js";
|
|
|
24
27
|
export const STANDALONE_MANIFEST = "dsm-package.yaml";
|
|
25
28
|
export const KNOWN_FIELDS = new Set([
|
|
26
29
|
"schema", "package", "version", "build", "displayname", "description",
|
|
27
|
-
"metadata", "maintainer", "os_min_ver", "arches", "backend", "ui", "capabilities", "i18n", "daemon", "install", "uninstall", "entry", "portal", "icon", "reproducible",
|
|
30
|
+
"metadata", "maintainer", "os_min_ver", "arches", "backend", "ui", "capabilities", "i18n", "daemon", "install", "uninstall", "entry", "portal", "icon", "reproducible", "root",
|
|
28
31
|
]);
|
|
29
32
|
const I18N_FIELDS = new Set(["messages", "glossary", "info"]);
|
|
30
33
|
const PACKAGE_ID = /^[A-Za-z][A-Za-z0-9]{1,63}$/;
|
|
31
34
|
const VERSION = /^\d+(\.\d+){0,3}$/;
|
|
32
35
|
const OS_MIN_VER = /^\d+\.\d+(\.\d+)?-\d+$/;
|
|
33
36
|
const GO_MAIN = /^\.\/[A-Za-z0-9_./-]*$/;
|
|
34
|
-
/** A Cargo package or binary name. */
|
|
35
|
-
const CARGO_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
36
37
|
function isRecord(value) {
|
|
37
38
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
38
39
|
}
|
|
@@ -49,18 +50,18 @@ class Collector {
|
|
|
49
50
|
/** A project-relative path that stays inside the project. */
|
|
50
51
|
path(value, field) {
|
|
51
52
|
if (typeof value !== "string" || value.trim() === "") {
|
|
52
|
-
return this.fail("STANDALONE_PATH_INVALID", `${field}
|
|
53
|
+
return this.fail("STANDALONE_PATH_INVALID", `${field} must be a relative path inside the project.`);
|
|
53
54
|
}
|
|
54
55
|
const resolved = resolve(this.root, value);
|
|
55
56
|
const rel = relative(this.root, resolved);
|
|
56
57
|
if (isAbsolute(value) || rel.startsWith("..") || isAbsolute(rel)) {
|
|
57
|
-
return this.fail("STANDALONE_PATH_OUTSIDE_PROJECT", `${field}
|
|
58
|
+
return this.fail("STANDALONE_PATH_OUTSIDE_PROJECT", `${field} "${value}" must be inside the project directory.`);
|
|
58
59
|
}
|
|
59
60
|
return resolved;
|
|
60
61
|
}
|
|
61
62
|
text(value, field) {
|
|
62
63
|
if (typeof value !== "string" || value.trim() === "" || /[\r\n"\\]/.test(value)) {
|
|
63
|
-
return this.fail("STANDALONE_TEXT_INVALID", `${field}
|
|
64
|
+
return this.fail("STANDALONE_TEXT_INVALID", `${field} must be single-line, non-empty text without " or \\.`);
|
|
64
65
|
}
|
|
65
66
|
return value;
|
|
66
67
|
}
|
|
@@ -68,7 +69,7 @@ class Collector {
|
|
|
68
69
|
export function loadStandaloneManifest(root) {
|
|
69
70
|
const file = join(root, STANDALONE_MANIFEST);
|
|
70
71
|
if (!existsSync(file)) {
|
|
71
|
-
return { ok: false, errors: [{ code: "STANDALONE_MANIFEST_MISSING", message:
|
|
72
|
+
return { ok: false, errors: [{ code: "STANDALONE_MANIFEST_MISSING", message: `${STANDALONE_MANIFEST} was not found.` }] };
|
|
72
73
|
}
|
|
73
74
|
let raw;
|
|
74
75
|
try {
|
|
@@ -79,9 +80,9 @@ export function loadStandaloneManifest(root) {
|
|
|
79
80
|
}
|
|
80
81
|
const c = new Collector(root);
|
|
81
82
|
if (existsSync(join(root, "compose.yaml")))
|
|
82
|
-
c.fail("PROJECT_RUNTIME_EXCLUSIVE", "compose.yaml
|
|
83
|
+
c.fail("PROJECT_RUNTIME_EXCLUSIVE", "compose.yaml and dsm-package.yaml are mutually exclusive; keep only one.");
|
|
83
84
|
if (!isRecord(raw)) {
|
|
84
|
-
c.fail("STANDALONE_MANIFEST_YAML", `${STANDALONE_MANIFEST}
|
|
85
|
+
c.fail("STANDALONE_MANIFEST_YAML", `${STANDALONE_MANIFEST} must be a YAML object.`);
|
|
85
86
|
return { ok: false, errors: c.errors };
|
|
86
87
|
}
|
|
87
88
|
const manifest = readManifest(raw, c);
|
|
@@ -90,7 +91,7 @@ export function loadStandaloneManifest(root) {
|
|
|
90
91
|
function readManifest(raw, c) {
|
|
91
92
|
for (const key of Object.keys(raw)) {
|
|
92
93
|
if (!KNOWN_FIELDS.has(key)) {
|
|
93
|
-
c.fail("STANDALONE_FIELD_UNKNOWN",
|
|
94
|
+
c.fail("STANDALONE_FIELD_UNKNOWN", `Unsupported field "${key}". A Standalone Package does not accept custom lifecycle scripts or DSM conf.`);
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
if (raw.reproducible !== undefined) {
|
|
@@ -102,22 +103,23 @@ function readManifest(raw, c) {
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
if (raw.schema !== 2) {
|
|
105
|
-
c.fail("STANDALONE_SCHEMA_INVALID", "schema: 2
|
|
106
|
+
c.fail("STANDALONE_SCHEMA_INVALID", "schema: 2 is required; use schema: 2 with backend.kind: go|binary, and change privilege: root, background and fileStation to capabilities.rootOps: true, capabilities.jobs: true and capabilities.files: session respectively.");
|
|
106
107
|
}
|
|
107
108
|
const capabilities = readCapabilities(raw.capabilities, c);
|
|
108
109
|
const rootOps = readRootOps(capabilities.rootOps === true, c);
|
|
110
|
+
const rootPrograms = raw.root === undefined ? undefined : readRootPrograms(raw.root, capabilities.rootOps === true, rootOps, c);
|
|
109
111
|
const packageId = typeof raw.package === "string" && PACKAGE_ID.test(raw.package)
|
|
110
112
|
? raw.package
|
|
111
|
-
: c.fail("STANDALONE_PACKAGE_INVALID", "package
|
|
113
|
+
: c.fail("STANDALONE_PACKAGE_INVALID", "package must start with a letter and contain only alphanumerics (2-64 characters).");
|
|
112
114
|
const version = typeof raw.version === "string" && VERSION.test(raw.version)
|
|
113
115
|
? raw.version
|
|
114
|
-
: c.fail("STANDALONE_VERSION_INVALID", "version
|
|
116
|
+
: c.fail("STANDALONE_VERSION_INVALID", "version must be a numeric version such as 1.2.0 (build goes in build).");
|
|
115
117
|
const build = Number.isInteger(raw.build) && raw.build >= 1 && raw.build <= 9999
|
|
116
118
|
? raw.build
|
|
117
|
-
: c.fail("STANDALONE_BUILD_INVALID", "build
|
|
119
|
+
: c.fail("STANDALONE_BUILD_INVALID", "build must be an integer from 1 to 9999.");
|
|
118
120
|
const osMinVer = typeof raw.os_min_ver === "string" && OS_MIN_VER.test(raw.os_min_ver)
|
|
119
121
|
? raw.os_min_ver
|
|
120
|
-
: c.fail("STANDALONE_OS_MIN_VER_INVALID", "os_min_ver
|
|
122
|
+
: c.fail("STANDALONE_OS_MIN_VER_INVALID", "os_min_ver format is <major>.<minor>-<build>, for example 7.2-64570.");
|
|
121
123
|
if (capabilities.rootOps === true && osMinVer && !isDsmAtLeast(osMinVer, DEFAULT_MIN_DSM)) {
|
|
122
124
|
c.fail("STANDALONE_OS_MIN_VER_INVALID", `root templates require DSM ${DEFAULT_MIN_DSM} or later.`);
|
|
123
125
|
}
|
|
@@ -130,9 +132,12 @@ function readManifest(raw, c) {
|
|
|
130
132
|
metadata = validateMetadata(raw.metadata);
|
|
131
133
|
}
|
|
132
134
|
catch {
|
|
133
|
-
c.fail("STANDALONE_TEXT_INVALID", "metadata
|
|
135
|
+
c.fail("STANDALONE_TEXT_INVALID", "The metadata fields are malformed; check the text, email, URLs and screenshots.");
|
|
134
136
|
}
|
|
135
137
|
const arches = readArches(raw.arches, c);
|
|
138
|
+
const binaryProblem = rootOps && arches && (raw.root === undefined || rootPrograms) ? rootBinaryProblem(rootOps, rootPrograms, arches) : undefined;
|
|
139
|
+
if (binaryProblem)
|
|
140
|
+
c.fail("STANDALONE_ROOT_PROGRAMS_INVALID", binaryProblem);
|
|
136
141
|
const backend = arches && readBackend(raw.backend, arches, c);
|
|
137
142
|
let permissions;
|
|
138
143
|
if (backend && backend.kind !== "binary") {
|
|
@@ -152,37 +157,51 @@ function readManifest(raw, c) {
|
|
|
152
157
|
}
|
|
153
158
|
const daemon = arches && readDaemon(raw.daemon, c.root, arches, e => c.fail(e.code, e.message));
|
|
154
159
|
if (capabilities.jobs && raw.daemon !== undefined && capabilities.delegate?.lifetime !== 'until-revoked') {
|
|
155
|
-
c.fail("CAPABILITY_COMBINATION_INVALID", "daemon
|
|
160
|
+
c.fail("CAPABILITY_COMBINATION_INVALID", "daemon can only coexist with an explicit until-revoked delegate fixed worker; plain capabilities.jobs stays mutually exclusive.");
|
|
156
161
|
}
|
|
157
162
|
if (backend?.kind === "binary") {
|
|
158
163
|
for (const name of ["files", "jobs", "identity", "delegate"]) {
|
|
159
164
|
if (isRecord(raw.capabilities) && raw.capabilities[name] !== undefined)
|
|
160
|
-
c.fail("CAPABILITY_UNSUPPORTED", `backend.kind: binary
|
|
165
|
+
c.fail("CAPABILITY_UNSUPPORTED", `backend.kind: binary does not support capabilities.${name}.`);
|
|
161
166
|
}
|
|
162
167
|
}
|
|
168
|
+
if (backend && isBridgeBackend(backend)) {
|
|
169
|
+
// The runtime offers delegate only as the until-revoked files.moveFromStaging integration (spec/bridge/v1).
|
|
170
|
+
const delegate = capabilities.delegate;
|
|
171
|
+
if (delegate && !(delegate.lifetime === "until-revoked" && delegate.operations.join() === "files.moveFromStaging" && delegate.destinations?.length)) {
|
|
172
|
+
c.fail("CAPABILITY_UNSUPPORTED", `For backend.runtime: ${backend.kind}, capabilities.delegate supports only operations: [files.moveFromStaging], destinations and lifetime: until-revoked.`);
|
|
173
|
+
}
|
|
174
|
+
if (raw.daemon !== undefined)
|
|
175
|
+
c.fail("CAPABILITY_UNSUPPORTED", `backend.runtime: ${backend.kind} does not support daemon; a v1 App runs once per request (exec mode).`);
|
|
176
|
+
if (permissions)
|
|
177
|
+
checkBridgeMethods(backend, permissions, c);
|
|
178
|
+
}
|
|
163
179
|
const sharedFolder = raw.install === undefined ? undefined : readSharedFolder(raw.install, e => c.fail(e.code, e.message));
|
|
164
180
|
const entry = raw.entry === undefined ? undefined : readEntry(raw.entry, backend, permissions?.policy, c);
|
|
165
181
|
if (entry === "admin-only" && permissions) {
|
|
166
182
|
const floored = floorAtAdministrators(permissions.policy, permissions.sdk);
|
|
167
183
|
if (floored.widened.length) {
|
|
168
|
-
c.fail("STANDALONE_ENTRY_WIDENED", `entry: admin-only
|
|
184
|
+
c.fail("STANDALONE_ENTRY_WIDENED", `entry: admin-only uses administrators as the floor for every route; ${floored.widened.join(", ")} in permissions.json would open it to others. Change them to administrators, or use entry: app-privilege / omit entry.`);
|
|
169
185
|
}
|
|
170
186
|
permissions.policy = floored.policy;
|
|
171
187
|
}
|
|
172
188
|
const ui = readUi(raw.ui, c);
|
|
189
|
+
if (backend && isBridgeBackend(backend) && ui && overlaps(backend.dir, ui.dir)) {
|
|
190
|
+
c.fail("STANDALONE_BRIDGE_APP_IN_UI", "backend.dir and ui.dir must not overlap: DSM serves the files under ui/ directly, so the App directory (target/app) cannot be inside it.");
|
|
191
|
+
}
|
|
173
192
|
if (capabilities.identity && ui && !ui.build)
|
|
174
|
-
c.fail("CAPABILITY_COMBINATION_INVALID", "capabilities.identity
|
|
193
|
+
c.fail("CAPABILITY_COMBINATION_INVALID", "capabilities.identity needs a standalone sign-in page; plain UI does not support it, use a vue UI (init --ui vue).");
|
|
175
194
|
if (capabilities.delegate?.scope === "install-selected" && ui && !ui.build)
|
|
176
|
-
c.fail("CAPABILITY_COMBINATION_INVALID", "delegate
|
|
195
|
+
c.fail("CAPABILITY_COMBINATION_INVALID", "The delegate install-selected scope can only be confirmed in a Vue panel; plain UI does not support it, use a vue UI (init --ui vue).");
|
|
177
196
|
if (backend && backend.kind !== "binary") {
|
|
178
|
-
for (const problem of sdkProblems(backend, ui?.build?.cwd, Boolean(capabilities.delegate || capabilities.rootOps)))
|
|
197
|
+
for (const problem of sdkProblems(backend, ui?.build?.cwd, isBridgeBackend(backend) || Boolean(capabilities.delegate || capabilities.rootOps)))
|
|
179
198
|
c.fail(problem.code, problem.message);
|
|
180
199
|
}
|
|
181
200
|
const i18n = raw.i18n === undefined ? undefined : readI18n(raw.i18n, c);
|
|
182
201
|
const icon = raw.icon === undefined ? undefined : readIcon(raw.icon, c);
|
|
183
202
|
const uninstall = readUninstall(raw.uninstall, { rootOps: capabilities.rootOps === true, ops: rootOps, en: i18n?.catalogs?.en }, e => c.fail(e.code, e.message));
|
|
184
203
|
const portal = raw.portal === undefined || !packageId ? undefined : readPortalDeclaration(raw.portal, packageId)
|
|
185
|
-
?? c.fail("STANDALONE_PORTAL_INVALID", "portal
|
|
204
|
+
?? c.fail("STANDALONE_PORTAL_INVALID", "portal must be {} or { alias: <1-64 lowercase alphanumerics or -, not a DSM reserved path> }.");
|
|
186
205
|
if (portal && !capabilities.rootOps && !capabilities.delegate) {
|
|
187
206
|
c.fail("STANDALONE_PORTAL_REQUIRES_EXECUTOR", PORTAL_REQUIRES_EXECUTOR);
|
|
188
207
|
}
|
|
@@ -215,6 +234,7 @@ function readManifest(raw, c) {
|
|
|
215
234
|
...(i18n ? { i18n } : {}),
|
|
216
235
|
capabilities,
|
|
217
236
|
...(rootOps ? { rootOps } : {}),
|
|
237
|
+
...(rootPrograms ? { rootPrograms } : {}),
|
|
218
238
|
...(daemon ? { daemon } : {}),
|
|
219
239
|
...(uninstall ? { uninstall } : {}),
|
|
220
240
|
...(sharedFolder ? { sharedFolder } : {}),
|
|
@@ -223,7 +243,29 @@ function readManifest(raw, c) {
|
|
|
223
243
|
...(icon ? { icon } : {}),
|
|
224
244
|
};
|
|
225
245
|
}
|
|
226
|
-
|
|
246
|
+
function checkBridgeMethods(backend, permissions, c) {
|
|
247
|
+
for (const problem of bridgeMethodProblems(backend.methods, permissions, `${relative(c.root, backend.dir)}/permissions.json`))
|
|
248
|
+
c.fail("STANDALONE_BRIDGE_METHODS_MISMATCH", problem);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* The runtime registers routes from the manifest's methods: a policy for
|
|
252
|
+
* another method, or an App method with an SDK name, would refuse to start.
|
|
253
|
+
* Without methods only the SDK names of the policy can be checked.
|
|
254
|
+
*/
|
|
255
|
+
export function bridgeMethodProblems(methods, permissions, file) {
|
|
256
|
+
const sdk = new Set(Object.values(permissions.sdk).flatMap(names => Object.keys(names)));
|
|
257
|
+
const problems = [];
|
|
258
|
+
const taken = (methods ?? []).filter(method => sdk.has(method));
|
|
259
|
+
if (taken.length)
|
|
260
|
+
problems.push(`App methods must not use an SDK method name: ${taken.join(", ")}.`);
|
|
261
|
+
if (!methods)
|
|
262
|
+
return problems;
|
|
263
|
+
const unlisted = Object.keys(permissions.policy.methods).filter(method => !sdk.has(method) && !methods.includes(method));
|
|
264
|
+
if (unlisted.length)
|
|
265
|
+
problems.push(`${file} sets methods the App does not have: ${unlisted.join(", ")}; the runtime refuses to start.`);
|
|
266
|
+
return problems;
|
|
267
|
+
}
|
|
268
|
+
/** Where an App's SDK version is declared: its Go backend; a prebuilt binary declares it in the project's go.mod. */
|
|
227
269
|
export function sdkBackend(m) {
|
|
228
270
|
return m.backend.kind === "binary" ? { kind: "go", dir: m.root } : m.backend;
|
|
229
271
|
}
|
|
@@ -236,13 +278,12 @@ function readIcon(value, c) {
|
|
|
236
278
|
return path;
|
|
237
279
|
}
|
|
238
280
|
catch (error) {
|
|
239
|
-
return c.fail("STANDALONE_ICON_INVALID", `icon
|
|
281
|
+
return c.fail("STANDALONE_ICON_INVALID", `icon: ${error.message}`);
|
|
240
282
|
}
|
|
241
283
|
}
|
|
242
284
|
/** Where each backend kind's CGI main wraps its authenticator in the SDK entry gate. */
|
|
243
285
|
const ENTRY_GATE = {
|
|
244
|
-
go: { main: b => b.kind === "go" ? join(b.dir, b.main, "main.go") : "", call: "dsm.NewEntryGate(", sdk: `backend/go.mod
|
|
245
|
-
rust: { main: b => b.kind === "rust" ? join(b.dir, "src/bin", `${b.bin}.rs`) : "", call: "new_entry_gate(", sdk: `backend/Cargo.toml 要以 release tag 相依 ${SDK_CRATE}` },
|
|
286
|
+
go: { main: b => b.kind === "go" ? join(b.dir, b.main, "main.go") : "", call: "dsm.NewEntryGate(", sdk: `backend/go.mod must require ${SDK_MODULE}` },
|
|
246
287
|
};
|
|
247
288
|
/** The SDK version, or none when the backend has none or pins it wrongly (reported by sdkProblems). */
|
|
248
289
|
function sdkVersionOrUndefined(backend) {
|
|
@@ -260,27 +301,35 @@ function sdkVersionOrUndefined(backend) {
|
|
|
260
301
|
*/
|
|
261
302
|
function readEntry(value, backend, policy, c) {
|
|
262
303
|
if (value !== "admin-only" && value !== "app-privilege") {
|
|
263
|
-
return c.fail("STANDALONE_ENTRY_INVALID", "entry
|
|
304
|
+
return c.fail("STANDALONE_ENTRY_INVALID", "entry must be admin-only or app-privilege; omit it to keep the existing permissions.json behavior.");
|
|
264
305
|
}
|
|
265
306
|
if (!backend)
|
|
266
307
|
return undefined;
|
|
267
|
-
|
|
308
|
+
// A bridge App's entry gate is the runtime's (bridge.json access).
|
|
309
|
+
const gate = isBridgeBackend(backend) ? undefined : ENTRY_GATE[backend.kind];
|
|
268
310
|
const main = gate?.main(backend) ?? "";
|
|
269
|
-
if (!gate || !sdkVersionOrUndefined(backend)
|
|
270
|
-
|| !existsSync(main) || !readFileSync(main, "utf8").includes(gate.call)) {
|
|
271
|
-
const shown = main ? relative(c.root, main) : "backend.main
|
|
272
|
-
return c.fail("STANDALONE_ENTRY_UNSUPPORTED", `entry
|
|
311
|
+
if (!isBridgeBackend(backend) && (!gate || !sdkVersionOrUndefined(backend))
|
|
312
|
+
|| gate && (!existsSync(main) || !readFileSync(main, "utf8").includes(gate.call))) {
|
|
313
|
+
const shown = main ? relative(c.root, main) : "the backend.main main.go";
|
|
314
|
+
return c.fail("STANDALONE_ENTRY_UNSUPPORTED", `entry needs the SDK entry gate: ${gate?.sdk ?? `backend/go.mod must require ${SDK_MODULE}`}, and wrap the authenticator with ${gate?.call.slice(0, -1) ?? "dsm.NewEntryGate"} in ${shown}.`);
|
|
273
315
|
}
|
|
274
316
|
if (value === "app-privilege" && !permitsOrdinaryUsers(policy)) {
|
|
275
|
-
return c.fail("STANDALONE_ENTRY_INVALID", "entry: app-privilege
|
|
317
|
+
return c.fail("STANDALONE_ENTRY_INVALID", "entry: app-privilege needs at least one route marked authenticated in permissions.json, otherwise regular users have nothing to use.");
|
|
276
318
|
}
|
|
277
319
|
return value;
|
|
278
320
|
}
|
|
321
|
+
/** Root settings are read against the loaded root/ programs; when those failed, their errors stand alone. */
|
|
322
|
+
function readRootPrograms(value, enabled, ops, c) {
|
|
323
|
+
if (enabled && !ops)
|
|
324
|
+
return undefined;
|
|
325
|
+
const read = readRootSettings(value, ops);
|
|
326
|
+
return "settings" in read ? read.settings : c.fail("STANDALONE_ROOT_PROGRAMS_INVALID", read.error);
|
|
327
|
+
}
|
|
279
328
|
function readRootOps(enabled, c) {
|
|
280
329
|
const dir = join(c.root, ROOT_OPS_DIR);
|
|
281
330
|
if (!enabled) {
|
|
282
331
|
return existsSync(dir)
|
|
283
|
-
? c.fail("STANDALONE_ROOT_OPS_UNUSED", `${ROOT_OPS_DIR}/
|
|
332
|
+
? c.fail("STANDALONE_ROOT_OPS_UNUSED", `${ROOT_OPS_DIR}/ needs capabilities.rootOps: true; declare the capability or delete this directory.`)
|
|
284
333
|
: undefined;
|
|
285
334
|
}
|
|
286
335
|
const loaded = loadRootOps(dir);
|
|
@@ -297,77 +346,106 @@ function readArches(value, c) {
|
|
|
297
346
|
&& new Set(value).size === value.length;
|
|
298
347
|
return ok
|
|
299
348
|
? value
|
|
300
|
-
: c.fail("STANDALONE_ARCHES_INVALID", "arches
|
|
349
|
+
: c.fail("STANDALONE_ARCHES_INVALID", "arches must be a list of unique x86_64/aarch64/armv7 values, at least one.");
|
|
301
350
|
}
|
|
302
351
|
function readBackend(value, arches, c) {
|
|
303
|
-
if (
|
|
304
|
-
|
|
352
|
+
if (isRecord(value) && value.runtime !== undefined) {
|
|
353
|
+
if (value.kind !== undefined)
|
|
354
|
+
return c.fail("STANDALONE_BACKEND_INVALID", "Use either backend.kind or backend.runtime; runtime: go is equivalent to kind: go.");
|
|
355
|
+
if (value.runtime === "go") {
|
|
356
|
+
const { runtime: _, ...go } = value;
|
|
357
|
+
value = { ...go, kind: "go" };
|
|
358
|
+
}
|
|
359
|
+
else if (BRIDGE_RUNTIMES.includes(value.runtime)) {
|
|
360
|
+
return readBridgeBackend(value, dir => c.path(dir, "backend.dir"), (code, message) => c.fail(code, message));
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
return c.fail("STANDALONE_BACKEND_INVALID", "backend.runtime must be go, python, node or rust.");
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (!isRecord(value) || !["go", "binary"].includes(String(value.kind))) {
|
|
367
|
+
return c.fail("STANDALONE_BACKEND_INVALID", "backend.kind must be go or binary (or name go, python, node, rust with backend.runtime).");
|
|
305
368
|
}
|
|
306
|
-
if (value.kind === "rust")
|
|
307
|
-
return readRustBackend(value, c);
|
|
308
369
|
if (value.kind === "go") {
|
|
309
|
-
if (Object.keys(value).some(key => !["kind", "dir", "main"].includes(key)) || (value.main !== undefined && (typeof value.main !== "string" || !GO_MAIN.test(value.main) || value.main.includes("..")))) {
|
|
310
|
-
return c.fail("STANDALONE_BACKEND_INVALID", "backend.kind: go
|
|
370
|
+
if (Object.keys(value).some(key => !["kind", "dir", "main", "timeout_seconds"].includes(key)) || (value.main !== undefined && (typeof value.main !== "string" || !GO_MAIN.test(value.main) || value.main.includes("..")))) {
|
|
371
|
+
return c.fail("STANDALONE_BACKEND_INVALID", "backend.kind: go needs dir, with optional main (./cmd/app by default) and timeout_seconds.");
|
|
311
372
|
}
|
|
373
|
+
const timeout = value.timeout_seconds;
|
|
374
|
+
if (timeout !== undefined && !validTimeoutSeconds(timeout))
|
|
375
|
+
return c.fail("STANDALONE_BACKEND_INVALID", `backend.${TIMEOUT_USAGE}`);
|
|
312
376
|
const dir = c.path(value.dir, "backend.dir");
|
|
313
|
-
|
|
377
|
+
if (!dir)
|
|
378
|
+
return undefined;
|
|
379
|
+
const backend = { kind: "go", dir, main: value.main ?? "./cmd/app" };
|
|
380
|
+
if (timeout === undefined)
|
|
381
|
+
return backend;
|
|
382
|
+
return goReadsTimeout(backend, c) ? { ...backend, timeoutSeconds: timeout } : undefined;
|
|
383
|
+
}
|
|
384
|
+
if (value.timeout_seconds !== undefined) {
|
|
385
|
+
return c.fail("STANDALONE_BACKEND_INVALID", "backend.kind: binary does not take timeout_seconds: a prebuilt CGI sets its own request deadline.");
|
|
314
386
|
}
|
|
315
387
|
const binary = value.files;
|
|
316
388
|
if (!isRecord(binary) || Object.keys(value).some(key => !["kind", "files"].includes(key))) {
|
|
317
|
-
return c.fail("STANDALONE_BACKEND_INVALID", "backend.kind: binary
|
|
389
|
+
return c.fail("STANDALONE_BACKEND_INVALID", "backend.kind: binary needs files keyed by architecture pointing at CGI executables.");
|
|
318
390
|
}
|
|
319
391
|
const files = {};
|
|
320
392
|
for (const arch of arches) {
|
|
321
393
|
if (!(arch in binary)) {
|
|
322
|
-
c.fail("STANDALONE_BINARY_ARCH_MISSING", `arches
|
|
394
|
+
c.fail("STANDALONE_BINARY_ARCH_MISSING", `arches declares ${arch}, but backend.files has no executable for it.`);
|
|
323
395
|
continue;
|
|
324
396
|
}
|
|
325
397
|
files[arch] = c.path(binary[arch], `backend.files.${arch}`);
|
|
326
398
|
}
|
|
327
399
|
for (const key of Object.keys(binary)) {
|
|
328
400
|
if (!arches.includes(key)) {
|
|
329
|
-
c.fail("STANDALONE_BACKEND_INVALID", `backend.files.${key}
|
|
401
|
+
c.fail("STANDALONE_BACKEND_INVALID", `backend.files.${key} is not in arches.`);
|
|
330
402
|
}
|
|
331
403
|
}
|
|
332
404
|
return Object.values(files).every(Boolean) ? { kind: "binary", files } : undefined;
|
|
333
405
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
406
|
+
/**
|
|
407
|
+
* The build passes the deadline as -X main.requestTimeout; a main without
|
|
408
|
+
* that variable would silently drop it and keep the SDK's default.
|
|
409
|
+
*/
|
|
410
|
+
function goReadsTimeout(backend, c) {
|
|
411
|
+
const main = join(backend.dir, backend.main, "main.go");
|
|
412
|
+
const source = existsSync(main) ? readFileSync(main, "utf8") : "";
|
|
413
|
+
if (/\brequestTimeout\b/.test(source) && source.includes("api.Timeout("))
|
|
414
|
+
return true;
|
|
415
|
+
c.fail("STANDALONE_TIMEOUT_UNSUPPORTED", `backend.timeout_seconds needs ${relative(c.root, main)} to declare var requestTimeout = "" (build passes the seconds with -X main.requestTimeout) and apply it with api.WithRequestDeadline(handler, api.Timeout(...)).`);
|
|
416
|
+
return false;
|
|
343
417
|
}
|
|
344
418
|
function readUi(value, c) {
|
|
345
419
|
if (!isRecord(value)) {
|
|
346
|
-
return c.fail("STANDALONE_UI_INVALID", "ui
|
|
420
|
+
return c.fail("STANDALONE_UI_INVALID", "ui must contain dir.");
|
|
347
421
|
}
|
|
348
422
|
const dir = c.path(value.dir, "ui.dir");
|
|
423
|
+
const read = value.window === undefined ? undefined : readWindowSize(value.window);
|
|
424
|
+
if (read && "error" in read)
|
|
425
|
+
return c.fail("STANDALONE_UI_WINDOW_INVALID", read.error);
|
|
426
|
+
const window = read ? { window: read.size } : {};
|
|
349
427
|
if (value.build === undefined) {
|
|
350
|
-
return dir ? { dir } : undefined;
|
|
428
|
+
return dir ? { dir, ...window } : undefined;
|
|
351
429
|
}
|
|
352
430
|
const build = value.build;
|
|
353
431
|
const command = isRecord(build) ? build.command : undefined;
|
|
354
432
|
if (!Array.isArray(command) || command.length === 0 || !command.every((arg) => typeof arg === "string" && arg !== "")) {
|
|
355
|
-
return c.fail("STANDALONE_UI_BUILD_INVALID", "ui.build.command
|
|
433
|
+
return c.fail("STANDALONE_UI_BUILD_INVALID", "ui.build.command must be an argv string array (not going through a shell), for example [npm, run, build].");
|
|
356
434
|
}
|
|
357
435
|
const cwd = c.path(build.cwd, "ui.build.cwd");
|
|
358
|
-
return dir && cwd ? { dir, build: { cwd, command: command } } : undefined;
|
|
436
|
+
return dir && cwd ? { dir, build: { cwd, command: command }, ...window } : undefined;
|
|
359
437
|
}
|
|
360
438
|
function readI18n(value, c) {
|
|
361
439
|
const invalid = (why) => c.fail("STANDALONE_I18N_INVALID", `i18n ${why}`);
|
|
362
440
|
if (!isRecord(value) || Object.keys(value).some((key) => !I18N_FIELDS.has(key))) {
|
|
363
|
-
return invalid("
|
|
441
|
+
return invalid("Only messages (the message directory), glossary (the terminology table) and info (per-language INFO text) are accepted.");
|
|
364
442
|
}
|
|
365
443
|
const info = readInfoTranslations(value.info, {
|
|
366
444
|
invalid: (why) => c.fail("STANDALONE_I18N_INVALID", `i18n.info ${why}`),
|
|
367
445
|
text: (text, field) => c.text(text, field),
|
|
368
446
|
});
|
|
369
447
|
if (value.messages === undefined) {
|
|
370
|
-
return value.glossary === undefined ? info && { info } : invalid("glossary
|
|
448
|
+
return value.glossary === undefined ? info && { info } : invalid("glossary needs messages.");
|
|
371
449
|
}
|
|
372
450
|
const dir = c.path(value.messages, "i18n.messages");
|
|
373
451
|
const glossary = value.glossary === undefined ? undefined : c.path(value.glossary, "i18n.glossary");
|
|
@@ -407,7 +485,7 @@ function readCapabilities(value, c) {
|
|
|
407
485
|
if (value === undefined)
|
|
408
486
|
return {};
|
|
409
487
|
if (!isRecord(value)) {
|
|
410
|
-
c.fail("CAPABILITY_INVALID", "capabilities
|
|
488
|
+
c.fail("CAPABILITY_INVALID", "capabilities must be an object.");
|
|
411
489
|
return {};
|
|
412
490
|
}
|
|
413
491
|
const result = {};
|
|
@@ -428,7 +506,7 @@ function readCapabilities(value, c) {
|
|
|
428
506
|
result.jobs = readBackground(setting === true ? { worker: "sha256-v1" } : setting, e => c.fail("CAPABILITY_INVALID", e.message.replace("background", "capabilities.jobs")));
|
|
429
507
|
}
|
|
430
508
|
else
|
|
431
|
-
c.fail("CAPABILITY_INVALID",
|
|
509
|
+
c.fail("CAPABILITY_INVALID", `Unsupported capabilities.${name} value; use files: session, identity: standalone, jobs: true or {worker: sha256-v1}, rootOps: true.`);
|
|
432
510
|
}
|
|
433
511
|
if (result.delegate && !result.jobs)
|
|
434
512
|
result.jobs = { worker: "sha256-v1" };
|
|
@@ -14,11 +14,11 @@ export const PORTAL_MESSAGES = {
|
|
|
14
14
|
cleanupRequired: { en: "The Application Portal entry was not fully removed. Remove the entry again.", "zh-TW": "應用程式入口未完全移除。請再移除一次入口。", "zh-CN": "应用程序门户入口未完全移除。请再移除一次入口。", ja: "アプリケーションポータルのエントリが完全に削除されていません。もう一度エントリを削除してください。", ko: "응용 프로그램 포털 항목이 완전히 제거되지 않았습니다. 항목을 다시 제거하십시오.", de: "Der Eintrag im Anwendungsportal wurde nicht vollständig entfernt. Entfernen Sie den Eintrag erneut.", fr: "L'entrée du Portail d'application n'a pas été entièrement supprimée. Supprimez de nouveau l'entrée.", es: "La entrada del Portal de aplicaciones no se eliminó por completo. Elimine la entrada de nuevo.", it: "La voce del Portale applicazioni non è stata rimossa completamente. Rimuovere di nuovo la voce." },
|
|
15
15
|
register: { en: "Enable entry", "zh-TW": "啟用入口", "zh-CN": "启用入口", ja: "エントリを有効にする", ko: "항목 활성화", de: "Eintrag aktivieren", fr: "Activer l'entrée", es: "Activar entrada", it: "Attiva voce" },
|
|
16
16
|
unregister: { en: "Remove entry", "zh-TW": "移除入口", "zh-CN": "移除入口", ja: "エントリを削除", ko: "항목 제거", de: "Eintrag entfernen", fr: "Supprimer l'entrée", es: "Eliminar entrada", it: "Rimuovi voce" },
|
|
17
|
-
portal_alias_invalid: { en: "Use 1-64 lowercase letters, digits or hyphens, not starting or ending with a hyphen.", "zh-TW": "請使用 1–64 個小寫英文字母、數字或連字號,且不可以連字號開頭或結尾。", "zh-CN": "请使用 1–64 个小写英文字母、数字或连字符,且不能以连字符开头或结尾。", ja: "1~64 文字の小文字英字・数字・ハイフンを使用し、先頭と末尾にハイフンは使えません。", ko: "1~64자의 영문 소문자, 숫자 또는 하이픈을 사용하고 하이픈으로 시작하거나 끝나지 않게 하십시오.", de: "Verwenden Sie 1–64 Kleinbuchstaben, Ziffern oder Bindestriche, ohne Bindestrich am Anfang oder Ende.", fr: "Utilisez 1 à 64 lettres minuscules, chiffres ou tirets, sans tiret au début ni à la fin.", es: "Use de 1 a 64 letras minúsculas, dígitos o guiones, sin guion al principio ni al final.", it: "
|
|
18
|
-
portal_alias_reserved: { en: "DSM already uses this alias. Choose another one.", "zh-TW": "DSM 已使用這個別名,請改用其他別名。", "zh-CN": "DSM 已使用此别名,请改用其他别名。", ja: "このエイリアスは DSM で使用されています。別のものを選んでください。", ko: "DSM에서 이미 사용하는 별칭입니다. 다른 별칭을 선택하십시오.", de: "DSM verwendet diesen Alias bereits. Wählen Sie einen anderen.", fr: "DSM utilise déjà cet alias. Choisissez-en un autre.", es: "DSM ya usa este alias. Elija otro.", it: "DSM usa già questo alias.
|
|
19
|
-
portal_alias_in_use: { en: "Another app already uses this alias. Choose another one.", "zh-TW": "另一個 App 已使用這個別名,請改用其他別名。", "zh-CN": "另一个应用已使用此别名,请改用其他别名。", ja: "別のアプリがこのエイリアスを使用しています。別のものを選んでください。", ko: "다른 앱이 이미 이 별칭을 사용합니다. 다른 별칭을 선택하십시오.", de: "Eine andere App verwendet diesen Alias bereits. Wählen Sie einen anderen.", fr: "Une autre application utilise déjà cet alias. Choisissez-en un autre.", es: "Otra aplicación ya usa este alias. Elija otro.", it: "Un'altra app usa già questo alias.
|
|
17
|
+
portal_alias_invalid: { en: "Use 1-64 lowercase letters, digits or hyphens, not starting or ending with a hyphen.", "zh-TW": "請使用 1–64 個小寫英文字母、數字或連字號,且不可以連字號開頭或結尾。", "zh-CN": "请使用 1–64 个小写英文字母、数字或连字符,且不能以连字符开头或结尾。", ja: "1~64 文字の小文字英字・数字・ハイフンを使用し、先頭と末尾にハイフンは使えません。", ko: "1~64자의 영문 소문자, 숫자 또는 하이픈을 사용하고 하이픈으로 시작하거나 끝나지 않게 하십시오.", de: "Verwenden Sie 1–64 Kleinbuchstaben, Ziffern oder Bindestriche, ohne Bindestrich am Anfang oder Ende.", fr: "Utilisez 1 à 64 lettres minuscules, chiffres ou tirets, sans tiret au début ni à la fin.", es: "Use de 1 a 64 letras minúsculas, dígitos o guiones, sin guion al principio ni al final.", it: "Usare da 1 a 64 lettere minuscole, cifre o trattini, senza trattino all'inizio o alla fine." },
|
|
18
|
+
portal_alias_reserved: { en: "DSM already uses this alias. Choose another one.", "zh-TW": "DSM 已使用這個別名,請改用其他別名。", "zh-CN": "DSM 已使用此别名,请改用其他别名。", ja: "このエイリアスは DSM で使用されています。別のものを選んでください。", ko: "DSM에서 이미 사용하는 별칭입니다. 다른 별칭을 선택하십시오.", de: "DSM verwendet diesen Alias bereits. Wählen Sie einen anderen.", fr: "DSM utilise déjà cet alias. Choisissez-en un autre.", es: "DSM ya usa este alias. Elija otro.", it: "DSM usa già questo alias. Sceglierne un altro." },
|
|
19
|
+
portal_alias_in_use: { en: "Another app already uses this alias. Choose another one.", "zh-TW": "另一個 App 已使用這個別名,請改用其他別名。", "zh-CN": "另一个应用已使用此别名,请改用其他别名。", ja: "別のアプリがこのエイリアスを使用しています。別のものを選んでください。", ko: "다른 앱이 이미 이 별칭을 사용합니다. 다른 별칭을 선택하십시오.", de: "Eine andere App verwendet diesen Alias bereits. Wählen Sie einen anderen.", fr: "Une autre application utilise déjà cet alias. Choisissez-en un autre.", es: "Otra aplicación ya usa este alias. Elija otro.", it: "Un'altra app usa già questo alias. Sceglierne un altro." },
|
|
20
20
|
admin_required: { en: "Only a DSM administrator can change this.", "zh-TW": "只有 DSM 管理員可以變更這項設定。", "zh-CN": "只有 DSM 管理员可以更改此设置。", ja: "これを変更できるのは DSM 管理者のみです。", ko: "DSM 관리자만 변경할 수 있습니다.", de: "Nur ein DSM-Administrator kann dies ändern.", fr: "Seul un administrateur DSM peut modifier ce paramètre.", es: "Solo un administrador de DSM puede cambiar esto.", it: "Solo un amministratore DSM può modificarlo." },
|
|
21
|
-
portal_unavailable: { en: "DSM did not accept the change. Try again or use Control Panel.", "zh-TW": "DSM 未接受變更。請再試一次或改用控制台。", "zh-CN": "DSM 未接受更改。请重试或改用控制面板。", ja: "DSM は変更を受け付けませんでした。再試行するか、コントロールパネルを使用してください。", ko: "DSM이 변경을 받아들이지 않았습니다. 다시 시도하거나 제어판을 사용하십시오.", de: "DSM hat die Änderung nicht übernommen. Versuchen Sie es erneut oder verwenden Sie die Systemsteuerung.", fr: "DSM n'a pas accepté la modification. Réessayez ou utilisez le Panneau de configuration.", es: "DSM no aceptó el cambio. Inténtelo de nuevo o use el Panel de control.", it: "DSM non ha accettato la modifica.
|
|
21
|
+
portal_unavailable: { en: "DSM did not accept the change. Try again or use Control Panel.", "zh-TW": "DSM 未接受變更。請再試一次或改用控制台。", "zh-CN": "DSM 未接受更改。请重试或改用控制面板。", ja: "DSM は変更を受け付けませんでした。再試行するか、コントロールパネルを使用してください。", ko: "DSM이 변경을 받아들이지 않았습니다. 다시 시도하거나 제어판을 사용하십시오.", de: "DSM hat die Änderung nicht übernommen. Versuchen Sie es erneut oder verwenden Sie die Systemsteuerung.", fr: "DSM n'a pas accepté la modification. Réessayez ou utilisez le Panneau de configuration.", es: "DSM no aceptó el cambio. Inténtelo de nuevo o use el Panel de control.", it: "DSM non ha accettato la modifica. Riprovare o usare il Pannello di controllo." },
|
|
22
22
|
PORTAL_FAILED: { en: "DSM did not accept the Application Portal change. Try again.", "zh-TW": "DSM 未接受應用程式入口的變更。請再試一次。", "zh-CN": "DSM 未接受应用程序门户的更改。请重试。", ja: "DSM はアプリケーションポータルの変更を受け付けませんでした。再試行してください。", ko: "DSM이 응용 프로그램 포털 변경을 받아들이지 않았습니다. 다시 시도하십시오.", de: "DSM hat die Änderung am Anwendungsportal nicht übernommen. Versuchen Sie es erneut.", fr: "DSM n'a pas accepté la modification du Portail d'application. Réessayez.", es: "DSM no aceptó el cambio en el Portal de aplicaciones. Inténtelo de nuevo.", it: "DSM non ha accettato la modifica al Portale applicazioni. Riprovare." },
|
|
23
23
|
};
|
|
24
24
|
function localizedMessages() {
|
|
@@ -105,13 +105,20 @@ export const PORTAL_RUNTIME = `function createPortalAccess(options) {
|
|
|
105
105
|
return options.isAdmin() ? result("unavailable", undefined, false) : result("unavailable");
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
|
+
// The backend reports cleanup-required while a register or unregister runs,
|
|
109
|
+
// so a status read waits for the change, whatever its result.
|
|
110
|
+
var changing = Promise.resolve();
|
|
108
111
|
function status() {
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
return changing.then(function () {
|
|
113
|
+
if (options.entry) return fromEntry();
|
|
114
|
+
return options.isAdmin() ? fromDsm() : fromBackend();
|
|
115
|
+
});
|
|
111
116
|
}
|
|
112
117
|
function change(method) {
|
|
113
118
|
if (!options.isAdmin()) return Promise.reject({ code: "admin_required" });
|
|
114
|
-
|
|
119
|
+
var call = options.call(method, {});
|
|
120
|
+
changing = call.then(function () {}, function () {});
|
|
121
|
+
return call.then(status, function (error) {
|
|
115
122
|
throw { code: error && error.code === "ADMIN_REQUIRED" ? "admin_required" : "PORTAL_FAILED" };
|
|
116
123
|
});
|
|
117
124
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The backend's request deadline in seconds unless the App sets backend.timeout_seconds. */
|
|
2
|
+
export declare const DEFAULT_REQUEST_TIMEOUT_SECONDS = 60;
|
|
3
|
+
/** spec/bridge/v1 timeout_seconds: the runtime refuses more. */
|
|
4
|
+
export declare const MAX_REQUEST_TIMEOUT_SECONDS = 3600;
|
|
5
|
+
export declare const TIMEOUT_USAGE = "timeout_seconds is the request deadline in seconds, a whole number from 1 to 3600; omitted, 60.";
|
|
6
|
+
/** Whether backend.timeout_seconds is a deadline the runtime accepts. */
|
|
7
|
+
export declare function validTimeoutSeconds(value: unknown): value is number;
|
|
8
|
+
/** The request deadline the CGI applies and the window waits past. */
|
|
9
|
+
export declare function requestTimeoutSeconds(backend: object): number;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The backend's request deadline in seconds unless the App sets backend.timeout_seconds. */
|
|
2
|
+
export const DEFAULT_REQUEST_TIMEOUT_SECONDS = 60;
|
|
3
|
+
/** spec/bridge/v1 timeout_seconds: the runtime refuses more. */
|
|
4
|
+
export const MAX_REQUEST_TIMEOUT_SECONDS = 3600;
|
|
5
|
+
export const TIMEOUT_USAGE = `timeout_seconds is the request deadline in seconds, a whole number from 1 to ${MAX_REQUEST_TIMEOUT_SECONDS}; omitted, ${DEFAULT_REQUEST_TIMEOUT_SECONDS}.`;
|
|
6
|
+
/** Whether backend.timeout_seconds is a deadline the runtime accepts. */
|
|
7
|
+
export function validTimeoutSeconds(value) {
|
|
8
|
+
return Number.isInteger(value) && value >= 1 && value <= MAX_REQUEST_TIMEOUT_SECONDS;
|
|
9
|
+
}
|
|
10
|
+
/** The request deadline the CGI applies and the window waits past. */
|
|
11
|
+
export function requestTimeoutSeconds(backend) {
|
|
12
|
+
return backend.timeoutSeconds ?? DEFAULT_REQUEST_TIMEOUT_SECONDS;
|
|
13
|
+
}
|