@hypit/hypit 0.2.0 → 0.2.2
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 +23 -1
- package/examples/provider-package/hypit.runtime.json +2 -2
- package/package.json +2 -1
- package/packages/caption-fine-studio/package.json +1 -1
- package/packages/cli/src/commands/environment.ts +12 -5
- package/packages/cli/src/output.ts +1 -1
- package/packages/credential-store-file/README.md +3 -1
- package/packages/credential-store-file/src/activation.ts +4 -3
- package/packages/credential-store-file/src/index.ts +1 -0
- package/packages/credential-store-file/src/paths.ts +16 -0
- package/packages/credential-store-file/src/store.ts +39 -11
- package/packages/credential-store-os/README.md +4 -0
- package/packages/credential-store-os/src/store.ts +10 -1
- package/packages/credential-store-platform/README.md +62 -0
- package/packages/credential-store-platform/package.json +23 -0
- package/packages/credential-store-platform/src/activation.ts +37 -0
- package/packages/credential-store-platform/src/index.ts +1 -0
- package/packages/credential-store-platform/src/store.ts +89 -0
- package/packages/media-execution/src/process-env.ts +3 -2
- package/packages/performance/package.json +2 -1
- package/packages/provider-hyperframes-local/src/process.ts +6 -2
- package/packages/runtime-local/src/programs.ts +8 -5
- package/packages/video-cli/README.md +6 -4
- package/packages/video-cli/package.json +1 -0
- package/packages/video-cli/src/distribution.ts +5 -2
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ Drop in a video and your agent clones the whole workflow — or describe what yo
|
|
|
110
110
|
|
|
111
111
|
## Stay Tuned with Us
|
|
112
112
|
|
|
113
|
-
Star
|
|
113
|
+
Star us, and you will receive all release notifications from GitHub without any delay!
|
|
114
114
|
|
|
115
115
|

|
|
116
116
|
|
|
@@ -147,6 +147,28 @@ commands and the repository layout.
|
|
|
147
147
|
## Launch Partner
|
|
148
148
|
|
|
149
149
|
<table>
|
|
150
|
+
<tr>
|
|
151
|
+
<td width="220" align="center">
|
|
152
|
+
<a href="https://watcha.cn">
|
|
153
|
+
<picture>
|
|
154
|
+
<source media="(prefers-color-scheme: dark)" srcset="./docs/public/watcha-dark.png">
|
|
155
|
+
<img alt="Watcha" src="./docs/public/watcha-light.png" width="170">
|
|
156
|
+
</picture>
|
|
157
|
+
</a>
|
|
158
|
+
</td>
|
|
159
|
+
<td>Watcha is an innovative ecosystem and community built for AI builders, connecting great products with high-quality users to turn every Demo into reality. Starting from AI product discovery and genuine feedback, Watcha provides product publishing, distribution, and reputation tracking to support builders from launch to early adoption.</td>
|
|
160
|
+
</tr>
|
|
161
|
+
<tr>
|
|
162
|
+
<td width="220" align="center">
|
|
163
|
+
<a href="https://tokendance.space/?utm_source=hypit">
|
|
164
|
+
<picture>
|
|
165
|
+
<source media="(prefers-color-scheme: dark)" srcset="./docs/public/tokendance-dark.png">
|
|
166
|
+
<img alt="TokenDance" src="./docs/public/tokendance-light.png" width="165">
|
|
167
|
+
</picture>
|
|
168
|
+
</a>
|
|
169
|
+
</td>
|
|
170
|
+
<td>TokenDance offers builders a unified multi-model gateway with optimized inference costs. Through programs like the Billion-Token Subsidy and the Developer Program, TokenDance helps early-stage builders cut costs so they can focus on product validation and growth.</td>
|
|
171
|
+
</tr>
|
|
150
172
|
<tr>
|
|
151
173
|
<td width="220" align="center">
|
|
152
174
|
<a href="https://openagents.org/blog/posts/2026-09-08-pilot-user-program-zh-hypit"><img alt="OpenAgents" src="./docs/public/openagents.png" width="180"></a>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"format": "hypit.runtime-local@1",
|
|
3
3
|
"dataRoot": ".hypit/execution",
|
|
4
|
-
"credentials": { "
|
|
4
|
+
"credentials": { "platform": { "use": "@hypit/credential-store-platform" } },
|
|
5
5
|
"endpoints": {
|
|
6
6
|
"images.personal": {
|
|
7
7
|
"use": "@example/provider-images",
|
|
8
|
-
"config": { "baseUrl": "https://images.example", "apiKey": { "store": "
|
|
8
|
+
"config": { "baseUrl": "https://images.example", "apiKey": { "store": "platform", "key": "images.personal" } }
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
11
|
"bindings": { "@hypit/gpt-image@1#gpt-image-2": "images.personal" }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypit/hypit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"homepage": "https://hypit.ai",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -205,6 +205,7 @@
|
|
|
205
205
|
"@hypit/credential-store-env": "workspace:*",
|
|
206
206
|
"@hypit/credential-store-file": "workspace:*",
|
|
207
207
|
"@hypit/credential-store-os": "workspace:*",
|
|
208
|
+
"@hypit/credential-store-platform": "workspace:*",
|
|
208
209
|
"@hypit/deck-track": "workspace:*",
|
|
209
210
|
"@hypit/deck-track-studio": "workspace:*",
|
|
210
211
|
"@hypit/elevenlabs-speech": "workspace:*",
|
|
@@ -172,7 +172,9 @@ export async function runEnvironmentCommand(input: {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
if (args.command === "programs") {
|
|
175
|
-
if (runtimeProfile === undefined)
|
|
175
|
+
if (runtimeProfile === undefined) {
|
|
176
|
+
throw new Error("programs requires a Runtime; run hypit runtime init, select one with runtime use, or pass --runtime <profile>");
|
|
177
|
+
}
|
|
176
178
|
const profile = resolve(runtimeProfile);
|
|
177
179
|
const host = await runtimeHost(profile);
|
|
178
180
|
if (args.action === "up" || args.action === "prepare") {
|
|
@@ -195,11 +197,13 @@ export async function runEnvironmentCommand(input: {
|
|
|
195
197
|
: await controller.programs.report(args.endpoints === undefined ? {} : { endpoints: args.endpoints });
|
|
196
198
|
const ready = result.programs.every((item) => item.state.state === "ready");
|
|
197
199
|
const needsAttention = (item: typeof result.programs[number]) => args.action === "down"
|
|
198
|
-
? item.
|
|
200
|
+
? item.action !== "nothing-to-stop"
|
|
201
|
+
&& (item.state.state !== "down" || item.action !== "stopped")
|
|
199
202
|
: item.state.state !== "ready";
|
|
200
203
|
// Readiness describes the service, not whether a stop was performed. An owned process can
|
|
201
204
|
// still be loading, and another command may have declined a concurrent stop during preparation.
|
|
202
205
|
const lifecycleOk = args.action === "status" || !result.programs.some(needsAttention);
|
|
206
|
+
const stoppedAny = result.programs.some((item) => item.action === "stopped");
|
|
203
207
|
const relevant = result.programs.filter((item) => args.presentation.verbose || args.action === "status" || needsAttention(item));
|
|
204
208
|
const urgent = relevant.filter(needsAttention);
|
|
205
209
|
const shownPrograms = [...urgent, ...relevant.filter((item) => !needsAttention(item)).slice(0, Math.max(0, args.limit - urgent.length))];
|
|
@@ -209,7 +213,8 @@ export async function runEnvironmentCommand(input: {
|
|
|
209
213
|
: args.action === "up"
|
|
210
214
|
? lifecycleOk ? "External programs ready" : "External programs need attention"
|
|
211
215
|
: args.action === "down"
|
|
212
|
-
? lifecycleOk ? "External programs stopped" : "
|
|
216
|
+
? lifecycleOk ? stoppedAny ? "External programs stopped" : "No external programs to stop"
|
|
217
|
+
: "External program stop needs attention"
|
|
213
218
|
: "External program status";
|
|
214
219
|
write({
|
|
215
220
|
format: "hypit.cli-programs@1",
|
|
@@ -232,7 +237,9 @@ export async function runEnvironmentCommand(input: {
|
|
|
232
237
|
}
|
|
233
238
|
|
|
234
239
|
if (args.command === "runtime") {
|
|
235
|
-
if (runtimeProfile === undefined)
|
|
240
|
+
if (runtimeProfile === undefined) {
|
|
241
|
+
throw new Error("runtime requires a Runtime; run hypit runtime init, select one with runtime use, or pass --runtime <profile>");
|
|
242
|
+
}
|
|
236
243
|
const profile = resolve(runtimeProfile);
|
|
237
244
|
const controller = await runtimeController(profile);
|
|
238
245
|
if (args.action === "up") {
|
|
@@ -298,7 +305,7 @@ export async function runEnvironmentCommand(input: {
|
|
|
298
305
|
write({ format: "hypit.cli-runtime-down@1", worker: worker.state },
|
|
299
306
|
stopped ? "Runtime Worker is down" : "Runtime Worker is still running",
|
|
300
307
|
stopped ? "success" : "warning", [["Worker", worker.state]],
|
|
301
|
-
[`Managed
|
|
308
|
+
[`Managed Programs are unchanged. To stop processes started by Hypit: ${commandHint(["programs", "down"], { projectRoot, runtimeProfile: resolve(profile) })}`]);
|
|
302
309
|
if (!stopped) io.setExitCode?.(1);
|
|
303
310
|
return;
|
|
304
311
|
}
|
|
@@ -1015,7 +1015,7 @@ export function writeCliHelp(io: CliIo, topic?: string): void {
|
|
|
1015
1015
|
row("doctor [profile]", "diagnose selected external setup"),
|
|
1016
1016
|
row("runtime init|use|unset", "create or select this project's Runtime Profile"),
|
|
1017
1017
|
row("runtime up|status|logs|down", "prepare and manage the local Build Runtime"),
|
|
1018
|
-
row("programs up|status|down", "manage declared external programs only"),
|
|
1018
|
+
row("programs prepare|up|status|down", "manage declared external programs only"),
|
|
1019
1019
|
row("packages install|status", "manage pinned upstream packages in the machine home"),
|
|
1020
1020
|
row("activity [--watch]", "show active Builds and their current phases"),
|
|
1021
1021
|
row("cancel <build-id>", "withdraw one active Build"),
|
|
@@ -4,7 +4,9 @@ A writable CredentialStore for a deployment that explicitly chooses local file s
|
|
|
4
4
|
Linux without an OS credential locker. It stores unencrypted credential values in an owner-private
|
|
5
5
|
directory outside the video project. On Windows the selected directory must have a private user ACL;
|
|
6
6
|
POSIX file modes do not set Windows ACLs. Choose `@hypit/credential-store-os` when using macOS Keychain
|
|
7
|
-
or Windows Credential Locker,
|
|
7
|
+
or Windows Credential Locker, `@hypit/credential-store-platform` for OS storage on macOS/Windows
|
|
8
|
+
and this file Store on Linux or `@hypit/credential-store-env` for externally
|
|
9
|
+
supplied, read-only values.
|
|
8
10
|
|
|
9
11
|
## Select it before login
|
|
10
12
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { join, resolve } from "node:path";
|
|
2
1
|
import {
|
|
3
2
|
createRuntimeCredentialStoreAdapterFacet,
|
|
4
3
|
runtimeConfigExact,
|
|
5
4
|
runtimeConfigObject,
|
|
6
5
|
runtimeConfigString,
|
|
7
6
|
} from "@hypit/runtime-kit";
|
|
7
|
+
import { resolveCredentialDirectory } from "./paths.js";
|
|
8
8
|
import { FileCredentialStore } from "./store.js";
|
|
9
9
|
|
|
10
10
|
const fileCredentialStoreAdapter = createRuntimeCredentialStoreAdapterFacet({
|
|
@@ -17,8 +17,9 @@ const fileCredentialStoreAdapter = createRuntimeCredentialStoreAdapterFacet({
|
|
|
17
17
|
open(context) {
|
|
18
18
|
const config = runtimeConfigObject(context.config, "file CredentialStore");
|
|
19
19
|
const path = runtimeConfigString(config.path, "file credential path");
|
|
20
|
-
return { value: new FileCredentialStore(
|
|
21
|
-
|
|
20
|
+
return { value: new FileCredentialStore(
|
|
21
|
+
resolveCredentialDirectory(context.hostStateRoot, path, "file CredentialStore"),
|
|
22
|
+
) };
|
|
22
23
|
},
|
|
23
24
|
});
|
|
24
25
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
2
|
+
|
|
3
|
+
/** Profile `path` is Host-state-relative; it must not write secrets outside that root. */
|
|
4
|
+
export function resolveCredentialDirectory(
|
|
5
|
+
hostStateRoot: string,
|
|
6
|
+
path: string | undefined,
|
|
7
|
+
label: string,
|
|
8
|
+
): string {
|
|
9
|
+
const root = resolve(hostStateRoot);
|
|
10
|
+
const directory = path === undefined ? join(root, "credentials") : resolve(root, path);
|
|
11
|
+
const relation = relative(root, directory);
|
|
12
|
+
if (relation === ".." || relation.startsWith(`..${sep}`) || isAbsolute(relation)) {
|
|
13
|
+
throw new Error(`${label} path must stay inside the Host state root`);
|
|
14
|
+
}
|
|
15
|
+
return directory;
|
|
16
|
+
}
|
|
@@ -8,6 +8,26 @@ function missing(error: unknown): boolean {
|
|
|
8
8
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
9
9
|
}
|
|
10
10
|
|
|
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
|
+
|
|
11
31
|
function credentialValue(value: unknown): CredentialValue {
|
|
12
32
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid credential value");
|
|
13
33
|
const record = value as Record<string, unknown>;
|
|
@@ -44,6 +64,10 @@ export class FileCredentialStore implements WritableCredentialStore {
|
|
|
44
64
|
async resolve(ref: CredentialRef): Promise<CredentialValue | undefined> {
|
|
45
65
|
if (!this.owns(ref)) return undefined;
|
|
46
66
|
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> {
|
|
47
71
|
try { await this.#privateDirectory(false); } catch (error) {
|
|
48
72
|
if (missing(error)) return undefined;
|
|
49
73
|
throw error;
|
|
@@ -69,21 +93,25 @@ export class FileCredentialStore implements WritableCredentialStore {
|
|
|
69
93
|
async put(ref: CredentialRef, value: CredentialValue): Promise<void> {
|
|
70
94
|
const path = this.#path(ref);
|
|
71
95
|
const contents = JSON.stringify(credentialValue(value));
|
|
72
|
-
await
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
await onDocument(path, async () => {
|
|
97
|
+
await this.#privateDirectory(true);
|
|
98
|
+
const temporary = join(this.directory, `.write-${randomUUID()}.tmp`);
|
|
99
|
+
const file = await open(temporary, "wx", 0o600);
|
|
100
|
+
try {
|
|
101
|
+
try { await file.writeFile(`${contents}\n`, "utf8"); } finally { await file.close(); }
|
|
102
|
+
await rename(temporary, path);
|
|
103
|
+
} finally { await rm(temporary, { force: true }); }
|
|
104
|
+
});
|
|
79
105
|
}
|
|
80
106
|
|
|
81
107
|
async delete(ref: CredentialRef): Promise<boolean> {
|
|
82
108
|
const path = this.#path(ref);
|
|
83
109
|
// Deletion never opens or decodes the old document.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
110
|
+
return await onDocument(path, async () => {
|
|
111
|
+
try { await unlink(path); return true; } catch (error) {
|
|
112
|
+
if (missing(error)) return false;
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
88
116
|
}
|
|
89
117
|
}
|
|
@@ -11,3 +11,7 @@ bytes do not appear in command arguments or temporary files.
|
|
|
11
11
|
|
|
12
12
|
Use `hypit auth login <endpoint> --runtime <profile>` to write the selected credential and
|
|
13
13
|
`hypit auth logout` to remove it.
|
|
14
|
+
|
|
15
|
+
A Profile that must also run on Linux can select `@hypit/credential-store-platform`:
|
|
16
|
+
that package uses this Store on macOS and Windows, and an unencrypted, owner-private file on Linux.
|
|
17
|
+
Selecting `@hypit/credential-store-os` directly continues to require macOS or Windows.
|
|
@@ -67,6 +67,8 @@ type WindowsCredentialResult = {
|
|
|
67
67
|
|
|
68
68
|
const windowsScript = fileURLToPath(new URL("../runtime/windows-credential.ps1", import.meta.url));
|
|
69
69
|
|
|
70
|
+
const windowsCredentialTimeoutMs = 10_000;
|
|
71
|
+
|
|
70
72
|
function windowsCredential(
|
|
71
73
|
operation: "read" | "write" | "delete",
|
|
72
74
|
service: string,
|
|
@@ -84,8 +86,13 @@ function windowsCredential(
|
|
|
84
86
|
const fail = (error: Error): void => {
|
|
85
87
|
if (settled) return;
|
|
86
88
|
settled = true;
|
|
89
|
+
clearTimeout(timer);
|
|
87
90
|
reject(error);
|
|
88
91
|
};
|
|
92
|
+
const timer = setTimeout(() => {
|
|
93
|
+
fail(new Error(`Windows credential ${operation} for ${account} timed out`));
|
|
94
|
+
child.kill("SIGKILL");
|
|
95
|
+
}, windowsCredentialTimeoutMs);
|
|
89
96
|
child.on("error", fail);
|
|
90
97
|
child.stdout.on("data", (chunk: Buffer) => {
|
|
91
98
|
stdout += chunk.toString("utf8");
|
|
@@ -97,6 +104,7 @@ function windowsCredential(
|
|
|
97
104
|
});
|
|
98
105
|
child.on("close", (code) => {
|
|
99
106
|
if (settled) return;
|
|
107
|
+
clearTimeout(timer);
|
|
100
108
|
if (code !== 0) {
|
|
101
109
|
fail(new Error(`Windows credential ${operation} for ${account} failed${stderr.trim().length === 0 ? "" : `: ${stderr.trim()}`}`));
|
|
102
110
|
return;
|
|
@@ -142,8 +150,9 @@ function platformBackend(service: string) {
|
|
|
142
150
|
}
|
|
143
151
|
if (process.platform === "win32") return windowsBackend();
|
|
144
152
|
// A Profile that selects this Store cannot be repaired by anything the user does here, and the
|
|
145
|
-
// other
|
|
153
|
+
// other Stores are the answer, so name them where the failure is read.
|
|
146
154
|
throw new Error("OS CredentialStore supports macOS and Windows only; select "
|
|
155
|
+
+ "@hypit/credential-store-platform (platform locker, owner-private file on Linux), "
|
|
147
156
|
+ "@hypit/credential-store-file (owner-private local file) or @hypit/credential-store-env "
|
|
148
157
|
+ "(externally supplied value) in this Profile's credentials instead");
|
|
149
158
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# `@hypit/credential-store-platform`
|
|
2
|
+
|
|
3
|
+
An explicitly selected CredentialStore policy: macOS Keychain on macOS, Windows Credential Locker
|
|
4
|
+
on Windows, and an unencrypted, owner-private file on Linux. Other platforms are unsupported by this
|
|
5
|
+
package and report an error. Linux may offer other credential services; this package chooses file
|
|
6
|
+
storage there and does not detect or integrate those services.
|
|
7
|
+
|
|
8
|
+
A Profile can select this package to use that policy on Linux, macOS and Windows.
|
|
9
|
+
`@hypit/credential-store-os`, `@hypit/credential-store-file` and `@hypit/credential-store-env`
|
|
10
|
+
remain selectable directly for a deployment with a different storage choice.
|
|
11
|
+
|
|
12
|
+
## Select it
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"credentials": {
|
|
17
|
+
"platform": { "use": "@hypit/credential-store-platform" }
|
|
18
|
+
},
|
|
19
|
+
"endpoints": {
|
|
20
|
+
"hypihub.default": {
|
|
21
|
+
"use": "@hypit/provider-hypihub",
|
|
22
|
+
"config": {
|
|
23
|
+
"baseUrl": "https://hypit.ai",
|
|
24
|
+
"apiKey": { "store": "platform", "key": "hypihub.oauth" }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The Endpoint still owns its acquisition flow and the Store only persists the result:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
hypit auth login hypihub.default --runtime <profile>
|
|
35
|
+
hypit auth status hypihub.default --runtime <profile>
|
|
36
|
+
hypit auth logout hypihub.default --runtime <profile>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## What it does not do
|
|
40
|
+
|
|
41
|
+
The choice is made once per host, from the platform the Runtime is running on. This is not a lookup
|
|
42
|
+
chain: a miss or failure in one store never tries another, and no credential is migrated between them. A
|
|
43
|
+
credential stored in a locker stays in that locker; a credential stored in a file stays in that file.
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
|
|
47
|
+
- `path` selects the directory for Linux file storage, resolved against the Host state root printed by
|
|
48
|
+
`hypit paths`. The default is the same `credentials` directory `@hypit/credential-store-file` uses,
|
|
49
|
+
so on Linux, switching between the two Stores finds the credential already stored.
|
|
50
|
+
- `service` selects the locker service name. The OS Store's own default applies when it is absent.
|
|
51
|
+
|
|
52
|
+
## Storage on each platform
|
|
53
|
+
|
|
54
|
+
On macOS and Windows the credential is held by the platform locker, with exactly the rules of
|
|
55
|
+
`@hypit/credential-store-os`: it is encrypted and access-controlled by the operating system, and it
|
|
56
|
+
is not readable from another machine.
|
|
57
|
+
|
|
58
|
+
On Linux the credential is held by an owner-private document with exactly the rules of
|
|
59
|
+
`@hypit/credential-store-file`: unencrypted JSON, one document per key, directory mode `0700`, file
|
|
60
|
+
mode `0600`, outside the video project, and never enumerated or indexed. Filesystem path-length
|
|
61
|
+
limits apply to an overlong key, which fails rather than selecting another name. A damaged document
|
|
62
|
+
can be replaced by `auth login` or deleted by `auth logout` without decoding the old value.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hypit/credential-store-platform",
|
|
3
|
+
"version": "0.0.0-dev",
|
|
4
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"hypit": {
|
|
15
|
+
"activation": "./src/activation.ts"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@hypit/credential-store-file": "workspace:*",
|
|
19
|
+
"@hypit/credential-store-os": "workspace:*",
|
|
20
|
+
"@hypit/runtime": "workspace:*",
|
|
21
|
+
"@hypit/runtime-kit": "workspace:*"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRuntimeCredentialStoreAdapterFacet,
|
|
3
|
+
runtimeConfigExact,
|
|
4
|
+
runtimeConfigObject,
|
|
5
|
+
runtimeConfigString,
|
|
6
|
+
} from "@hypit/runtime-kit";
|
|
7
|
+
import { resolveCredentialDirectory } from "@hypit/credential-store-file";
|
|
8
|
+
import { PlatformCredentialStore } from "./store.js";
|
|
9
|
+
|
|
10
|
+
const platformCredentialStoreAdapter = createRuntimeCredentialStoreAdapterFacet({
|
|
11
|
+
use: "@hypit/credential-store-platform",
|
|
12
|
+
validate(context) {
|
|
13
|
+
const config = runtimeConfigObject(context.config, "platform CredentialStore");
|
|
14
|
+
runtimeConfigExact(config, ["path", "service"], "platform CredentialStore");
|
|
15
|
+
runtimeConfigString(config.path, "platform credential path");
|
|
16
|
+
runtimeConfigString(config.service, "platform credential service");
|
|
17
|
+
},
|
|
18
|
+
open(context) {
|
|
19
|
+
const config = runtimeConfigObject(context.config, "platform CredentialStore");
|
|
20
|
+
const path = runtimeConfigString(config.path, "platform credential path");
|
|
21
|
+
const service = runtimeConfigString(config.service, "platform credential service");
|
|
22
|
+
return {
|
|
23
|
+
value: new PlatformCredentialStore({
|
|
24
|
+
// Linux uses the same directory the file Store uses by default, so a Profile
|
|
25
|
+
// that switches between them on Linux finds the credential it stored.
|
|
26
|
+
directory: resolveCredentialDirectory(context.hostStateRoot, path, "platform CredentialStore"),
|
|
27
|
+
...(service === undefined ? {} : { service }),
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const hypitPackage = {
|
|
34
|
+
format: "hypit.node-package@1" as const,
|
|
35
|
+
hostFacets: [platformCredentialStoreAdapter],
|
|
36
|
+
};
|
|
37
|
+
export default hypitPackage;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PlatformCredentialStore } from "./store.js";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
credentialRef,
|
|
3
|
+
verifyCredentialRef,
|
|
4
|
+
} from "@hypit/runtime";
|
|
5
|
+
import type {
|
|
6
|
+
CredentialRef,
|
|
7
|
+
CredentialValue,
|
|
8
|
+
WritableCredentialStore,
|
|
9
|
+
} from "@hypit/runtime";
|
|
10
|
+
import { FileCredentialStore } from "@hypit/credential-store-file";
|
|
11
|
+
import { OsCredentialStore } from "@hypit/credential-store-os";
|
|
12
|
+
import type {
|
|
13
|
+
OsCredentialDeleter,
|
|
14
|
+
OsCredentialReader,
|
|
15
|
+
OsCredentialWriter,
|
|
16
|
+
} from "@hypit/credential-store-os";
|
|
17
|
+
|
|
18
|
+
export type PlatformCredentialStoreOptions = {
|
|
19
|
+
/** Directory holding the documents this Store selects on Linux. */
|
|
20
|
+
readonly directory: string;
|
|
21
|
+
/** Service the locker's entries are filed under; the OS Store's own default applies where absent. */
|
|
22
|
+
readonly service?: string;
|
|
23
|
+
/**
|
|
24
|
+
* The platform to select for, defaulting to the running one. A Profile never states this: it is
|
|
25
|
+
* separate so the selection can be exercised on one machine for every platform.
|
|
26
|
+
*/
|
|
27
|
+
readonly platform?: NodeJS.Platform;
|
|
28
|
+
/** Locker backend to read and write with, where the platform's own locker is not the one to use. */
|
|
29
|
+
readonly locker?: {
|
|
30
|
+
readonly read: OsCredentialReader;
|
|
31
|
+
readonly write: OsCredentialWriter;
|
|
32
|
+
readonly remove: OsCredentialDeleter;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* An explicitly selected storage policy: OS credentials on macOS/Windows, files on Linux.
|
|
38
|
+
* The selected backend's errors propagate; other platforms are unsupported by this package.
|
|
39
|
+
*/
|
|
40
|
+
export class PlatformCredentialStore implements WritableCredentialStore {
|
|
41
|
+
readonly #delegate: WritableCredentialStore;
|
|
42
|
+
readonly #backing: "os" | "file";
|
|
43
|
+
|
|
44
|
+
constructor(options: PlatformCredentialStoreOptions) {
|
|
45
|
+
const platform = options.platform ?? process.platform;
|
|
46
|
+
if (platform === "darwin" || platform === "win32") {
|
|
47
|
+
this.#backing = "os";
|
|
48
|
+
this.#delegate = new OsCredentialStore({
|
|
49
|
+
...(options.service === undefined ? {} : { service: options.service }),
|
|
50
|
+
...options.locker,
|
|
51
|
+
});
|
|
52
|
+
} else if (platform === "linux") {
|
|
53
|
+
this.#backing = "file";
|
|
54
|
+
this.#delegate = new FileCredentialStore(options.directory);
|
|
55
|
+
} else {
|
|
56
|
+
throw new Error(`Platform CredentialStore does not support ${platform}; select an explicit CredentialStore supported by this host`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
owns(ref: CredentialRef): boolean {
|
|
61
|
+
return ref.store === "platform";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Each delegate owns its own name and refuses refs filed under another, so a ref this Store owns
|
|
66
|
+
* is retargeted before the delegate sees it. Only the name changes; the opaque key is untouched.
|
|
67
|
+
*/
|
|
68
|
+
#translated(ref: CredentialRef): CredentialRef {
|
|
69
|
+
return credentialRef(this.#backing, ref.key);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async resolve(ref: CredentialRef): Promise<CredentialValue | undefined> {
|
|
73
|
+
verifyCredentialRef(ref);
|
|
74
|
+
if (!this.owns(ref)) return undefined;
|
|
75
|
+
return await this.#delegate.resolve(this.#translated(ref));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async put(ref: CredentialRef, value: CredentialValue): Promise<void> {
|
|
79
|
+
verifyCredentialRef(ref);
|
|
80
|
+
if (!this.owns(ref)) throw new Error(`Platform CredentialStore does not own ${ref.store}`);
|
|
81
|
+
await this.#delegate.put(this.#translated(ref), value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async delete(ref: CredentialRef): Promise<boolean> {
|
|
85
|
+
verifyCredentialRef(ref);
|
|
86
|
+
if (!this.owns(ref)) throw new Error(`Platform CredentialStore does not own ${ref.store}`);
|
|
87
|
+
return await this.#delegate.delete(this.#translated(ref));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const windowsProcessCreation = ["PATHEXT", "SYSTEMROOT", "WINDIR", "ComSpec"] as const;
|
|
1
|
+
const windowsProcessCreation = ["PATHEXT", "SYSTEMROOT", "WINDIR", "ComSpec", "TEMP", "TMP"] as const;
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* ffmpeg and ffprobe are started as PATH executables. The Host environment is
|
|
@@ -7,9 +7,10 @@ const windowsProcessCreation = ["PATHEXT", "SYSTEMROOT", "WINDIR", "ComSpec"] as
|
|
|
7
7
|
*/
|
|
8
8
|
export function mediaProcessEnv(
|
|
9
9
|
extra?: Readonly<Record<string, string>>,
|
|
10
|
+
platform: NodeJS.Platform = process.platform,
|
|
10
11
|
): NodeJS.ProcessEnv {
|
|
11
12
|
const env: NodeJS.ProcessEnv = { PATH: process.env.PATH ?? "" };
|
|
12
|
-
if (
|
|
13
|
+
if (platform === "win32") {
|
|
13
14
|
for (const name of windowsProcessCreation) {
|
|
14
15
|
const value = process.env[name];
|
|
15
16
|
if (value !== undefined && value.length > 0) env[name] = value;
|
|
@@ -35,8 +35,12 @@ export async function mediaExecutablePath(value: string): Promise<string> {
|
|
|
35
35
|
throw new Error(`HyperFrames media executable ${value} is unavailable; correct the Provider's ffmpegPath or ffprobePath.`);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function processEnvironment(): NodeJS.ProcessEnv {
|
|
39
|
-
|
|
38
|
+
export function processEnvironment(platform: NodeJS.Platform = process.platform): NodeJS.ProcessEnv {
|
|
39
|
+
// POSIX temp is TMPDIR; Windows is TEMP/TMP. HyperFrames writes extracted
|
|
40
|
+
// frames under %TEMP%\hf-render-… and ffmpeg creates temporary files the same way.
|
|
41
|
+
const names = platform === "win32"
|
|
42
|
+
? ["PATH", "PATHEXT", "SYSTEMROOT", "WINDIR", "ComSpec", "TEMP", "TMP", "USERPROFILE", "LANG", "LC_ALL"] as const
|
|
43
|
+
: ["PATH", "HOME", "TMPDIR", "LANG", "LC_ALL"] as const;
|
|
40
44
|
return Object.fromEntries(names.flatMap((name) => process.env[name] === undefined
|
|
41
45
|
? []
|
|
42
46
|
: [[name, process.env[name]]])) as NodeJS.ProcessEnv;
|
|
@@ -504,11 +504,14 @@ export async function takeManagedProgramsDown(
|
|
|
504
504
|
if (pid === undefined || !processAlive(pid)) {
|
|
505
505
|
if (pid !== undefined) await rm(join(directory(dataRoot, program), "process.pid"), { force: true });
|
|
506
506
|
const state = await program.probe();
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
|
|
507
|
+
if (state.state === "down" || program.start === undefined) {
|
|
508
|
+
// Probe-only Programs describe installed tools or resources. Readiness means they are
|
|
509
|
+
// usable, not that a process exists for Hypit to stop.
|
|
510
|
+
return { ...base, action: "nothing-to-stop", state };
|
|
511
|
+
}
|
|
512
|
+
// Someone else's process, or one started by hand. Killing it is not this
|
|
513
|
+
// command's business; saying so is.
|
|
514
|
+
return { ...base, action: "not-ours", state, detail: `${program.id} is running without a Hypit process record` };
|
|
512
515
|
}
|
|
513
516
|
const term = await stopProcessTree(pid);
|
|
514
517
|
if (term === "denied") {
|
|
@@ -35,10 +35,12 @@ Keep an existing chosen service, or configure the chosen local or hosted Provide
|
|
|
35
35
|
bindings. HypiHub is the recommended integrated hosted route in the official Distribution; other
|
|
36
36
|
services use project Provider packages. If the user chooses HypiHub,
|
|
37
37
|
`hypit auth login hypihub.default` connects that account after choosing its CredentialStore.
|
|
38
|
-
The starter selects the
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
The starter selects the [platform CredentialStore](../credential-store-platform/README.md#select-it):
|
|
39
|
+
macOS Keychain or Windows Credential Locker on those two platforms, and an owner-private file on
|
|
40
|
+
Linux, so the Profile it writes needs no edit on any of them. Name another Store in `credentials` and
|
|
41
|
+
in the Endpoint's credential reference — as the
|
|
42
|
+
[file CredentialStore](../credential-store-file/README.md#select-it-before-login) shows — to choose it
|
|
43
|
+
explicitly. This selection is configuration; execution never switches stores automatically.
|
|
42
44
|
`hypit doctor --endpoint <name>` checks a selected Endpoint;
|
|
43
45
|
`hypit runtime up --endpoint <name>` prepares that Endpoint and starts the Worker. Repeat the flag
|
|
44
46
|
for several chosen Endpoints; omitting it prepares the whole Profile. `hypit programs up --endpoint
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"@hypit/protocol": "workspace:*",
|
|
26
26
|
"@hypit/provider-hypihub": "workspace:*",
|
|
27
27
|
"@hypit/runtime": "workspace:*",
|
|
28
|
+
"@hypit/runtime-kit": "workspace:*",
|
|
28
29
|
"@hypit/runtime-host-node": "workspace:*",
|
|
29
30
|
"@hypit/runtime-local": "workspace:*",
|
|
30
31
|
"@hypit/speech": "workspace:*",
|
|
@@ -25,15 +25,18 @@ export const videoCliDistribution: CliDistribution = {
|
|
|
25
25
|
initialRuntimeProfile: {
|
|
26
26
|
format: "hypit.runtime-local@1",
|
|
27
27
|
dataRoot: ".hypit/runtimes/local",
|
|
28
|
+
// The starter selects the portable Store, so the Profile it writes is openable and writable on
|
|
29
|
+
// Linux as well: macOS and Windows keep the platform locker, and a Linux host uses an
|
|
30
|
+
// owner-private file. `os` and `file` stay selectable by name for one explicit backend.
|
|
28
31
|
credentials: {
|
|
29
|
-
|
|
32
|
+
platform: { use: "@hypit/credential-store-platform" },
|
|
30
33
|
},
|
|
31
34
|
endpoints: {
|
|
32
35
|
"hypihub.default": {
|
|
33
36
|
use: "@hypit/provider-hypihub",
|
|
34
37
|
config: {
|
|
35
38
|
baseUrl: "https://hypit.ai",
|
|
36
|
-
apiKey: { store: "
|
|
39
|
+
apiKey: { store: "platform", key: "hypihub.oauth" },
|
|
37
40
|
},
|
|
38
41
|
},
|
|
39
42
|
"media.local": {
|