@norskvideo/ctl-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/base.css +53 -0
  2. package/browser.d.ts +8 -0
  3. package/browser.js +11 -0
  4. package/capabilities-router.d.ts +9 -0
  5. package/capabilities-router.js +15 -0
  6. package/cjs-interop.d.ts +33 -0
  7. package/cjs-interop.js +61 -0
  8. package/components/ProductIframe.d.ts +37 -0
  9. package/components/ProductIframe.js +119 -0
  10. package/components/ProductTemplateBuildForm.d.ts +40 -0
  11. package/components/ProductTemplateBuildForm.js +81 -0
  12. package/components/index.d.ts +3 -0
  13. package/components/index.js +3 -0
  14. package/components/ui-primitives.d.ts +22 -0
  15. package/components/ui-primitives.js +13 -0
  16. package/dev-url.d.ts +1 -0
  17. package/dev-url.js +14 -0
  18. package/docker-runner.d.ts +20 -0
  19. package/docker-runner.js +54 -0
  20. package/fonts/Geist-LICENSE.txt +92 -0
  21. package/fonts/Geist.woff2 +0 -0
  22. package/fonts/GeistMono.woff2 +0 -0
  23. package/fonts/README.md +21 -0
  24. package/fonts/STUDIO-FONT-SYNC.md +86 -0
  25. package/index.d.ts +15 -0
  26. package/index.js +17 -0
  27. package/license-registration.d.ts +52 -0
  28. package/license-registration.js +38 -0
  29. package/license-stager.d.ts +30 -0
  30. package/license-stager.js +118 -0
  31. package/license-v2.d.ts +107 -0
  32. package/license-v2.js +205 -0
  33. package/manifest-fetch.d.ts +29 -0
  34. package/manifest-fetch.js +100 -0
  35. package/manifest-router.d.ts +11 -0
  36. package/manifest-router.js +16 -0
  37. package/manifest-schema.d.ts +113 -0
  38. package/manifest-schema.js +135 -0
  39. package/manifest-seed.d.ts +28 -0
  40. package/manifest-seed.js +40 -0
  41. package/openapi-router.d.ts +12 -0
  42. package/openapi-router.js +21 -0
  43. package/package.json +46 -0
  44. package/parsing.d.ts +9 -0
  45. package/parsing.js +83 -0
  46. package/product-error.d.ts +4 -0
  47. package/product-error.js +8 -0
  48. package/product-health-monitor.d.ts +72 -0
  49. package/product-health-monitor.js +136 -0
  50. package/product-service.d.ts +118 -0
  51. package/product-service.js +340 -0
  52. package/product-template-error.d.ts +10 -0
  53. package/product-template-error.js +14 -0
  54. package/product-template-materials.d.ts +17 -0
  55. package/product-template-materials.js +51 -0
  56. package/product-template-parsing.d.ts +14 -0
  57. package/product-template-parsing.js +66 -0
  58. package/product-template-record.d.ts +45 -0
  59. package/product-template-record.js +1 -0
  60. package/product-types.d.ts +31 -0
  61. package/product-types.js +1 -0
  62. package/proxy-middleware.d.ts +7 -0
  63. package/proxy-middleware.js +112 -0
  64. package/runtime.d.ts +2 -0
  65. package/runtime.js +21 -0
  66. package/validate.d.ts +3 -0
  67. package/validate.js +22 -0
  68. package/workflow.d.ts +60 -0
  69. package/workflow.js +57 -0
@@ -0,0 +1,112 @@
1
+ import { logger } from "@norskvideo/ctl-foundation";
2
+ const HOP_BY_HOP = new Set([
3
+ "connection",
4
+ "keep-alive",
5
+ "proxy-authenticate",
6
+ "proxy-authorization",
7
+ "te",
8
+ "trailers",
9
+ "transfer-encoding",
10
+ "upgrade",
11
+ "host",
12
+ "content-length",
13
+ ]);
14
+ function targetBaseUrl(reg) {
15
+ if (reg.spec.kind === "container") {
16
+ return reg.port !== undefined ? `http://127.0.0.1:${reg.port}` : null;
17
+ }
18
+ return reg.spec.url.replace(/\/$/, "");
19
+ }
20
+ function buildOutgoingHeaders(req) {
21
+ const out = {};
22
+ for (const [key, value] of Object.entries(req.headers)) {
23
+ if (HOP_BY_HOP.has(key.toLowerCase()))
24
+ continue;
25
+ if (value === undefined)
26
+ continue;
27
+ out[key] = Array.isArray(value) ? value.join(", ") : value;
28
+ }
29
+ return out;
30
+ }
31
+ async function readRequestBody(req) {
32
+ if (req.method === "GET" || req.method === "HEAD")
33
+ return undefined;
34
+ return new Promise((resolve, reject) => {
35
+ const chunks = [];
36
+ req.on("data", (chunk) => chunks.push(chunk));
37
+ req.on("end", () => {
38
+ if (!chunks.length)
39
+ return resolve(undefined);
40
+ const buf = Buffer.concat(chunks);
41
+ resolve(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
42
+ });
43
+ req.on("error", reject);
44
+ });
45
+ }
46
+ export function createProductProxyMiddleware(svc) {
47
+ return async (req, res, next) => {
48
+ const name = req.params.name;
49
+ if (typeof name !== "string" || !name)
50
+ return next();
51
+ const registrations = svc.list();
52
+ const reg = registrations.find((p) => p.name === name);
53
+ if (!reg) {
54
+ res.status(404).json({ status: "error", message: `unknown product '${name}'` });
55
+ return;
56
+ }
57
+ const base = targetBaseUrl(reg);
58
+ if (!base) {
59
+ res.status(502).json({ status: "error", message: `product '${name}' has no reachable base URL` });
60
+ return;
61
+ }
62
+ const targetUrl = `${base}${req.url}`;
63
+ const headers = buildOutgoingHeaders(req);
64
+ const body = await readRequestBody(req);
65
+ let upstream;
66
+ try {
67
+ upstream = await fetch(targetUrl, {
68
+ method: req.method,
69
+ headers,
70
+ body: body,
71
+ redirect: "manual",
72
+ });
73
+ }
74
+ catch (e) {
75
+ logger.warn(`Proxy to ${targetUrl} failed: ${String(e)}`);
76
+ res.status(502).json({ status: "error", message: `upstream fetch failed: ${String(e)}` });
77
+ return;
78
+ }
79
+ res.status(upstream.status);
80
+ const productPrefix = `/products/${encodeURIComponent(name)}`;
81
+ upstream.headers.forEach((value, key) => {
82
+ const lower = key.toLowerCase();
83
+ if (HOP_BY_HOP.has(lower))
84
+ return;
85
+ // Rewrite root-absolute redirect Locations so 3xx responses don't
86
+ // escape the product prefix and land on the runner SPA. Relative
87
+ // (non-/-prefixed) and full-URL Locations pass through unchanged.
88
+ if (lower === "location" && value.startsWith("/") && !value.startsWith("//")) {
89
+ res.setHeader(key, `${productPrefix}${value}`);
90
+ return;
91
+ }
92
+ res.setHeader(key, value);
93
+ });
94
+ if (!upstream.body) {
95
+ res.end();
96
+ return;
97
+ }
98
+ const reader = upstream.body.getReader();
99
+ try {
100
+ while (true) {
101
+ const { done, value } = await reader.read();
102
+ if (done)
103
+ break;
104
+ res.write(value);
105
+ }
106
+ }
107
+ catch (e) {
108
+ logger.warn(`Proxy stream from ${targetUrl} broke: ${String(e)}`);
109
+ }
110
+ res.end();
111
+ };
112
+ }
package/runtime.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function setBasename(b: string): void;
2
+ export declare function getBasename(): string;
package/runtime.js ADDED
@@ -0,0 +1,21 @@
1
+ // Shared basename state for a product's configure UI. Authoritative source is
2
+ // `init.instanceContext.basename` from the runner; we fall back to deriving it
3
+ // from the URL pre-init so that fetches before init lands and standalone usage
4
+ // both work without a special case. Set once on init, read everywhere.
5
+ //
6
+ // Reached via "@norskvideo/ctl-sdk/runtime" rather than the browser barrel: this has no
7
+ // dependencies, and the barrel would pull the zod-backed schema surface into a
8
+ // frontend that only wants a URL prefix.
9
+ let basename = null;
10
+ export function setBasename(b) {
11
+ basename = b;
12
+ }
13
+ export function getBasename() {
14
+ if (basename !== null)
15
+ return basename;
16
+ // Fallback: strip the `/configure/...` suffix from the iframe's URL.
17
+ // Standalone (`/configure/`) → "". Proxied (`/products/<name>/configure/`)
18
+ // → "/products/<name>". Ensures fetch URLs work before `init` arrives.
19
+ const m = window.location.pathname.match(/^(.*?)\/configure(\/|$)/);
20
+ return m ? m[1] : "";
21
+ }
package/validate.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { ValidationResult } from "@norskvideo/ctl-foundation/browser";
2
+ export declare function validateInstanceId(id: string): ValidationResult;
3
+ export declare function isValidInstanceId(id: string): boolean;
package/validate.js ADDED
@@ -0,0 +1,22 @@
1
+ const ID_PATTERN = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
2
+ export function validateInstanceId(id) {
3
+ if (id.length === 0) {
4
+ return { status: "error", message: "Instance ID must not be empty" };
5
+ }
6
+ if (id.length > 63) {
7
+ return { status: "error", message: "Instance ID must be 63 characters or fewer" };
8
+ }
9
+ if (id.startsWith("-")) {
10
+ return { status: "error", message: "Instance ID must not start with a hyphen" };
11
+ }
12
+ if (id.endsWith("-")) {
13
+ return { status: "error", message: "Instance ID must not end with a hyphen" };
14
+ }
15
+ if (!ID_PATTERN.test(id)) {
16
+ return { status: "error", message: "Instance ID must contain only lowercase letters, digits, and hyphens" };
17
+ }
18
+ return { status: "valid" };
19
+ }
20
+ export function isValidInstanceId(id) {
21
+ return validateInstanceId(id).status === "valid";
22
+ }
package/workflow.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ export type SubscriptionFilter = {
2
+ media: "video" | "audio" | "subtitle" | "ancillary" | "playlist";
3
+ renditionName?: string;
4
+ sourceName?: string;
5
+ programNumber?: number;
6
+ streamId?: number;
7
+ };
8
+ export type SubscriptionStreams = {
9
+ type: "take-all-streams";
10
+ filter: SubscriptionFilter[];
11
+ } | {
12
+ type: "take-first-stream";
13
+ filter: SubscriptionFilter[];
14
+ } | {
15
+ type: "take-specific-streams";
16
+ filter: SubscriptionFilter[];
17
+ };
18
+ export type Subscription = {
19
+ source: string;
20
+ streams: SubscriptionStreams;
21
+ };
22
+ export type WorkflowComponent<Config = Record<string, unknown>> = {
23
+ type: string;
24
+ config: Config;
25
+ subscriptions: Subscription[];
26
+ };
27
+ export type LayoutInfo = {
28
+ id: string;
29
+ x: number;
30
+ y: number;
31
+ };
32
+ export type WorkflowDoc = {
33
+ components: WorkflowComponent[];
34
+ __layout?: LayoutInfo[];
35
+ __globalConfig?: Record<string, unknown>;
36
+ };
37
+ export declare const ALL_AV: SubscriptionStreams;
38
+ export declare const VIDEO_ONLY: SubscriptionStreams;
39
+ export declare const AUDIO_ONLY: SubscriptionStreams;
40
+ export declare const FIRST_VIDEO: SubscriptionStreams;
41
+ export declare const FIRST_AUDIO: SubscriptionStreams;
42
+ export type SyntheticNodeInfo = {
43
+ identifier: string;
44
+ subscription: {
45
+ accepts: {
46
+ type: "simple-stream";
47
+ video: true;
48
+ audio: true;
49
+ subtitle: true;
50
+ ancillary: true;
51
+ playlist: true;
52
+ acceptsTransient: true;
53
+ };
54
+ produces: {
55
+ type: "dynamic-streams";
56
+ streams: <S>(cfg: unknown, inputStreams: S) => S;
57
+ };
58
+ };
59
+ };
60
+ export declare function syntheticInfo<T = SyntheticNodeInfo>(identifier: string): T;
package/workflow.js ADDED
@@ -0,0 +1,57 @@
1
+ // Shared Studio-workflow document shape and the product-agnostic subscription
2
+ // constants, consolidated from the four media-graph products' identical
3
+ // `shared/src/workflow/types.ts` cores. This is the structural shape of the
4
+ // Studio YAML each product emits — zero runtime dependencies, so it lives on
5
+ // its own `./workflow` sdk subpath rather than the `.` barrel (which drags
6
+ // express). Product-specific consts (probe's MARKERS_AND_CAPTIONS, funke's
7
+ // ANCILLARY_ONLY) and per-product document extensions (probe's `__signature`)
8
+ // stay in each product's own `types.ts`.
9
+ export const ALL_AV = {
10
+ type: "take-all-streams",
11
+ filter: [{ media: "video" }, { media: "audio" }],
12
+ };
13
+ export const VIDEO_ONLY = {
14
+ type: "take-all-streams",
15
+ filter: [{ media: "video" }],
16
+ };
17
+ export const AUDIO_ONLY = {
18
+ type: "take-all-streams",
19
+ filter: [{ media: "audio" }],
20
+ };
21
+ export const FIRST_VIDEO = {
22
+ type: "take-first-stream",
23
+ filter: [{ media: "video" }],
24
+ };
25
+ export const FIRST_AUDIO = {
26
+ type: "take-first-stream",
27
+ filter: [{ media: "audio" }],
28
+ };
29
+ // A permissive builder NodeInfo for an identifier the real component library
30
+ // does not know (a product's own component, or an alpha/custom node that lives
31
+ // only in the running studio image). It accepts any media and PASSES INPUT
32
+ // STREAMS THROUGH unchanged, so a node downstream of a synthetic resolves the
33
+ // same streams it would against the real component and the emitted subscriptions
34
+ // match the hand-written ones. Synthetic nodes are not stream/config-validated,
35
+ // but any edge whose SOURCE is a real built-in still validates against it.
36
+ //
37
+ // Generic over the caller's builder NodeInfo type so sdk needs no
38
+ // norsk-studio-builder dependency: each product infers `T` from the position it
39
+ // assigns the result to (a `find(): NodeInfoForBuilder | undefined`), and the
40
+ // body's structure is checked against that `T` at the call site.
41
+ export function syntheticInfo(identifier) {
42
+ return {
43
+ identifier,
44
+ subscription: {
45
+ accepts: {
46
+ type: "simple-stream",
47
+ video: true,
48
+ audio: true,
49
+ subtitle: true,
50
+ ancillary: true,
51
+ playlist: true,
52
+ acceptsTransient: true,
53
+ },
54
+ produces: { type: "dynamic-streams", streams: (_cfg, inputStreams) => inputStreams },
55
+ },
56
+ };
57
+ }