@norskvideo/ctl-sdk 0.1.5 → 0.1.7
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/index.d.ts +1 -0
- package/index.js +1 -0
- package/license-staged.d.ts +20 -0
- package/license-staged.js +55 -0
- package/package.json +1 -1
- package/product-service.d.ts +7 -5
- package/product-service.js +12 -12
package/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from "./dev-url.js";
|
|
|
5
5
|
export * from "./docker-runner.js";
|
|
6
6
|
export * from "./http-proxy.js";
|
|
7
7
|
export * from "./license-registration.js";
|
|
8
|
+
export * from "./license-staged.js";
|
|
8
9
|
export * from "./license-stager.js";
|
|
9
10
|
export * from "./license-v2.js";
|
|
10
11
|
export * from "./manifest-fetch.js";
|
package/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./dev-url.js";
|
|
|
7
7
|
export * from "./docker-runner.js";
|
|
8
8
|
export * from "./http-proxy.js";
|
|
9
9
|
export * from "./license-registration.js";
|
|
10
|
+
export * from "./license-staged.js";
|
|
10
11
|
export * from "./license-stager.js";
|
|
11
12
|
export * from "./license-v2.js";
|
|
12
13
|
export * from "./manifest-fetch.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The staged copy corresponding to an operator-supplied licence path, if one
|
|
3
|
+
* exists under `licensesDir`. `exists` is injected so this stays unit-testable.
|
|
4
|
+
*/
|
|
5
|
+
export declare function stagedLicenseCandidate(operatorPath: string, licensesDir: string | undefined, exists?: (path: string) => boolean): string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Explain an unreadable licence in terms of the thing that's actually wrong.
|
|
8
|
+
*
|
|
9
|
+
* A permissions failure and a missing file are different mistakes: the first is
|
|
10
|
+
* "right file, wrong path for this process", the second is a typo. Naming the
|
|
11
|
+
* daemon user on an ENOENT would send the operator hunting a permissions
|
|
12
|
+
* problem that isn't there.
|
|
13
|
+
*/
|
|
14
|
+
export declare function unreadableLicenseMessage(opts: {
|
|
15
|
+
operatorPath: string;
|
|
16
|
+
cause: string;
|
|
17
|
+
daemonUser: string;
|
|
18
|
+
licensesDir?: string;
|
|
19
|
+
stagedCandidate?: string;
|
|
20
|
+
}): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostics for a licence file the daemon cannot read.
|
|
3
|
+
*
|
|
4
|
+
* The licence-driven routes (`preview-license`, `from-license`) read the file
|
|
5
|
+
* daemon-side, and the daemon runs unprivileged as its service user — so an
|
|
6
|
+
* operator who passes the path they used at install time (`~/licence.json`, in
|
|
7
|
+
* *their* home) gets an EACCES that reads like a broken licence rather than a
|
|
8
|
+
* wrong path.
|
|
9
|
+
*
|
|
10
|
+
* The remedy is almost always sitting in `licensesDir`: the installers stage a
|
|
11
|
+
* copy there, deliberately keeping the operator's basename, so the file they
|
|
12
|
+
* meant can be found by name and named back to them. Shared by the hosts (ctl
|
|
13
|
+
* and mgr) so the two daemons explain the same mistake the same way.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
17
|
+
/**
|
|
18
|
+
* The staged copy corresponding to an operator-supplied licence path, if one
|
|
19
|
+
* exists under `licensesDir`. `exists` is injected so this stays unit-testable.
|
|
20
|
+
*/
|
|
21
|
+
export function stagedLicenseCandidate(operatorPath, licensesDir, exists = existsSync) {
|
|
22
|
+
if (!licensesDir)
|
|
23
|
+
return undefined;
|
|
24
|
+
// Already the staged copy — suggesting it back would be circular.
|
|
25
|
+
if (resolve(dirname(operatorPath)) === resolve(licensesDir))
|
|
26
|
+
return undefined;
|
|
27
|
+
const candidate = join(licensesDir, basename(operatorPath));
|
|
28
|
+
return exists(candidate) ? candidate : undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Explain an unreadable licence in terms of the thing that's actually wrong.
|
|
32
|
+
*
|
|
33
|
+
* A permissions failure and a missing file are different mistakes: the first is
|
|
34
|
+
* "right file, wrong path for this process", the second is a typo. Naming the
|
|
35
|
+
* daemon user on an ENOENT would send the operator hunting a permissions
|
|
36
|
+
* problem that isn't there.
|
|
37
|
+
*/
|
|
38
|
+
export function unreadableLicenseMessage(opts) {
|
|
39
|
+
const { operatorPath, cause, daemonUser, licensesDir, stagedCandidate } = opts;
|
|
40
|
+
const missing = /ENOENT/.test(cause);
|
|
41
|
+
const parts = [`cannot read license file '${operatorPath}': ${cause}`];
|
|
42
|
+
if (missing) {
|
|
43
|
+
parts.push("Check the path — nothing exists there.");
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
parts.push(`The daemon runs as '${daemonUser}', which cannot read it.`);
|
|
47
|
+
}
|
|
48
|
+
if (stagedCandidate) {
|
|
49
|
+
parts.push(`A staged licence of the same name exists at '${stagedCandidate}' — pass that instead.`);
|
|
50
|
+
}
|
|
51
|
+
else if (!missing) {
|
|
52
|
+
parts.push(`Licences are staged under ${licensesDir ?? "the daemon's licenses directory"} at install time; pass the staged path, or copy the file there readable by '${daemonUser}'.`);
|
|
53
|
+
}
|
|
54
|
+
return parts.join(" ");
|
|
55
|
+
}
|
package/package.json
CHANGED
package/product-service.d.ts
CHANGED
|
@@ -49,9 +49,11 @@ export type ImportProductTemplateBytesFn = (opts: {
|
|
|
49
49
|
kind: "product-default";
|
|
50
50
|
}>;
|
|
51
51
|
}) => Promise<void>;
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
52
|
+
/** Every container operation this service performs, behind an interface so
|
|
53
|
+
* tests can drive them without shelling out to Docker. Defaults wire straight
|
|
54
|
+
* to docker-runner + the readiness probe. Reach for these rather than the
|
|
55
|
+
* module functions: calling docker-runner directly re-creates the bypass that
|
|
56
|
+
* made an injected fake silently inert on add/remove. */
|
|
55
57
|
export interface ProductContainerOps {
|
|
56
58
|
run(image: string, hostPort: number): Promise<string>;
|
|
57
59
|
remove(containerId: string): Promise<void>;
|
|
@@ -72,8 +74,8 @@ export interface ProductServiceOptions {
|
|
|
72
74
|
/** Liveness probe for dev-mode products. Injectable for tests; defaults to a
|
|
73
75
|
* fast timeout-bounded GET of the dev URL's `/manifest.json`. */
|
|
74
76
|
isDevUrlAlive?: (baseUrl: string) => Promise<boolean>;
|
|
75
|
-
/** Container lifecycle ops used by stopAll/restoreAll.
|
|
76
|
-
* defaults to the real docker-runner functions. */
|
|
77
|
+
/** Container lifecycle ops used by add/remove/restart/stopAll/restoreAll.
|
|
78
|
+
* Injectable for tests; defaults to the real docker-runner functions. */
|
|
77
79
|
containerOps?: ProductContainerOps;
|
|
78
80
|
}
|
|
79
81
|
export declare class ProductService {
|
package/product-service.js
CHANGED
|
@@ -76,25 +76,25 @@ export class ProductService {
|
|
|
76
76
|
}
|
|
77
77
|
port = allocated;
|
|
78
78
|
logger.info(`Starting product container: ${spec.image} on host port ${port}`);
|
|
79
|
-
containerId = await
|
|
79
|
+
containerId = await this.containerOps.run(spec.image, port);
|
|
80
80
|
baseUrl = specBaseUrl(spec, port);
|
|
81
81
|
}
|
|
82
82
|
try {
|
|
83
|
-
await waitForReady(baseUrl);
|
|
83
|
+
await this.containerOps.waitForReady(baseUrl);
|
|
84
84
|
}
|
|
85
85
|
catch (e) {
|
|
86
86
|
if (containerId)
|
|
87
|
-
await
|
|
87
|
+
await this.containerOps.remove(containerId);
|
|
88
88
|
throw e;
|
|
89
89
|
}
|
|
90
90
|
const manifest = await fetchManifest(baseUrl).catch(async (e) => {
|
|
91
91
|
if (containerId)
|
|
92
|
-
await
|
|
92
|
+
await this.containerOps.remove(containerId);
|
|
93
93
|
throw e;
|
|
94
94
|
});
|
|
95
95
|
if (existing.some((p) => p.name === manifest.name)) {
|
|
96
96
|
if (containerId)
|
|
97
|
-
await
|
|
97
|
+
await this.containerOps.remove(containerId);
|
|
98
98
|
throw new ProductError("NAME_CONFLICT", `product '${manifest.name}' already registered`);
|
|
99
99
|
}
|
|
100
100
|
const warnings = [];
|
|
@@ -111,7 +111,7 @@ export class ProductService {
|
|
|
111
111
|
licenseFile = resolved.file;
|
|
112
112
|
if (resolved.fatal) {
|
|
113
113
|
if (containerId)
|
|
114
|
-
await
|
|
114
|
+
await this.containerOps.remove(containerId);
|
|
115
115
|
throw new ProductError("LICENSE_INVALID", resolved.fatal);
|
|
116
116
|
}
|
|
117
117
|
if (resolved.warning)
|
|
@@ -137,19 +137,19 @@ export class ProductService {
|
|
|
137
137
|
const parsed = contents !== undefined ? parseLicenseContents(contents) : undefined;
|
|
138
138
|
if (parsed?.kind === "invalid-v2") {
|
|
139
139
|
if (containerId)
|
|
140
|
-
await
|
|
140
|
+
await this.containerOps.remove(containerId);
|
|
141
141
|
throw new ProductError("LICENSE_INVALID", parsed.reason);
|
|
142
142
|
}
|
|
143
143
|
if (parsed?.kind === "legacy") {
|
|
144
144
|
if (containerId)
|
|
145
|
-
await
|
|
145
|
+
await this.containerOps.remove(containerId);
|
|
146
146
|
throw new ProductError("LICENSE_INVALID", notV2EnvelopeMessage("V1 licenses are no longer accepted at registration"));
|
|
147
147
|
}
|
|
148
148
|
if (parsed?.kind === "v2") {
|
|
149
149
|
const check = checkProductEntitlement(parsed, manifest.name, spec.kind === "container" ? spec.image : undefined);
|
|
150
150
|
if (!check.ok) {
|
|
151
151
|
if (containerId)
|
|
152
|
-
await
|
|
152
|
+
await this.containerOps.remove(containerId);
|
|
153
153
|
throw new ProductError("LICENSE_INVALID", check.reason);
|
|
154
154
|
}
|
|
155
155
|
warnings.push(...check.warnings);
|
|
@@ -159,7 +159,7 @@ export class ProductService {
|
|
|
159
159
|
// stable, human-readable name (replacing docker's random alias). Tracked
|
|
160
160
|
// by id, so a rename failure is harmless — hence best-effort.
|
|
161
161
|
if (containerId)
|
|
162
|
-
await
|
|
162
|
+
await this.containerOps.rename(containerId, productContainerName(manifest.name));
|
|
163
163
|
// Sanity-probe the config screen so we catch (a) URLs that don't
|
|
164
164
|
// serve anything at the manifest-declared path, and (b) the common
|
|
165
165
|
// "registered the vite dev port" mistake — both are hard rejections
|
|
@@ -170,7 +170,7 @@ export class ProductService {
|
|
|
170
170
|
}
|
|
171
171
|
catch (e) {
|
|
172
172
|
if (containerId)
|
|
173
|
-
await
|
|
173
|
+
await this.containerOps.remove(containerId);
|
|
174
174
|
throw e;
|
|
175
175
|
}
|
|
176
176
|
}
|
|
@@ -227,7 +227,7 @@ export class ProductService {
|
|
|
227
227
|
if (!target)
|
|
228
228
|
throw new ProductError("NOT_FOUND", `product '${name}' not registered`);
|
|
229
229
|
if (target.containerId)
|
|
230
|
-
await
|
|
230
|
+
await this.containerOps.remove(target.containerId);
|
|
231
231
|
await this.store.update((products) => products.filter((p) => p.name !== name));
|
|
232
232
|
logger.info(`Product '${name}' removed`);
|
|
233
233
|
}
|