@rynx-ai/daemon 0.1.0 → 0.1.9
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/dist/browser-cli-args.d.ts +20 -0
- package/dist/browser-cli-args.js +100 -0
- package/dist/browser-runner-session-context.d.ts +24 -0
- package/dist/browser-runner-session-context.js +113 -0
- package/dist/cdp-keyboard-input.d.ts +6 -0
- package/dist/cdp-keyboard-input.js +209 -0
- package/dist/channel-store.d.ts +0 -6
- package/dist/channel-store.js +2 -18
- package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
- package/dist/chrome-for-testing-release-resolver.js +214 -0
- package/dist/chrome-for-testing-store.d.ts +85 -0
- package/dist/chrome-for-testing-store.js +836 -0
- package/dist/cli.js +467 -30
- package/dist/control-client.d.ts +113 -0
- package/dist/control-client.js +792 -0
- package/dist/control-deps.js +37 -6
- package/dist/control-endpoint.d.ts +14 -0
- package/dist/control-endpoint.js +124 -0
- package/dist/control-link-store.d.ts +16 -0
- package/dist/control-link-store.js +53 -0
- package/dist/daemon-owner.d.ts +14 -0
- package/dist/daemon-owner.js +104 -0
- package/dist/daemon-server.d.ts +12 -2
- package/dist/daemon-server.js +559 -28
- package/dist/db.js +140 -10
- package/dist/desktop-browser-host-client.d.ts +34 -0
- package/dist/desktop-browser-host-client.js +413 -0
- package/dist/direct-runtime-authenticator.d.ts +13 -0
- package/dist/direct-runtime-authenticator.js +127 -0
- package/dist/direct-runtime-config.d.ts +20 -0
- package/dist/direct-runtime-config.js +101 -0
- package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
- package/dist/embedded-browser-profile-cleanup.js +338 -0
- package/dist/headless-browser-host.d.ts +14 -0
- package/dist/headless-browser-host.js +2156 -0
- package/dist/index-daemon.js +28 -5
- package/dist/installation-id.d.ts +33 -0
- package/dist/installation-id.js +692 -0
- package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
- package/dist/migrations/cleanup-legacy-sessions.js +10 -28
- package/dist/plugin-cli.d.ts +30 -0
- package/dist/plugin-cli.js +270 -0
- package/dist/plugin-host-rpc.d.ts +85 -0
- package/dist/plugin-host-rpc.js +878 -0
- package/dist/plugin-install-restart.d.ts +13 -0
- package/dist/plugin-install-restart.js +24 -0
- package/dist/plugin-installer.d.ts +36 -15
- package/dist/plugin-installer.js +420 -155
- package/dist/plugin-package.d.ts +38 -0
- package/dist/plugin-package.js +553 -0
- package/dist/plugin-runtime-control.d.ts +2 -0
- package/dist/plugin-runtime-control.js +37 -0
- package/dist/plugin-store.d.ts +37 -8
- package/dist/plugin-store.js +80 -15
- package/dist/plugin-supervisor.d.ts +160 -0
- package/dist/plugin-supervisor.js +1143 -0
- package/dist/pm2.d.ts +3 -1
- package/dist/pm2.js +55 -13
- package/dist/provider-cli-service.d.ts +46 -0
- package/dist/provider-cli-service.js +510 -0
- package/dist/registry.d.ts +6 -19
- package/dist/registry.js +22 -87
- package/dist/remote-runtime-access-store.d.ts +61 -0
- package/dist/remote-runtime-access-store.js +386 -0
- package/dist/remote-runtime-admin.d.ts +32 -0
- package/dist/remote-runtime-admin.js +107 -0
- package/dist/remote-runtime-connection-manager.d.ts +92 -0
- package/dist/remote-runtime-connection-manager.js +1409 -0
- package/dist/remote-runtime-credential-store.d.ts +45 -0
- package/dist/remote-runtime-credential-store.js +639 -0
- package/dist/remote-runtime-target-control.d.ts +47 -0
- package/dist/remote-runtime-target-control.js +433 -0
- package/dist/remote-runtime-target-store.d.ts +53 -0
- package/dist/remote-runtime-target-store.js +307 -0
- package/dist/runtime-share-addresses.d.ts +21 -0
- package/dist/runtime-share-addresses.js +50 -0
- package/dist/session-browser-capability-store.d.ts +13 -0
- package/dist/session-browser-capability-store.js +58 -0
- package/dist/session-emulator-binding-store.d.ts +10 -0
- package/dist/session-emulator-binding-store.js +61 -0
- package/dist/session-launch-operations.d.ts +41 -0
- package/dist/session-launch-operations.js +124 -0
- package/dist/session-meta-store.d.ts +3 -2
- package/dist/session-meta-store.js +57 -7
- package/dist/setup.js +9 -4
- package/dist/skills-catalog.js +8 -4
- package/dist/update.d.ts +11 -2
- package/dist/update.js +0 -27
- package/package.json +18 -10
package/dist/registry.d.ts
CHANGED
|
@@ -1,23 +1,10 @@
|
|
|
1
|
-
import { type ChannelInstanceDescriptor, type ChannelTypeDescriptor
|
|
2
|
-
import {
|
|
3
|
-
/** Directory
|
|
1
|
+
import { type ChannelInstanceDescriptor, type ChannelTypeDescriptor } from "@rynx-ai/core";
|
|
2
|
+
import type { PluginSupervisor } from "./plugin-supervisor.js";
|
|
3
|
+
/** Directory plugin packages and staging data live under. */
|
|
4
4
|
export declare function pluginsDir(): string;
|
|
5
|
-
/** All channel types currently available (from loaded plugins). */
|
|
6
5
|
export declare function listChannelTypes(): ChannelTypeDescriptor[];
|
|
7
6
|
export declare function getChannelType(type: string): ChannelTypeDescriptor | undefined;
|
|
8
|
-
/**
|
|
9
|
-
export declare function
|
|
10
|
-
/**
|
|
11
|
-
export declare function importPluginManifest(pkgDir: string): Promise<RynxPlugin | null>;
|
|
12
|
-
/**
|
|
13
|
-
* Rebuild the channel-type catalog from the enabled plugins in the registry.
|
|
14
|
-
* Call at boot and after an install/enable/disable change. Failures to load a
|
|
15
|
-
* single plugin are warned and skipped — one bad plugin can't break the rest.
|
|
16
|
-
*/
|
|
17
|
-
export declare function loadPlugins(warn?: (message: string) => void): Promise<void>;
|
|
18
|
-
/**
|
|
19
|
-
* Resolve every enabled instance (joined with its channel) to a mountable
|
|
20
|
-
* descriptor, binding each to the factory its channel `type` resolves to in the
|
|
21
|
-
* catalog. Unknown types (no plugin providing them) are skipped.
|
|
22
|
-
*/
|
|
7
|
+
/** Rebuild the catalog from static manifests projected by the supervisor. */
|
|
8
|
+
export declare function loadPlugins(supervisor: PluginSupervisor, warn?: (message: string) => void): Promise<void>;
|
|
9
|
+
/** Join enabled channel instances to their remote runner-backed factories. */
|
|
23
10
|
export declare function resolveChannelInstances(warn?: (message: string) => void): Promise<ChannelInstanceDescriptor[]>;
|
package/dist/registry.js
CHANGED
|
@@ -1,117 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Static plugin catalog.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* reads the catalog synchronously. The installer calls `loadPlugins()` after a
|
|
9
|
-
* change so a freshly-installed plugin's types appear without a restart.
|
|
4
|
+
* Installation records already contain validated pure-data manifests. This
|
|
5
|
+
* module joins their chat-channel declarations to host-side remote lifecycle
|
|
6
|
+
* adapters supplied by PluginSupervisor. It never imports an installed plugin
|
|
7
|
+
* entry point.
|
|
10
8
|
*/
|
|
11
|
-
import { readFileSync } from "node:fs";
|
|
12
9
|
import { join } from "node:path";
|
|
13
|
-
import { pathToFileURL } from "node:url";
|
|
14
10
|
import { rynxHome, } from "@rynx-ai/core";
|
|
15
11
|
import { listEnabledDescriptors } from "./instance-store.js";
|
|
16
|
-
|
|
17
|
-
/** Directory npm-installed plugins live under (override via `RYNX_PLUGINS_DIR`). */
|
|
12
|
+
/** Directory plugin packages and staging data live under. */
|
|
18
13
|
export function pluginsDir() {
|
|
19
14
|
return process.env.RYNX_PLUGINS_DIR?.trim() || join(rynxHome(), "plugins");
|
|
20
15
|
}
|
|
21
|
-
/** type → descriptor, rebuilt by {@link loadPlugins}. */
|
|
22
16
|
const catalog = new Map();
|
|
23
|
-
/** All channel types currently available (from loaded plugins). */
|
|
24
17
|
export function listChannelTypes() {
|
|
25
18
|
return [...catalog.values()];
|
|
26
19
|
}
|
|
27
20
|
export function getChannelType(type) {
|
|
28
21
|
return catalog.get(type);
|
|
29
22
|
}
|
|
30
|
-
/**
|
|
31
|
-
function
|
|
32
|
-
const pkg = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf8"));
|
|
33
|
-
let rel = pkg.rynx?.plugin;
|
|
34
|
-
if (!rel) {
|
|
35
|
-
const dot = pkg.exports && typeof pkg.exports === "object" && "." in pkg.exports
|
|
36
|
-
? pkg.exports["."]
|
|
37
|
-
: pkg.exports;
|
|
38
|
-
if (typeof dot === "string") {
|
|
39
|
-
rel = dot;
|
|
40
|
-
}
|
|
41
|
-
else if (dot && typeof dot === "object") {
|
|
42
|
-
const cond = dot;
|
|
43
|
-
rel = cond.import ?? cond.default ?? cond.require;
|
|
44
|
-
}
|
|
45
|
-
rel = rel ?? pkg.main ?? "index.js";
|
|
46
|
-
}
|
|
47
|
-
return pathToFileURL(join(pkgDir, rel)).href;
|
|
48
|
-
}
|
|
49
|
-
/** Absolute package directory a plugin record resolves to. */
|
|
50
|
-
export function pluginPkgDir(rec) {
|
|
51
|
-
// local: spec is an absolute directory; npm: resolve under the plugins prefix.
|
|
52
|
-
return rec.source === "local" ? rec.spec : join(pluginsDir(), "node_modules", ...rec.spec.split("/"));
|
|
53
|
-
}
|
|
54
|
-
/** Import a package directory's `plugin` manifest export (or null if absent). */
|
|
55
|
-
export async function importPluginManifest(pkgDir) {
|
|
56
|
-
const mod = (await import(entryUrl(pkgDir)));
|
|
57
|
-
const plugin = (mod.plugin ?? mod.default);
|
|
58
|
-
return plugin && typeof plugin === "object" ? plugin : null;
|
|
59
|
-
}
|
|
60
|
-
function importPlugin(rec) {
|
|
61
|
-
return importPluginManifest(pluginPkgDir(rec));
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Rebuild the channel-type catalog from the enabled plugins in the registry.
|
|
65
|
-
* Call at boot and after an install/enable/disable change. Failures to load a
|
|
66
|
-
* single plugin are warned and skipped — one bad plugin can't break the rest.
|
|
67
|
-
*/
|
|
68
|
-
export async function loadPlugins(warn = () => { }) {
|
|
23
|
+
/** Rebuild the catalog from static manifests projected by the supervisor. */
|
|
24
|
+
export async function loadPlugins(supervisor, warn = () => { }) {
|
|
69
25
|
catalog.clear();
|
|
70
|
-
for (const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
plugin = await importPlugin(rec);
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
warn(`plugin "${rec.name}" failed to load: ${messageOf(error)}`);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (!plugin) {
|
|
80
|
-
warn(`plugin "${rec.name}" has no \`plugin\` export — skipped`);
|
|
26
|
+
for (const channel of supervisor.createChannelTypeDescriptors()) {
|
|
27
|
+
if (catalog.has(channel.type)) {
|
|
28
|
+
warn(`chat channel type "${channel.type}" is declared by more than one plugin; later declaration ignored`);
|
|
81
29
|
continue;
|
|
82
30
|
}
|
|
83
|
-
|
|
84
|
-
if (catalog.has(channel.type)) {
|
|
85
|
-
warn(`channel type "${channel.type}" already provided; "${rec.name}" ignored for it`);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
catalog.set(channel.type, channel);
|
|
89
|
-
}
|
|
31
|
+
catalog.set(channel.type, channel);
|
|
90
32
|
}
|
|
91
33
|
}
|
|
92
|
-
/**
|
|
93
|
-
* Resolve every enabled instance (joined with its channel) to a mountable
|
|
94
|
-
* descriptor, binding each to the factory its channel `type` resolves to in the
|
|
95
|
-
* catalog. Unknown types (no plugin providing them) are skipped.
|
|
96
|
-
*/
|
|
34
|
+
/** Join enabled channel instances to their remote runner-backed factories. */
|
|
97
35
|
export async function resolveChannelInstances(warn = () => { }) {
|
|
98
36
|
const instances = [];
|
|
99
|
-
for (const
|
|
100
|
-
const
|
|
101
|
-
if (!
|
|
102
|
-
warn(`unknown channel type "${
|
|
37
|
+
for (const descriptor of listEnabledDescriptors()) {
|
|
38
|
+
const channel = catalog.get(descriptor.type);
|
|
39
|
+
if (!channel) {
|
|
40
|
+
warn(`unknown chat channel type "${descriptor.type}" (instance "${descriptor.instanceId}") — no enabled plugin declares it; skipped`);
|
|
103
41
|
continue;
|
|
104
42
|
}
|
|
105
43
|
instances.push({
|
|
106
|
-
instanceId:
|
|
107
|
-
type:
|
|
108
|
-
factory:
|
|
109
|
-
options:
|
|
110
|
-
agent:
|
|
44
|
+
instanceId: descriptor.instanceId,
|
|
45
|
+
type: descriptor.type,
|
|
46
|
+
factory: channel.factory,
|
|
47
|
+
options: descriptor.options,
|
|
48
|
+
agent: descriptor.agent,
|
|
111
49
|
});
|
|
112
50
|
}
|
|
113
51
|
return instances;
|
|
114
52
|
}
|
|
115
|
-
function messageOf(error) {
|
|
116
|
-
return error instanceof Error ? error.message : String(error);
|
|
117
|
-
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
declare const DEFAULT_AUTHORITIES: readonly ["daemon.full"];
|
|
3
|
+
export type RuntimeAuthority = (typeof DEFAULT_AUTHORITIES)[number];
|
|
4
|
+
export type RemoteRuntimeAccessErrorCode = "invalid_claim" | "claim_expired" | "claim_used" | "grant_not_found" | "grant_revoked" | "grant_identity_mismatch" | "conflict" | "corrupt_state";
|
|
5
|
+
export declare class RemoteRuntimeAccessError extends Error {
|
|
6
|
+
readonly code: RemoteRuntimeAccessErrorCode;
|
|
7
|
+
constructor(code: RemoteRuntimeAccessErrorCode, message: string, options?: ErrorOptions);
|
|
8
|
+
}
|
|
9
|
+
export interface RuntimePairingClaim {
|
|
10
|
+
claimId: string;
|
|
11
|
+
claimSecret: string;
|
|
12
|
+
authorities: readonly RuntimeAuthority[];
|
|
13
|
+
clientLabel: string | null;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
expiresAt: string;
|
|
16
|
+
}
|
|
17
|
+
export interface RuntimeClientGrant {
|
|
18
|
+
grantId: string;
|
|
19
|
+
clientId: string;
|
|
20
|
+
/** Canonical Ed25519 SubjectPublicKeyInfo DER, base64url encoded. */
|
|
21
|
+
clientPublicKey: string;
|
|
22
|
+
clientLabel: string | null;
|
|
23
|
+
authorities: readonly RuntimeAuthority[];
|
|
24
|
+
createdAt: string;
|
|
25
|
+
revokedAt: string | null;
|
|
26
|
+
lastUsedAt: string | null;
|
|
27
|
+
}
|
|
28
|
+
export interface CreatePairingClaimInput {
|
|
29
|
+
ttlMs: number;
|
|
30
|
+
label?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface RedeemPairingClaimInput {
|
|
33
|
+
claimId: string;
|
|
34
|
+
claimSecret: string;
|
|
35
|
+
clientId: string;
|
|
36
|
+
clientPublicKey: string;
|
|
37
|
+
label?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ResolveActiveGrantInput {
|
|
40
|
+
grantId: string;
|
|
41
|
+
clientId: string;
|
|
42
|
+
clientPublicKey: string;
|
|
43
|
+
}
|
|
44
|
+
type Clock = () => Date;
|
|
45
|
+
/** SQLite-backed enrollment and client authorization state for a direct runtime. */
|
|
46
|
+
export declare class RemoteRuntimeAccessStore {
|
|
47
|
+
private readonly connection;
|
|
48
|
+
private readonly clock;
|
|
49
|
+
constructor(connection?: Database.Database, clock?: Clock);
|
|
50
|
+
createPairingClaim(input: CreatePairingClaimInput): RuntimePairingClaim;
|
|
51
|
+
redeemPairingClaim(input: RedeemPairingClaimInput): RuntimeClientGrant;
|
|
52
|
+
resolveActiveGrant(input: ResolveActiveGrantInput): RuntimeClientGrant;
|
|
53
|
+
listClientGrants(): RuntimeClientGrant[];
|
|
54
|
+
/** Recheck an already authenticated peer before each dispatched operation. */
|
|
55
|
+
isClientGrantActive(grantIdInput: string, clientIdInput: string): boolean;
|
|
56
|
+
revokeClientGrant(grantIdInput: string): boolean;
|
|
57
|
+
private replayConsumedClaim;
|
|
58
|
+
private deleteRetainedExpiredClaims;
|
|
59
|
+
private now;
|
|
60
|
+
}
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { createHash, createPublicKey, randomBytes, timingSafeEqual, } from "node:crypto";
|
|
2
|
+
import { db } from "./db.js";
|
|
3
|
+
const CLAIM_SECRET_HASH_DOMAIN = "rynx-runtime-pairing-claim-secret-v1\0";
|
|
4
|
+
const DEFAULT_AUTHORITIES = Object.freeze(["daemon.full"]);
|
|
5
|
+
const MIN_TTL_MS = 1_000;
|
|
6
|
+
const MAX_TTL_MS = 15 * 60_000;
|
|
7
|
+
const EXPIRED_UNCONSUMED_CLAIM_RETENTION_MS = 24 * 60 * 60_000;
|
|
8
|
+
const CLAIM_CLEANUP_BATCH_SIZE = 256;
|
|
9
|
+
const MAX_ID_LENGTH = 256;
|
|
10
|
+
const MAX_KEY_LENGTH = 512;
|
|
11
|
+
const MAX_LABEL_LENGTH = 128;
|
|
12
|
+
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]*$/;
|
|
13
|
+
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
14
|
+
export class RemoteRuntimeAccessError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
constructor(code, message, options) {
|
|
17
|
+
super(message, options);
|
|
18
|
+
this.name = "RemoteRuntimeAccessError";
|
|
19
|
+
this.code = code;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** SQLite-backed enrollment and client authorization state for a direct runtime. */
|
|
23
|
+
export class RemoteRuntimeAccessStore {
|
|
24
|
+
connection;
|
|
25
|
+
clock;
|
|
26
|
+
constructor(connection = db(), clock = () => new Date()) {
|
|
27
|
+
this.connection = connection;
|
|
28
|
+
this.clock = clock;
|
|
29
|
+
}
|
|
30
|
+
createPairingClaim(input) {
|
|
31
|
+
const ttlMs = validTtl(input.ttlMs);
|
|
32
|
+
const clientLabel = optionalLabel(input.label, "conflict");
|
|
33
|
+
const claimId = `claim_${randomBytes(18).toString("base64url")}`;
|
|
34
|
+
const claimSecret = randomBytes(32).toString("base64url");
|
|
35
|
+
const created = this.now();
|
|
36
|
+
const createdAt = created.toISOString();
|
|
37
|
+
const expiresAt = new Date(created.getTime() + ttlMs).toISOString();
|
|
38
|
+
const create = this.connection.transaction(() => {
|
|
39
|
+
this.deleteRetainedExpiredClaims(created);
|
|
40
|
+
this.connection
|
|
41
|
+
.prepare(`INSERT INTO runtime_pairing_claims
|
|
42
|
+
(claim_id, secret_hash, authorities, client_label, created_at, expires_at,
|
|
43
|
+
consumed_at, consumed_client_id, consumed_client_public_key, consumed_grant_id)
|
|
44
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, NULL, NULL)`)
|
|
45
|
+
.run(claimId, hashClaimSecret(claimSecret).toString("hex"), JSON.stringify(DEFAULT_AUTHORITIES), clientLabel, createdAt, expiresAt);
|
|
46
|
+
});
|
|
47
|
+
create.immediate();
|
|
48
|
+
return {
|
|
49
|
+
claimId,
|
|
50
|
+
claimSecret,
|
|
51
|
+
authorities: DEFAULT_AUTHORITIES,
|
|
52
|
+
clientLabel,
|
|
53
|
+
createdAt,
|
|
54
|
+
expiresAt,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
redeemPairingClaim(input) {
|
|
58
|
+
const claimId = validId(input.claimId, "claimId", "invalid_claim");
|
|
59
|
+
const claimSecret = validClaimSecret(input.claimSecret);
|
|
60
|
+
const clientId = validId(input.clientId, "clientId", "invalid_claim");
|
|
61
|
+
const clientPublicKey = canonicalEd25519PublicKey(input.clientPublicKey, "invalid_claim");
|
|
62
|
+
const requestedLabel = optionalLabel(input.label, "invalid_claim");
|
|
63
|
+
const redeem = this.connection.transaction(() => {
|
|
64
|
+
const now = this.now().toISOString();
|
|
65
|
+
const claim = this.connection
|
|
66
|
+
.prepare(`SELECT claim_id, secret_hash, authorities, client_label, created_at, expires_at,
|
|
67
|
+
consumed_at, consumed_client_id, consumed_client_public_key, consumed_grant_id
|
|
68
|
+
FROM runtime_pairing_claims
|
|
69
|
+
WHERE claim_id = ?`)
|
|
70
|
+
.get(claimId);
|
|
71
|
+
if (!claim) {
|
|
72
|
+
throw accessError("invalid_claim", "pairing claim is invalid");
|
|
73
|
+
}
|
|
74
|
+
assertAuthorities(claim.authorities);
|
|
75
|
+
assertClaimSecret(claim.secret_hash, claimSecret);
|
|
76
|
+
if (claim.consumed_at !== null) {
|
|
77
|
+
return this.replayConsumedClaim(claim, clientId, clientPublicKey);
|
|
78
|
+
}
|
|
79
|
+
assertUnconsumedClaimShape(claim);
|
|
80
|
+
if (claim.expires_at <= now) {
|
|
81
|
+
throw accessError("claim_expired", "pairing claim has expired");
|
|
82
|
+
}
|
|
83
|
+
const grantId = `grant_${randomBytes(18).toString("base64url")}`;
|
|
84
|
+
const clientLabel = requestedLabel ?? claim.client_label;
|
|
85
|
+
this.connection
|
|
86
|
+
.prepare(`INSERT INTO runtime_client_grants
|
|
87
|
+
(grant_id, client_id, client_public_key, client_label, authorities,
|
|
88
|
+
created_at, revoked_at, last_used_at)
|
|
89
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL, NULL)`)
|
|
90
|
+
.run(grantId, clientId, clientPublicKey, clientLabel, JSON.stringify(DEFAULT_AUTHORITIES), now);
|
|
91
|
+
const consumed = this.connection
|
|
92
|
+
.prepare(`UPDATE runtime_pairing_claims
|
|
93
|
+
SET consumed_at = ?, consumed_client_id = ?, consumed_client_public_key = ?,
|
|
94
|
+
consumed_grant_id = ?
|
|
95
|
+
WHERE claim_id = ? AND consumed_at IS NULL`)
|
|
96
|
+
.run(now, clientId, clientPublicKey, grantId, claimId);
|
|
97
|
+
if (consumed.changes !== 1) {
|
|
98
|
+
throw accessError("conflict", "pairing claim was consumed concurrently");
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
grantId,
|
|
102
|
+
clientId,
|
|
103
|
+
clientPublicKey,
|
|
104
|
+
clientLabel,
|
|
105
|
+
authorities: DEFAULT_AUTHORITIES,
|
|
106
|
+
createdAt: now,
|
|
107
|
+
revokedAt: null,
|
|
108
|
+
lastUsedAt: null,
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
try {
|
|
112
|
+
return redeem.immediate();
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error instanceof RemoteRuntimeAccessError)
|
|
116
|
+
throw error;
|
|
117
|
+
if (isSqliteConstraint(error)) {
|
|
118
|
+
throw accessError("conflict", "client identity already has a runtime grant", error);
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
resolveActiveGrant(input) {
|
|
124
|
+
const grantId = validId(input.grantId, "grantId", "grant_not_found");
|
|
125
|
+
const clientId = validId(input.clientId, "clientId", "grant_identity_mismatch");
|
|
126
|
+
const clientPublicKey = canonicalEd25519PublicKey(input.clientPublicKey, "grant_identity_mismatch");
|
|
127
|
+
const resolve = this.connection.transaction(() => {
|
|
128
|
+
const row = this.connection
|
|
129
|
+
.prepare(`SELECT grant_id, client_id, client_public_key, client_label, authorities,
|
|
130
|
+
created_at, revoked_at, last_used_at
|
|
131
|
+
FROM runtime_client_grants
|
|
132
|
+
WHERE grant_id = ?`)
|
|
133
|
+
.get(grantId);
|
|
134
|
+
if (!row)
|
|
135
|
+
throw accessError("grant_not_found", "runtime client grant was not found");
|
|
136
|
+
const grant = grantFromRow(row);
|
|
137
|
+
if (grant.clientId !== clientId || grant.clientPublicKey !== clientPublicKey) {
|
|
138
|
+
throw accessError("grant_identity_mismatch", "runtime client grant identity does not match");
|
|
139
|
+
}
|
|
140
|
+
if (grant.revokedAt !== null) {
|
|
141
|
+
throw accessError("grant_revoked", "runtime client grant has been revoked");
|
|
142
|
+
}
|
|
143
|
+
const lastUsedAt = this.now().toISOString();
|
|
144
|
+
const updated = this.connection
|
|
145
|
+
.prepare(`UPDATE runtime_client_grants
|
|
146
|
+
SET last_used_at = ?
|
|
147
|
+
WHERE grant_id = ? AND client_id = ? AND client_public_key = ? AND revoked_at IS NULL`)
|
|
148
|
+
.run(lastUsedAt, grantId, clientId, clientPublicKey);
|
|
149
|
+
if (updated.changes !== 1) {
|
|
150
|
+
throw accessError("grant_revoked", "runtime client grant is no longer active");
|
|
151
|
+
}
|
|
152
|
+
return { ...grant, lastUsedAt };
|
|
153
|
+
});
|
|
154
|
+
return resolve.immediate();
|
|
155
|
+
}
|
|
156
|
+
listClientGrants() {
|
|
157
|
+
const rows = this.connection
|
|
158
|
+
.prepare(`SELECT grant_id, client_id, client_public_key, client_label, authorities,
|
|
159
|
+
created_at, revoked_at, last_used_at
|
|
160
|
+
FROM runtime_client_grants
|
|
161
|
+
ORDER BY created_at, grant_id`)
|
|
162
|
+
.all();
|
|
163
|
+
return rows.map(grantFromRow);
|
|
164
|
+
}
|
|
165
|
+
/** Recheck an already authenticated peer before each dispatched operation. */
|
|
166
|
+
isClientGrantActive(grantIdInput, clientIdInput) {
|
|
167
|
+
const grantId = validId(grantIdInput, "grantId", "grant_not_found");
|
|
168
|
+
const clientId = validId(clientIdInput, "clientId", "grant_identity_mismatch");
|
|
169
|
+
const row = this.connection
|
|
170
|
+
.prepare(`SELECT grant_id, client_id, client_public_key, client_label, authorities,
|
|
171
|
+
created_at, revoked_at, last_used_at
|
|
172
|
+
FROM runtime_client_grants
|
|
173
|
+
WHERE grant_id = ?`)
|
|
174
|
+
.get(grantId);
|
|
175
|
+
if (!row)
|
|
176
|
+
return false;
|
|
177
|
+
const grant = grantFromRow(row);
|
|
178
|
+
return grant.clientId === clientId && grant.revokedAt === null;
|
|
179
|
+
}
|
|
180
|
+
revokeClientGrant(grantIdInput) {
|
|
181
|
+
const grantId = validId(grantIdInput, "grantId", "grant_not_found");
|
|
182
|
+
const revokedAt = this.now().toISOString();
|
|
183
|
+
const revoke = this.connection.transaction(() => {
|
|
184
|
+
const revoked = this.connection
|
|
185
|
+
.prepare(`UPDATE runtime_client_grants
|
|
186
|
+
SET revoked_at = ?
|
|
187
|
+
WHERE grant_id = ? AND revoked_at IS NULL`)
|
|
188
|
+
.run(revokedAt, grantId).changes === 1;
|
|
189
|
+
// An active grant keeps its consumed claim so the enrolling client can
|
|
190
|
+
// recover an accepted response it missed. Revocation ends that retry
|
|
191
|
+
// contract, so remove the no-longer-secret enrollment record atomically.
|
|
192
|
+
this.connection
|
|
193
|
+
.prepare(`DELETE FROM runtime_pairing_claims
|
|
194
|
+
WHERE consumed_grant_id = ? AND consumed_at IS NOT NULL`)
|
|
195
|
+
.run(grantId);
|
|
196
|
+
return revoked;
|
|
197
|
+
});
|
|
198
|
+
return revoke.immediate();
|
|
199
|
+
}
|
|
200
|
+
replayConsumedClaim(claim, clientId, clientPublicKey) {
|
|
201
|
+
if (claim.consumed_client_id === null ||
|
|
202
|
+
claim.consumed_client_public_key === null ||
|
|
203
|
+
claim.consumed_grant_id === null) {
|
|
204
|
+
throw accessError("corrupt_state", "consumed pairing claim is incomplete");
|
|
205
|
+
}
|
|
206
|
+
let consumedPublicKey;
|
|
207
|
+
try {
|
|
208
|
+
consumedPublicKey = canonicalEd25519PublicKey(claim.consumed_client_public_key, "corrupt_state");
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (error instanceof RemoteRuntimeAccessError)
|
|
212
|
+
throw error;
|
|
213
|
+
throw accessError("corrupt_state", "consumed pairing claim has an invalid client key", error);
|
|
214
|
+
}
|
|
215
|
+
if (claim.consumed_client_id !== clientId || consumedPublicKey !== clientPublicKey) {
|
|
216
|
+
throw accessError("claim_used", "pairing claim has already been used");
|
|
217
|
+
}
|
|
218
|
+
const row = this.connection
|
|
219
|
+
.prepare(`SELECT grant_id, client_id, client_public_key, client_label, authorities,
|
|
220
|
+
created_at, revoked_at, last_used_at
|
|
221
|
+
FROM runtime_client_grants
|
|
222
|
+
WHERE grant_id = ?`)
|
|
223
|
+
.get(claim.consumed_grant_id);
|
|
224
|
+
if (!row) {
|
|
225
|
+
throw accessError("corrupt_state", "consumed pairing claim has no associated grant");
|
|
226
|
+
}
|
|
227
|
+
const grant = grantFromRow(row);
|
|
228
|
+
if (grant.clientId !== clientId || grant.clientPublicKey !== clientPublicKey) {
|
|
229
|
+
throw accessError("corrupt_state", "consumed pairing claim and grant identities differ");
|
|
230
|
+
}
|
|
231
|
+
if (grant.revokedAt !== null) {
|
|
232
|
+
throw accessError("grant_revoked", "runtime client grant has been revoked");
|
|
233
|
+
}
|
|
234
|
+
return grant;
|
|
235
|
+
}
|
|
236
|
+
deleteRetainedExpiredClaims(now) {
|
|
237
|
+
const cutoff = new Date(now.getTime() - EXPIRED_UNCONSUMED_CLAIM_RETENTION_MS).toISOString();
|
|
238
|
+
this.connection
|
|
239
|
+
.prepare(`DELETE FROM runtime_pairing_claims
|
|
240
|
+
WHERE claim_id IN (
|
|
241
|
+
SELECT claim_id
|
|
242
|
+
FROM runtime_pairing_claims
|
|
243
|
+
WHERE expires_at <= ? AND consumed_at IS NULL
|
|
244
|
+
ORDER BY expires_at
|
|
245
|
+
LIMIT ?
|
|
246
|
+
)`)
|
|
247
|
+
.run(cutoff, CLAIM_CLEANUP_BATCH_SIZE);
|
|
248
|
+
}
|
|
249
|
+
now() {
|
|
250
|
+
const value = this.clock();
|
|
251
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
|
|
252
|
+
throw accessError("corrupt_state", "runtime access clock returned an invalid date");
|
|
253
|
+
}
|
|
254
|
+
return new Date(value.getTime());
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function validTtl(value) {
|
|
258
|
+
if (!Number.isSafeInteger(value) || value < MIN_TTL_MS || value > MAX_TTL_MS) {
|
|
259
|
+
throw accessError("conflict", `ttlMs must be an integer between ${MIN_TTL_MS} and ${MAX_TTL_MS}`);
|
|
260
|
+
}
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
function validId(value, label, code) {
|
|
264
|
+
if (typeof value !== "string" ||
|
|
265
|
+
value.length === 0 ||
|
|
266
|
+
value.length > MAX_ID_LENGTH ||
|
|
267
|
+
value.trim() !== value ||
|
|
268
|
+
!ID_PATTERN.test(value)) {
|
|
269
|
+
throw accessError(code, `${label} is invalid`);
|
|
270
|
+
}
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
function validClaimSecret(value) {
|
|
274
|
+
if (typeof value !== "string" ||
|
|
275
|
+
value.length !== 43 ||
|
|
276
|
+
!BASE64URL_PATTERN.test(value) ||
|
|
277
|
+
Buffer.from(value, "base64url").length !== 32 ||
|
|
278
|
+
Buffer.from(value, "base64url").toString("base64url") !== value) {
|
|
279
|
+
throw accessError("invalid_claim", "pairing claim is invalid");
|
|
280
|
+
}
|
|
281
|
+
return value;
|
|
282
|
+
}
|
|
283
|
+
function optionalLabel(value, code) {
|
|
284
|
+
if (value === undefined)
|
|
285
|
+
return null;
|
|
286
|
+
if (typeof value !== "string" ||
|
|
287
|
+
value.length === 0 ||
|
|
288
|
+
value.length > MAX_LABEL_LENGTH ||
|
|
289
|
+
value.trim() !== value ||
|
|
290
|
+
/[\u0000-\u001f\u007f]/.test(value)) {
|
|
291
|
+
throw accessError(code, "client label is invalid");
|
|
292
|
+
}
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
function canonicalEd25519PublicKey(value, code) {
|
|
296
|
+
if (typeof value !== "string" ||
|
|
297
|
+
value.length === 0 ||
|
|
298
|
+
value.length > MAX_KEY_LENGTH ||
|
|
299
|
+
!BASE64URL_PATTERN.test(value)) {
|
|
300
|
+
throw accessError(code, "client public key is invalid");
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const bytes = Buffer.from(value, "base64url");
|
|
304
|
+
if (bytes.toString("base64url") !== value) {
|
|
305
|
+
throw new Error("non-canonical base64url");
|
|
306
|
+
}
|
|
307
|
+
const key = createPublicKey({ key: bytes, format: "der", type: "spki" });
|
|
308
|
+
if (key.asymmetricKeyType !== "ed25519") {
|
|
309
|
+
throw new Error("not an Ed25519 key");
|
|
310
|
+
}
|
|
311
|
+
return key.export({ format: "der", type: "spki" }).toString("base64url");
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (error instanceof RemoteRuntimeAccessError)
|
|
315
|
+
throw error;
|
|
316
|
+
throw accessError(code, "client public key is invalid", error);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function assertAuthorities(raw) {
|
|
320
|
+
try {
|
|
321
|
+
const parsed = JSON.parse(raw);
|
|
322
|
+
if (!Array.isArray(parsed) ||
|
|
323
|
+
parsed.length !== 1 ||
|
|
324
|
+
parsed[0] !== DEFAULT_AUTHORITIES[0]) {
|
|
325
|
+
throw new Error("unsupported authorities");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
throw accessError("corrupt_state", "runtime authorities are corrupt", error);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function assertClaimSecret(storedHash, claimSecret) {
|
|
333
|
+
const expected = hashClaimSecret(claimSecret);
|
|
334
|
+
let actual;
|
|
335
|
+
try {
|
|
336
|
+
if (!/^[0-9a-f]{64}$/.test(storedHash))
|
|
337
|
+
throw new Error("invalid secret hash");
|
|
338
|
+
actual = Buffer.from(storedHash, "hex");
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
throw accessError("corrupt_state", "pairing claim secret hash is corrupt", error);
|
|
342
|
+
}
|
|
343
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
|
|
344
|
+
throw accessError("invalid_claim", "pairing claim is invalid");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function assertUnconsumedClaimShape(claim) {
|
|
348
|
+
if (claim.consumed_client_id !== null ||
|
|
349
|
+
claim.consumed_client_public_key !== null ||
|
|
350
|
+
claim.consumed_grant_id !== null) {
|
|
351
|
+
throw accessError("corrupt_state", "unconsumed pairing claim has consumed identity state");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function hashClaimSecret(secret) {
|
|
355
|
+
return createHash("sha256").update(CLAIM_SECRET_HASH_DOMAIN).update(secret).digest();
|
|
356
|
+
}
|
|
357
|
+
function grantFromRow(row) {
|
|
358
|
+
validId(row.grant_id, "stored grantId", "corrupt_state");
|
|
359
|
+
validId(row.client_id, "stored clientId", "corrupt_state");
|
|
360
|
+
const clientPublicKey = canonicalEd25519PublicKey(row.client_public_key, "corrupt_state");
|
|
361
|
+
if (clientPublicKey !== row.client_public_key) {
|
|
362
|
+
throw accessError("corrupt_state", "stored client public key is not canonical");
|
|
363
|
+
}
|
|
364
|
+
const clientLabel = row.client_label === null ? null : optionalLabel(row.client_label, "corrupt_state");
|
|
365
|
+
assertAuthorities(row.authorities);
|
|
366
|
+
return {
|
|
367
|
+
grantId: row.grant_id,
|
|
368
|
+
clientId: row.client_id,
|
|
369
|
+
clientPublicKey,
|
|
370
|
+
clientLabel,
|
|
371
|
+
authorities: DEFAULT_AUTHORITIES,
|
|
372
|
+
createdAt: row.created_at,
|
|
373
|
+
revokedAt: row.revoked_at,
|
|
374
|
+
lastUsedAt: row.last_used_at,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function isSqliteConstraint(error) {
|
|
378
|
+
return (typeof error === "object" &&
|
|
379
|
+
error !== null &&
|
|
380
|
+
"code" in error &&
|
|
381
|
+
typeof error.code === "string" &&
|
|
382
|
+
error.code.startsWith("SQLITE_CONSTRAINT"));
|
|
383
|
+
}
|
|
384
|
+
function accessError(code, message, cause) {
|
|
385
|
+
return new RemoteRuntimeAccessError(code, message, cause === undefined ? undefined : { cause });
|
|
386
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type PairingOffer } from "@rynx-ai/protocol/direct-runtime";
|
|
2
|
+
import type { DirectRuntimeConnectionHub, RemoteRuntimeAdminHost } from "@rynx-ai/server";
|
|
3
|
+
import type { DirectRuntimeConfig } from "./direct-runtime-config.js";
|
|
4
|
+
import type { DaemonDirectRuntimeEncryptionIdentity, DaemonInstallationIdentity } from "./installation-id.js";
|
|
5
|
+
import { RemoteRuntimeAccessStore } from "./remote-runtime-access-store.js";
|
|
6
|
+
export declare class DaemonRemoteRuntimeAdmin implements RemoteRuntimeAdminHost {
|
|
7
|
+
private readonly identity;
|
|
8
|
+
private readonly encryption;
|
|
9
|
+
private readonly access;
|
|
10
|
+
private readonly directConfig;
|
|
11
|
+
private readonly connections;
|
|
12
|
+
constructor(identity: DaemonInstallationIdentity, encryption: DaemonDirectRuntimeEncryptionIdentity, access: RemoteRuntimeAccessStore, directConfig: DirectRuntimeConfig, connections: DirectRuntimeConnectionHub);
|
|
13
|
+
listShareAddresses(): import("@rynx-ai/protocol/control").ControlRuntimeShareAddress[];
|
|
14
|
+
createPairingOffer(input?: {
|
|
15
|
+
clientLabel?: string;
|
|
16
|
+
address?: string;
|
|
17
|
+
}): PairingOffer;
|
|
18
|
+
listClientGrants(): {
|
|
19
|
+
grantId: string;
|
|
20
|
+
clientId: string;
|
|
21
|
+
clientLabel: string | null;
|
|
22
|
+
clientKeyFingerprint: string;
|
|
23
|
+
authorities: "daemon.full"[];
|
|
24
|
+
createdAt: string;
|
|
25
|
+
revokedAt: string | null;
|
|
26
|
+
lastUsedAt: string | null;
|
|
27
|
+
}[];
|
|
28
|
+
revokeClientGrant(grantId: string): {
|
|
29
|
+
revoked: boolean;
|
|
30
|
+
closedConnections: number;
|
|
31
|
+
};
|
|
32
|
+
}
|