@hypit/hypit 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -6
- package/bin/hypit.mjs +12 -9
- package/dist/public/generation.d.ts +4 -5
- package/examples/provider-package/README.md +92 -16
- package/examples/provider-package/hypit.runtime.json +8 -1
- package/examples/provider-package/packages/provider-videos/package.json +11 -0
- package/examples/provider-package/packages/provider-videos/src/activation.ts +24 -0
- package/examples/provider-package/packages/provider-videos/src/provider.ts +181 -0
- package/examples/provider-package/packages/provider-videos/tsconfig.json +9 -0
- package/package.json +5 -1
- package/packages/build-result/package.json +1 -1
- package/packages/build-result/src/store.ts +1 -1
- package/packages/credential-store-file/README.md +9 -4
- package/packages/credential-store-file/package.json +2 -1
- package/packages/credential-store-file/src/activation.ts +2 -2
- package/packages/credential-store-file/src/index.ts +0 -1
- package/packages/credential-store-file/src/store.ts +13 -40
- package/packages/credential-store-os/runtime/windows-credential.ps1 +1 -2
- package/packages/credential-store-os/src/store.ts +2 -68
- package/packages/credential-store-os/src/windows.ts +47 -0
- package/packages/credential-store-platform/README.md +2 -1
- package/packages/credential-store-platform/src/activation.ts +2 -2
- package/packages/file-io-node/README.md +20 -0
- package/packages/file-io-node/package.json +13 -0
- package/packages/file-io-node/src/index.ts +1 -0
- package/packages/{build-result → file-io-node}/src/replace-file-windows.ts +3 -3
- package/packages/generation/README.md +6 -0
- package/packages/generation/src/mapping.ts +30 -10
- package/packages/media-execution/src/execute.ts +3 -2
- package/packages/media-execution/src/toolchain.ts +10 -36
- package/packages/provider-hiapi/README.md +75 -0
- package/packages/provider-hiapi/package.json +24 -0
- package/packages/provider-hiapi/src/activation.ts +62 -0
- package/packages/provider-hiapi/src/errors.ts +44 -0
- package/packages/provider-hiapi/src/index.ts +2 -0
- package/packages/provider-hiapi/src/mapping.ts +129 -0
- package/packages/provider-hiapi/src/provider.ts +215 -0
- package/packages/provider-hiapi/src/routes.ts +154 -0
- package/packages/provider-hyperframes-local/src/program.ts +2 -1
- package/packages/provider-media-local/README.md +2 -1
- package/packages/provider-monid/README.md +60 -0
- package/packages/provider-monid/package.json +19 -0
- package/packages/provider-monid/src/activation.ts +62 -0
- package/packages/provider-monid/src/errors.ts +55 -0
- package/packages/provider-monid/src/index.ts +2 -0
- package/packages/provider-monid/src/mapping.ts +33 -0
- package/packages/provider-monid/src/provider.ts +242 -0
- package/packages/provider-monid/src/routes.ts +103 -0
- package/packages/provider-pollo/README.md +59 -0
- package/packages/provider-pollo/package.json +22 -0
- package/packages/provider-pollo/src/activation.ts +62 -0
- package/packages/provider-pollo/src/errors.ts +41 -0
- package/packages/provider-pollo/src/index.ts +2 -0
- package/packages/provider-pollo/src/mapping.ts +60 -0
- package/packages/provider-pollo/src/provider.ts +194 -0
- package/packages/provider-pollo/src/routes.ts +120 -0
- package/packages/provider-tokendance/README.md +67 -0
- package/packages/provider-tokendance/package.json +21 -0
- package/packages/provider-tokendance/src/activation.ts +62 -0
- package/packages/provider-tokendance/src/errors.ts +47 -0
- package/packages/provider-tokendance/src/index.ts +2 -0
- package/packages/provider-tokendance/src/mapping.ts +60 -0
- package/packages/provider-tokendance/src/provider.ts +268 -0
- package/packages/provider-tokendance/src/routes.ts +192 -0
- package/packages/runtime-local/src/programs.ts +2 -0
- package/packages/video-cli/package.json +4 -0
- package/packages/credential-store-file/src/paths.ts +0 -16
- /package/packages/{build-result → file-io-node}/src/replace-file.ts +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { replaceFile } from "@hypit/file-io-node";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdir, open,
|
|
3
|
+
import { mkdir, open, rm, stat, unlink } from "node:fs/promises";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { verifyCredentialRef } from "@hypit/runtime";
|
|
5
6
|
import type { CredentialRef, CredentialValue, WritableCredentialStore } from "@hypit/runtime";
|
|
@@ -8,26 +9,6 @@ function missing(error: unknown): boolean {
|
|
|
8
9
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
/**
|
|
12
|
-
* One process reads a credential for one Endpoint action while another action's refresh replaces
|
|
13
|
-
* it, so a read handle and a replacement can reach the same document at the same instant. Windows
|
|
14
|
-
* refuses to replace a file any handle still holds. Each document's work takes its turn instead,
|
|
15
|
-
* keyed by path so separate Store instances over one directory share the same order. Keys are
|
|
16
|
-
* independent: a turn on one document never delays another.
|
|
17
|
-
*/
|
|
18
|
-
const documentTurns = new Map<string, Promise<unknown>>();
|
|
19
|
-
|
|
20
|
-
function onDocument<T>(path: string, work: () => Promise<T>): Promise<T> {
|
|
21
|
-
const turn = (documentTurns.get(path) ?? Promise.resolve()).then(work, work);
|
|
22
|
-
// The successor waits for this turn to settle, not to succeed, and the last turn clears the path.
|
|
23
|
-
const settled = turn.then(() => undefined, () => undefined);
|
|
24
|
-
documentTurns.set(path, settled);
|
|
25
|
-
void settled.then(() => {
|
|
26
|
-
if (documentTurns.get(path) === settled) documentTurns.delete(path);
|
|
27
|
-
});
|
|
28
|
-
return turn;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
12
|
function credentialValue(value: unknown): CredentialValue {
|
|
32
13
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid credential value");
|
|
33
14
|
const record = value as Record<string, unknown>;
|
|
@@ -64,10 +45,6 @@ export class FileCredentialStore implements WritableCredentialStore {
|
|
|
64
45
|
async resolve(ref: CredentialRef): Promise<CredentialValue | undefined> {
|
|
65
46
|
if (!this.owns(ref)) return undefined;
|
|
66
47
|
const path = this.#path(ref);
|
|
67
|
-
return await onDocument(path, async () => await this.#read(path));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async #read(path: string): Promise<CredentialValue | undefined> {
|
|
71
48
|
try { await this.#privateDirectory(false); } catch (error) {
|
|
72
49
|
if (missing(error)) return undefined;
|
|
73
50
|
throw error;
|
|
@@ -93,25 +70,21 @@ export class FileCredentialStore implements WritableCredentialStore {
|
|
|
93
70
|
async put(ref: CredentialRef, value: CredentialValue): Promise<void> {
|
|
94
71
|
const path = this.#path(ref);
|
|
95
72
|
const contents = JSON.stringify(credentialValue(value));
|
|
96
|
-
await
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
} finally { await rm(temporary, { force: true }); }
|
|
104
|
-
});
|
|
73
|
+
await this.#privateDirectory(true);
|
|
74
|
+
const temporary = join(this.directory, `.write-${randomUUID()}.tmp`);
|
|
75
|
+
const file = await open(temporary, "wx", 0o600);
|
|
76
|
+
try {
|
|
77
|
+
try { await file.writeFile(`${contents}\n`, "utf8"); } finally { await file.close(); }
|
|
78
|
+
await replaceFile(temporary, path);
|
|
79
|
+
} finally { await rm(temporary, { force: true }); }
|
|
105
80
|
}
|
|
106
81
|
|
|
107
82
|
async delete(ref: CredentialRef): Promise<boolean> {
|
|
108
83
|
const path = this.#path(ref);
|
|
109
84
|
// Deletion never opens or decodes the old document.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
});
|
|
85
|
+
try { await unlink(path); return true; } catch (error) {
|
|
86
|
+
if (missing(error)) return false;
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
116
89
|
}
|
|
117
90
|
}
|
|
@@ -35,8 +35,7 @@ switch ($Operation) {
|
|
|
35
35
|
@{ found = $true; secret = [Convert]::ToBase64String($bytes) } | ConvertTo-Json -Compress
|
|
36
36
|
}
|
|
37
37
|
"write" {
|
|
38
|
-
|
|
39
|
-
if ($null -ne $existing) { $vault.Remove($existing) }
|
|
38
|
+
# Add replaces the same resource/account; do not destroy the old value before it succeeds.
|
|
40
39
|
$bytes = [Convert]::FromBase64String([string]$request.secret)
|
|
41
40
|
$secret = [System.Text.Encoding]::UTF8.GetString($bytes)
|
|
42
41
|
$credential = New-Object Windows.Security.Credentials.PasswordCredential(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { execFile
|
|
2
|
-
import {
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { windowsCredential } from "./windows.js";
|
|
3
3
|
|
|
4
4
|
import { verifyCredentialRef } from "@hypit/runtime";
|
|
5
5
|
import type {
|
|
@@ -59,72 +59,6 @@ function macosDeleter(service: string): OsCredentialDeleter {
|
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
type WindowsCredentialResult = {
|
|
63
|
-
readonly found?: boolean;
|
|
64
|
-
readonly deleted?: boolean;
|
|
65
|
-
readonly secret?: string;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const windowsScript = fileURLToPath(new URL("../runtime/windows-credential.ps1", import.meta.url));
|
|
69
|
-
|
|
70
|
-
const windowsCredentialTimeoutMs = 10_000;
|
|
71
|
-
|
|
72
|
-
function windowsCredential(
|
|
73
|
-
operation: "read" | "write" | "delete",
|
|
74
|
-
service: string,
|
|
75
|
-
account: string,
|
|
76
|
-
secret?: string,
|
|
77
|
-
): Promise<WindowsCredentialResult> {
|
|
78
|
-
return new Promise((resolve, reject) => {
|
|
79
|
-
const child = spawn("powershell.exe", [
|
|
80
|
-
"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
|
|
81
|
-
"-File", windowsScript, "-Operation", operation,
|
|
82
|
-
], { shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
83
|
-
let stdout = "";
|
|
84
|
-
let stderr = "";
|
|
85
|
-
let settled = false;
|
|
86
|
-
const fail = (error: Error): void => {
|
|
87
|
-
if (settled) return;
|
|
88
|
-
settled = true;
|
|
89
|
-
clearTimeout(timer);
|
|
90
|
-
reject(error);
|
|
91
|
-
};
|
|
92
|
-
const timer = setTimeout(() => {
|
|
93
|
-
fail(new Error(`Windows credential ${operation} for ${account} timed out`));
|
|
94
|
-
child.kill("SIGKILL");
|
|
95
|
-
}, windowsCredentialTimeoutMs);
|
|
96
|
-
child.on("error", fail);
|
|
97
|
-
child.stdout.on("data", (chunk: Buffer) => {
|
|
98
|
-
stdout += chunk.toString("utf8");
|
|
99
|
-
if (stdout.length > 4 * 1024 * 1024) fail(new Error("Windows credential response is too large"));
|
|
100
|
-
});
|
|
101
|
-
child.stderr.on("data", (chunk: Buffer) => {
|
|
102
|
-
stderr += chunk.toString("utf8");
|
|
103
|
-
if (stderr.length > 64 * 1024) fail(new Error("Windows credential error is too large"));
|
|
104
|
-
});
|
|
105
|
-
child.on("close", (code) => {
|
|
106
|
-
if (settled) return;
|
|
107
|
-
clearTimeout(timer);
|
|
108
|
-
if (code !== 0) {
|
|
109
|
-
fail(new Error(`Windows credential ${operation} for ${account} failed${stderr.trim().length === 0 ? "" : `: ${stderr.trim()}`}`));
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
try {
|
|
113
|
-
const result = JSON.parse(stdout.replace(/^\uFEFF/u, "").trim()) as WindowsCredentialResult;
|
|
114
|
-
settled = true;
|
|
115
|
-
resolve(result);
|
|
116
|
-
} catch {
|
|
117
|
-
fail(new Error(`Windows credential ${operation} returned an invalid response`));
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
child.stdin.end(JSON.stringify({
|
|
121
|
-
service,
|
|
122
|
-
account,
|
|
123
|
-
...(secret === undefined ? {} : { secret: Buffer.from(secret, "utf8").toString("base64") }),
|
|
124
|
-
}));
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
62
|
function windowsBackend(): {
|
|
129
63
|
readonly read: OsCredentialReader;
|
|
130
64
|
readonly write: OsCredentialWriter;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
type WindowsCredentialResult = {
|
|
5
|
+
readonly found?: boolean;
|
|
6
|
+
readonly deleted?: boolean;
|
|
7
|
+
readonly secret?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const windowsScript = fileURLToPath(new URL("../runtime/windows-credential.ps1", import.meta.url));
|
|
11
|
+
|
|
12
|
+
/** The OS adapter owns this one child; Node owns its timeout, bounded output and termination. */
|
|
13
|
+
export function windowsCredential(
|
|
14
|
+
operation: "read" | "write" | "delete",
|
|
15
|
+
service: string,
|
|
16
|
+
account: string,
|
|
17
|
+
secret?: string,
|
|
18
|
+
): Promise<WindowsCredentialResult> {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const child = execFile("powershell.exe", [
|
|
21
|
+
"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
|
|
22
|
+
"-File", windowsScript, "-Operation", operation,
|
|
23
|
+
], {
|
|
24
|
+
shell: false, windowsHide: true, encoding: "utf8", timeout: 10_000,
|
|
25
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
26
|
+
}, (error, stdout, stderr) => {
|
|
27
|
+
if (error !== null) {
|
|
28
|
+
reject(new Error(`Windows credential ${operation} for ${account} failed`
|
|
29
|
+
+ (error.code === undefined ? "" : ` (${error.code})`)
|
|
30
|
+
+ (error.killed ? " (child terminated)" : "")
|
|
31
|
+
+ (stderr.trim().length === 0 ? "" : `: ${stderr.trim()}`)));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try { resolve(JSON.parse(stdout.replace(/^\uFEFF/u, "").trim()) as WindowsCredentialResult); }
|
|
35
|
+
catch { reject(new Error(`Windows credential ${operation} returned an invalid response`)); }
|
|
36
|
+
});
|
|
37
|
+
// A closed input pipe is an operation failure too. Do not leave the child waiting for a request.
|
|
38
|
+
child.stdin!.on("error", () => {
|
|
39
|
+
child.kill();
|
|
40
|
+
reject(new Error(`Windows credential ${operation} request could not be written`));
|
|
41
|
+
});
|
|
42
|
+
child.stdin!.end(JSON.stringify({
|
|
43
|
+
service, account,
|
|
44
|
+
...(secret === undefined ? {} : { secret: Buffer.from(secret, "utf8").toString("base64") }),
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -53,7 +53,8 @@ credential stored in a locker stays in that locker; a credential stored in a fil
|
|
|
53
53
|
|
|
54
54
|
On macOS and Windows the credential is held by the platform locker, with exactly the rules of
|
|
55
55
|
`@hypit/credential-store-os`: it is encrypted and access-controlled by the operating system, and it
|
|
56
|
-
|
|
56
|
+
follows that locker’s access and synchronization policy. Windows Credential Locker may roam
|
|
57
|
+
credentials through the user’s Microsoft account.
|
|
57
58
|
|
|
58
59
|
On Linux the credential is held by an owner-private document with exactly the rules of
|
|
59
60
|
`@hypit/credential-store-file`: unencrypted JSON, one document per key, directory mode `0700`, file
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
1
2
|
import {
|
|
2
3
|
createRuntimeCredentialStoreAdapterFacet,
|
|
3
4
|
runtimeConfigExact,
|
|
4
5
|
runtimeConfigObject,
|
|
5
6
|
runtimeConfigString,
|
|
6
7
|
} from "@hypit/runtime-kit";
|
|
7
|
-
import { resolveCredentialDirectory } from "@hypit/credential-store-file";
|
|
8
8
|
import { PlatformCredentialStore } from "./store.js";
|
|
9
9
|
|
|
10
10
|
const platformCredentialStoreAdapter = createRuntimeCredentialStoreAdapterFacet({
|
|
@@ -23,7 +23,7 @@ const platformCredentialStoreAdapter = createRuntimeCredentialStoreAdapterFacet(
|
|
|
23
23
|
value: new PlatformCredentialStore({
|
|
24
24
|
// Linux uses the same directory the file Store uses by default, so a Profile
|
|
25
25
|
// that switches between them on Linux finds the credential it stored.
|
|
26
|
-
directory:
|
|
26
|
+
directory: path === undefined ? join(context.hostStateRoot, "credentials") : resolve(context.hostStateRoot, path),
|
|
27
27
|
...(service === undefined ? {} : { service }),
|
|
28
28
|
}),
|
|
29
29
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# `@hypit/file-io-node`
|
|
2
|
+
|
|
3
|
+
Node filesystem operations shared by file-backed adapters. This package has no knowledge of
|
|
4
|
+
credentials, Build Results, Runtime or repository formats.
|
|
5
|
+
|
|
6
|
+
`replaceFile(from, to)` publishes a complete file at a destination on the same filesystem. The
|
|
7
|
+
caller owns creating and closing the source file, its contents and permissions, and temporary-file
|
|
8
|
+
cleanup. Existing readers retain the old file; subsequent opens see the new file. The destination
|
|
9
|
+
may be absent. Concurrent replacements are ordered by the filesystem, with the last replacement
|
|
10
|
+
supplying the current value.
|
|
11
|
+
|
|
12
|
+
POSIX uses Node's `rename`. Windows uses `SetFileInformationByHandle(FileRenameInfoEx)` with
|
|
13
|
+
`REPLACE_IF_EXISTS | POSIX_SEMANTICS`, the same primitive previously owned by `build-result`.
|
|
14
|
+
This avoids `MoveFileExW` rejecting replacement while another process has the destination open.
|
|
15
|
+
The native structure layout comes from Koffi; constants are Windows API values.
|
|
16
|
+
|
|
17
|
+
There is no pre-read, queue, lock file, retry or fallback. Windows filesystems must support this
|
|
18
|
+
operation; unsupported operations, access denials and other I/O errors are reported to the caller.
|
|
19
|
+
Read-only destinations are not overridden. The package does not promise power-loss durability or
|
|
20
|
+
coordinate transactions across multiple files.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { replaceFile } from "./replace-file.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { toNamespacedPath } from "node:path";
|
|
1
|
+
import { resolve, toNamespacedPath } from "node:path";
|
|
2
2
|
import koffi from "koffi";
|
|
3
3
|
|
|
4
4
|
const kernel = koffi.load("kernel32.dll");
|
|
@@ -35,7 +35,7 @@ function failure(syscall: string, from: string, to: string): Error {
|
|
|
35
35
|
* https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/4217551b-d2c0-42cb-9dc1-69a716cf6d0c
|
|
36
36
|
*/
|
|
37
37
|
export function replaceWindowsFile(from: string, to: string): void {
|
|
38
|
-
const filename = Buffer.from(toNamespacedPath(to), "utf16le");
|
|
38
|
+
const filename = Buffer.from(toNamespacedPath(resolve(to)), "utf16le");
|
|
39
39
|
const nameOffset = koffi.offsetof(renameInfo, "FileName");
|
|
40
40
|
// Win32 requires a NUL-terminated FileName even though FileNameLength excludes
|
|
41
41
|
// that terminator. Buffer.alloc leaves the final WCHAR zero-initialized.
|
|
@@ -45,7 +45,7 @@ export function replaceWindowsFile(from: string, to: string): void {
|
|
|
45
45
|
info.writeUInt32LE(filename.length, koffi.offsetof(renameInfo, "FileNameLength"));
|
|
46
46
|
filename.copy(info, nameOffset);
|
|
47
47
|
|
|
48
|
-
const handle = createFile(toNamespacedPath(from), DELETE, SHARE_READ_WRITE_DELETE,
|
|
48
|
+
const handle = createFile(toNamespacedPath(resolve(from)), DELETE, SHARE_READ_WRITE_DELETE,
|
|
49
49
|
null, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) as number | bigint;
|
|
50
50
|
if (handle === -1 || handle === -1n) throw failure("CreateFileW", from, to);
|
|
51
51
|
try {
|
|
@@ -37,3 +37,9 @@ Media wire mappings (`url`, `urlArray`, `itemObject`) may declare `resourceField
|
|
|
37
37
|
paths against the declared media fields. False, zero and empty strings remain values; omitted fields
|
|
38
38
|
remain absent. The resolver implements the service protocol; this package knows no particular
|
|
39
39
|
service or classification. Providers using custom transports carry those fields through that boundary.
|
|
40
|
+
|
|
41
|
+
A Model item field being optional means authors may omit it. If a request supplies that field,
|
|
42
|
+
`mappingSupportsRequest` and final wire compilation require the mapping to carry it through
|
|
43
|
+
`fieldKeys` or `resourceFields`, including an explicit `false`. A service may support requests
|
|
44
|
+
without a particular optional field and refuse requests that supply it; no field is silently dropped.
|
|
45
|
+
Final compilation checks the whole request before resolving or uploading any reference.
|
|
@@ -89,8 +89,9 @@ export function selectWireModelForRequest(
|
|
|
89
89
|
request: GenerationRequest,
|
|
90
90
|
pendingPorts: readonly string[] = [],
|
|
91
91
|
): string {
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
const unsupported = unsupportedRequestField(mapping, request);
|
|
93
|
+
assert(unsupported === undefined,
|
|
94
|
+
`${mapping.capability.name} request contains ${unsupported} this Provider cannot map`);
|
|
94
95
|
const present = new Set(presentPorts(request));
|
|
95
96
|
for (const port of pendingPorts) {
|
|
96
97
|
assert(mapping.fields[port] !== undefined,
|
|
@@ -149,19 +150,38 @@ export async function compileWireRequest(
|
|
|
149
150
|
return { model, input: canonicalize(input) };
|
|
150
151
|
}
|
|
151
152
|
|
|
152
|
-
/**
|
|
153
|
-
|
|
153
|
+
/** Describe only a supplied value the mapping cannot carry; no model table or saved verdict. */
|
|
154
|
+
function unsupportedRequestField(mapping: GenerationWireMapping, value: unknown): string | undefined {
|
|
154
155
|
const request = value as GenerationRequest | undefined;
|
|
155
|
-
if (request === undefined || request.ports === null || typeof request.ports !== "object")
|
|
156
|
-
|
|
156
|
+
if (request === undefined || request === null || request.ports === null || typeof request.ports !== "object") {
|
|
157
|
+
return "invalid ports";
|
|
158
|
+
}
|
|
159
|
+
for (const [port, supplied] of Object.entries(request.ports)) {
|
|
160
|
+
const field = mapping.fields[port];
|
|
161
|
+
if (field === undefined) return `port ${port}`;
|
|
162
|
+
if (field.as !== "url" && field.as !== "urlArray" && field.as !== "itemObject") continue;
|
|
163
|
+
for (const item of supplied as readonly GenerationMediaValue[]) {
|
|
164
|
+
for (const name of Object.keys(item.fields ?? {})) {
|
|
165
|
+
if (!field.resourceFields?.includes(name)
|
|
166
|
+
&& !(field.as === "itemObject" && Object.hasOwn(field.fieldKeys, name))) {
|
|
167
|
+
return `field ${port}.${name}`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Optional fields may be omitted by authors, but supplied fields must reach the service. */
|
|
176
|
+
export function mappingSupportsRequest(mapping: GenerationWireMapping, value: unknown): boolean {
|
|
177
|
+
return unsupportedRequestField(mapping, value) === undefined;
|
|
157
178
|
}
|
|
158
179
|
|
|
159
180
|
/**
|
|
160
|
-
*
|
|
181
|
+
* Check structural port coverage and required item fields against the model declaration.
|
|
161
182
|
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* generation returned the wrong result.
|
|
183
|
+
* A service may omit optional item capabilities. mappingSupportsRequest and final compilation
|
|
184
|
+
* additionally refuse requests that actually supply such an unmapped field.
|
|
165
185
|
*/
|
|
166
186
|
export function assertMappingCoversPorts(
|
|
167
187
|
table: GenerationPortTable,
|
|
@@ -689,7 +689,7 @@ export async function executeNormalizeMedia(
|
|
|
689
689
|
await runProcess({
|
|
690
690
|
executable: env.ffmpegPath,
|
|
691
691
|
argv: ["-y", ...visualInput, "-an", "-vf", filter,
|
|
692
|
-
"-frames:v", String(plan.frameCount), "-fps_mode", "cfr", ...encoderArgs, output],
|
|
692
|
+
"-frames:v", String(plan.frameCount), "-r", fps, "-fps_mode", "cfr", ...encoderArgs, output],
|
|
693
693
|
timeoutMs: env.processTimeoutMs,
|
|
694
694
|
maxStdoutBytes: 64 * 1024,
|
|
695
695
|
...(env.sharedLibraryPath === undefined ? {} : { sharedLibraryPath: env.sharedLibraryPath }),
|
|
@@ -951,7 +951,8 @@ export async function executeTransformMedia(
|
|
|
951
951
|
argv.push("-map", "0:v:0", "-an", "-vf", plan.videoFilters.join(","));
|
|
952
952
|
}
|
|
953
953
|
argv.push(
|
|
954
|
-
"-frames:v", String(plan.frameCount),
|
|
954
|
+
"-frames:v", String(plan.frameCount),
|
|
955
|
+
"-r", `${media.timeline.frameRate.numerator}/${media.timeline.frameRate.denominator}`, "-fps_mode", "cfr",
|
|
955
956
|
"-c:v", "libx264", "-preset", "veryfast", "-pix_fmt", "yuv420p",
|
|
956
957
|
"-movflags", "+faststart", output,
|
|
957
958
|
);
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { mediaProcessEnv } from "./process-env.js";
|
|
2
3
|
|
|
3
4
|
export type MediaToolchainState =
|
|
4
5
|
| { readonly state: "ready"; readonly ffprobeVersion: string; readonly ffmpegVersion?: string }
|
|
5
6
|
| { readonly state: "down" | "mismatch"; readonly detail: string };
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
const REQUIRED_FILTERS = [
|
|
9
|
-
"aformat", "amix", "aresample", "asetpts", "atempo", "atrim",
|
|
10
|
-
"loop", "pad", "scale", "select", "setpts", "setsar", "trim",
|
|
11
|
-
] as const;
|
|
12
|
-
|
|
13
|
-
function run(executable: string, args: readonly string[]): Promise<{ ok: boolean; output: string }> {
|
|
8
|
+
function run(executable: string, args: readonly string[], env: NodeJS.ProcessEnv): Promise<{ ok: boolean; output: string }> {
|
|
14
9
|
return new Promise((resolve) => {
|
|
15
10
|
execFile(executable, [...args], {
|
|
11
|
+
env,
|
|
16
12
|
timeout: 15_000,
|
|
17
13
|
shell: false,
|
|
18
14
|
windowsHide: true,
|
|
@@ -29,22 +25,18 @@ function firstLine(value: string): string {
|
|
|
29
25
|
return value.split(/\r?\n/u, 1)[0]?.trim() ?? "";
|
|
30
26
|
}
|
|
31
27
|
|
|
32
|
-
function missingNames(output: string, required: readonly string[]): readonly string[] {
|
|
33
|
-
return required.filter((name) => !new RegExp(`(?:^|\\s)${name.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}(?:\\s|$)`, "mu")
|
|
34
|
-
.test(output));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
28
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* a custom installation remains valid when it supplies the same capability.
|
|
29
|
+
* Check availability of the selected executables in their execution environment.
|
|
30
|
+
* This does not predict compatibility with every future media command: the execution
|
|
31
|
+
* implementation owns those commands and reports the tool's actual failure.
|
|
42
32
|
*/
|
|
43
33
|
export async function probeMediaToolchain(options: {
|
|
44
34
|
readonly ffprobePath: string;
|
|
45
35
|
readonly ffmpegPath?: string;
|
|
36
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
46
37
|
}): Promise<MediaToolchainState> {
|
|
47
|
-
const
|
|
38
|
+
const env = options.environment ?? mediaProcessEnv();
|
|
39
|
+
const probeVersion = await run(options.ffprobePath, ["-version"], env);
|
|
48
40
|
if (!probeVersion.ok) {
|
|
49
41
|
return { state: "down", detail: `${options.ffprobePath} is unavailable: ${probeVersion.output}` };
|
|
50
42
|
}
|
|
@@ -52,28 +44,10 @@ export async function probeMediaToolchain(options: {
|
|
|
52
44
|
if (ffprobeVersion.length === 0) return { state: "mismatch", detail: "ffprobe returned no version" };
|
|
53
45
|
if (options.ffmpegPath === undefined) return { state: "ready", ffprobeVersion };
|
|
54
46
|
|
|
55
|
-
const
|
|
56
|
-
run(options.ffmpegPath, ["-version"]),
|
|
57
|
-
run(options.ffmpegPath, ["-hide_banner", "-encoders"]),
|
|
58
|
-
run(options.ffmpegPath, ["-hide_banner", "-filters"]),
|
|
59
|
-
]);
|
|
47
|
+
const ffmpegVersionResult = await run(options.ffmpegPath, ["-version"], env);
|
|
60
48
|
if (!ffmpegVersionResult.ok) {
|
|
61
49
|
return { state: "down", detail: `${options.ffmpegPath} is unavailable: ${ffmpegVersionResult.output}` };
|
|
62
50
|
}
|
|
63
|
-
if (!encoders.ok || !filters.ok) {
|
|
64
|
-
return { state: "mismatch", detail: "ffmpeg could not enumerate its encoders and filters" };
|
|
65
|
-
}
|
|
66
|
-
const missingEncoders = missingNames(encoders.output, REQUIRED_ENCODERS);
|
|
67
|
-
const missingFilters = missingNames(filters.output, REQUIRED_FILTERS);
|
|
68
|
-
if (missingEncoders.length > 0 || missingFilters.length > 0) {
|
|
69
|
-
return {
|
|
70
|
-
state: "mismatch",
|
|
71
|
-
detail: [
|
|
72
|
-
missingEncoders.length === 0 ? "" : `missing encoders ${missingEncoders.join(", ")}`,
|
|
73
|
-
missingFilters.length === 0 ? "" : `missing filters ${missingFilters.join(", ")}`,
|
|
74
|
-
].filter(Boolean).join("; "),
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
51
|
const ffmpegVersion = firstLine(ffmpegVersionResult.output);
|
|
78
52
|
return ffmpegVersion.length === 0
|
|
79
53
|
? { state: "mismatch", detail: "ffmpeg returned no version" }
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# `@hypit/provider-hiapi`
|
|
2
|
+
|
|
3
|
+
Hypit Runtime Provider for a [HiAPI](https://www.hiapi.ai) account. Every capability submits one
|
|
4
|
+
task to `POST /v1/tasks` with the model's documented `input` fields, polls
|
|
5
|
+
`GET /v1/tasks/{taskId}` until the task is terminal, downloads `data.output[].url` and stores the
|
|
6
|
+
files in the current Build. Submission carries an `Idempotency-Key`, so a retried submission returns
|
|
7
|
+
the original task.
|
|
8
|
+
|
|
9
|
+
| Capability | HiAPI model |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `@hypit/seedance@1#seedance-2` | `seedance-2.0` |
|
|
12
|
+
| `@hypit/seedance@1#seedance-2-fast` | `seedance-2.0-fast` |
|
|
13
|
+
| `@hypit/seedance@1#seedance-2-mini` | `seedance-2.0-mini` |
|
|
14
|
+
| `@hypit/seedance@1#seedance-2.5` | `seedance-2.5/reference-to-video` with a reference video, `seedance-2.5/image-to-video` with frames, reference images or reference audio, otherwise `seedance-2.5/text-to-video` |
|
|
15
|
+
| `@hypit/seedream@1#seedream-5-lite` | `seedream-5.0-lite/image-to-image` with references, otherwise `seedream-5.0-lite/text-to-image` |
|
|
16
|
+
| `@hypit/minimax-h3@1#minimax-h3` | `minimax-h3` |
|
|
17
|
+
| `@hypit/gpt-image@1#gpt-image-2` | `gpt-image-2/image-to-image` with references, otherwise `gpt-image-2/text-to-image` |
|
|
18
|
+
| `@hypit/nano-banana@1#nano-banana-2` | `Nano-Banana-2` |
|
|
19
|
+
| `@hypit/nano-banana@1#nano-banana-pro` | `Nano-Banana-Pro` |
|
|
20
|
+
| `@hypit/grok-imagine@1#grok-imagine-video` | `grok-imagine/image-to-video` with images, otherwise `grok-imagine/text-to-video` |
|
|
21
|
+
| `@hypit/grok-imagine@1#grok-imagine-video-1.5-preview` | `grok-imagine-1.5/image-to-video` |
|
|
22
|
+
|
|
23
|
+
HiAPI's model index at `https://www.hiapi.ai/docs/models.json` lists further models and routes;
|
|
24
|
+
this Provider maps only the models the Distribution already describes, on each model's standard
|
|
25
|
+
route.
|
|
26
|
+
|
|
27
|
+
Service limits this Provider reports as unsupported before submitting:
|
|
28
|
+
|
|
29
|
+
- `seedance-2.0-mini` has no `web_search` field; `web-search="true"` is unsupported there.
|
|
30
|
+
- `seedance-2.5/image-to-video` takes only `aspect-ratio="adaptive"`; `text-to-video` and
|
|
31
|
+
`image-to-video` render 720p or 1080p, `reference-to-video` also 480p.
|
|
32
|
+
- Seedream 5.0 lite renders 2K (`quality="basic"`) or 4K (`quality="ultra"`); `high` (3K) is
|
|
33
|
+
unsupported. `output-format` and `nsfw-check` have no HiAPI field and are not sent.
|
|
34
|
+
- `minimax-h3` renders 2K only and accepts up to five reference images; requests send
|
|
35
|
+
`watermark: false`.
|
|
36
|
+
- GPT Image 2: `background` only at 1K; `auto` ratio only at 1K; 2K excludes `5:4`, `4:5`, `3:1`,
|
|
37
|
+
`1:3`, `9:21`; 4K excludes `1:1`, `3:1`, `1:3`, `9:21`; image-to-image takes up to six references.
|
|
38
|
+
- Grok Imagine renders 480p or 720p; `grok-imagine-1.5/image-to-video` animates exactly one image.
|
|
39
|
+
|
|
40
|
+
Seedance visual references may carry `person-reference`; the Provider accepts the declaration and
|
|
41
|
+
transmits nothing for it, since HiAPI has no field for it. Seedance rejects reference images and
|
|
42
|
+
videos that contain a real human face; HiAPI offers no way to register authorized portrait material,
|
|
43
|
+
so such a request fails with the service's moderation error.
|
|
44
|
+
|
|
45
|
+
Reference images and audio travel inline as `data:` URLs, which HiAPI documents for its Seedance
|
|
46
|
+
inputs, within the per-file sizes each model page states: 30 MB images and 15 MB audio for
|
|
47
|
+
`seedance-2.0-mini` and the Seedance 2.5 models (2.5 also caps combined images at 120 MB), 10 MB
|
|
48
|
+
images for `seedream-5.0-lite/image-to-image` and `grok-imagine/image-to-video`, 20 MB for
|
|
49
|
+
`grok-imagine-1.5/image-to-video`, 30 MB for `Nano-Banana-Pro`. The Provider checks these before
|
|
50
|
+
submitting. A reference video must be a public HTTPS URL; configure `publicAssetUrl` when embedding
|
|
51
|
+
the Provider, otherwise such a request fails before submission.
|
|
52
|
+
|
|
53
|
+
Runtime Profile example:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"endpoints": {
|
|
58
|
+
"hiapi.default": {
|
|
59
|
+
"use": "@hypit/provider-hiapi",
|
|
60
|
+
"pool": "hiapi.default",
|
|
61
|
+
"config": {
|
|
62
|
+
"apiKey": { "store": "platform", "key": "hiapi.api-key" },
|
|
63
|
+
"defaultConcurrency": 3,
|
|
64
|
+
"pollIntervalMs": 10000
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`baseUrl` defaults to `https://api.hiapi.ai`. Store the API key with
|
|
72
|
+
`hypit auth login hiapi.default --runtime hypit.runtime.json`. Optional `requestTimeoutMs`,
|
|
73
|
+
`operationTimeoutMs` and `actionLimits` bound single HTTP calls, the whole remote task and action
|
|
74
|
+
concurrency. HTTP failures keep HiAPI's `error_code` and message; failed tasks keep
|
|
75
|
+
`data.error.code` and message, with any signed URL in the message redacted.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hypit/provider-hiapi",
|
|
3
|
+
"version": "0.0.0-dev",
|
|
4
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": { ".": "./src/index.ts" },
|
|
8
|
+
"hypit": { "activation": "./src/activation.ts" },
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@hypit/endpoint-kit": "workspace:*",
|
|
11
|
+
"@hypit/generation": "workspace:*",
|
|
12
|
+
"@hypit/protocol": "workspace:*",
|
|
13
|
+
"@hypit/runtime": "workspace:*",
|
|
14
|
+
"@hypit/runtime-kit": "workspace:*"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@hypit/gpt-image": "workspace:*",
|
|
18
|
+
"@hypit/grok-imagine": "workspace:*",
|
|
19
|
+
"@hypit/minimax-h3": "workspace:*",
|
|
20
|
+
"@hypit/nano-banana": "workspace:*",
|
|
21
|
+
"@hypit/seedance": "workspace:*",
|
|
22
|
+
"@hypit/seedream": "workspace:*"
|
|
23
|
+
}
|
|
24
|
+
}
|