@rynx-ai/daemon 0.1.0 → 0.1.10-beta.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/dist/app-browser-host-supervisor.d.ts +97 -0
- package/dist/app-browser-host-supervisor.js +529 -0
- package/dist/browser-artifact-management.d.ts +20 -0
- package/dist/browser-artifact-management.js +61 -0
- 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/chrome-inspection-gateway.d.ts +58 -0
- package/dist/chrome-inspection-gateway.js +806 -0
- package/dist/chrome-inspection-manager.d.ts +94 -0
- package/dist/chrome-inspection-manager.js +573 -0
- package/dist/cli.js +478 -30
- package/dist/control-client.d.ts +122 -0
- package/dist/control-client.js +855 -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-build-status.d.ts +8 -0
- package/dist/daemon-build-status.js +18 -0
- package/dist/daemon-owner.d.ts +14 -0
- package/dist/daemon-owner.js +104 -0
- package/dist/daemon-server.d.ts +29 -2
- package/dist/daemon-server.js +1113 -26
- package/dist/db.js +196 -10
- package/dist/desktop-browser-host-client.d.ts +35 -0
- package/dist/desktop-browser-host-client.js +415 -0
- package/dist/direct-runtime-authenticator.d.ts +13 -0
- package/dist/direct-runtime-authenticator.js +132 -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 +2173 -0
- package/dist/index-daemon.js +55 -5
- package/dist/installation-id.d.ts +33 -0
- package/dist/installation-id.js +692 -0
- package/dist/maintenance-management.d.ts +11 -0
- package/dist/maintenance-management.js +13 -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 +71 -15
- package/dist/plugin-installer.js +521 -155
- package/dist/plugin-management-service.d.ts +58 -0
- package/dist/plugin-management-service.js +240 -0
- package/dist/plugin-package.d.ts +61 -0
- package/dist/plugin-package.js +732 -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 +92 -18
- 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 +62 -0
- package/dist/remote-runtime-access-store.js +393 -0
- package/dist/remote-runtime-admin.d.ts +35 -0
- package/dist/remote-runtime-admin.js +110 -0
- package/dist/remote-runtime-connection-manager.d.ts +103 -0
- package/dist/remote-runtime-connection-manager.js +1575 -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 +51 -0
- package/dist/remote-runtime-target-control.js +488 -0
- package/dist/remote-runtime-target-store.d.ts +56 -0
- package/dist/remote-runtime-target-store.js +326 -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-log-store.js +29 -0
- package/dist/session-meta-store.d.ts +3 -2
- package/dist/session-meta-store.js +59 -7
- package/dist/session-pending-message-store.d.ts +2 -0
- package/dist/session-pending-message-store.js +41 -0
- package/dist/session-resource-store.d.ts +32 -0
- package/dist/session-resource-store.js +700 -0
- package/dist/setup.d.ts +16 -0
- package/dist/setup.js +144 -5
- package/dist/skills-catalog.js +8 -4
- package/dist/update.d.ts +11 -2
- package/dist/update.js +0 -27
- package/package.json +23 -10
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
deleteRevokedClientGrant(grantIdInput: string): boolean;
|
|
58
|
+
private replayConsumedClaim;
|
|
59
|
+
private deleteRetainedExpiredClaims;
|
|
60
|
+
private now;
|
|
61
|
+
}
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,393 @@
|
|
|
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
|
+
deleteRevokedClientGrant(grantIdInput) {
|
|
201
|
+
const grantId = validId(grantIdInput, "grantId", "grant_not_found");
|
|
202
|
+
return this.connection
|
|
203
|
+
.prepare(`DELETE FROM runtime_client_grants
|
|
204
|
+
WHERE grant_id = ? AND revoked_at IS NOT NULL`)
|
|
205
|
+
.run(grantId).changes === 1;
|
|
206
|
+
}
|
|
207
|
+
replayConsumedClaim(claim, clientId, clientPublicKey) {
|
|
208
|
+
if (claim.consumed_client_id === null ||
|
|
209
|
+
claim.consumed_client_public_key === null ||
|
|
210
|
+
claim.consumed_grant_id === null) {
|
|
211
|
+
throw accessError("corrupt_state", "consumed pairing claim is incomplete");
|
|
212
|
+
}
|
|
213
|
+
let consumedPublicKey;
|
|
214
|
+
try {
|
|
215
|
+
consumedPublicKey = canonicalEd25519PublicKey(claim.consumed_client_public_key, "corrupt_state");
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
if (error instanceof RemoteRuntimeAccessError)
|
|
219
|
+
throw error;
|
|
220
|
+
throw accessError("corrupt_state", "consumed pairing claim has an invalid client key", error);
|
|
221
|
+
}
|
|
222
|
+
if (claim.consumed_client_id !== clientId || consumedPublicKey !== clientPublicKey) {
|
|
223
|
+
throw accessError("claim_used", "pairing claim has already been used");
|
|
224
|
+
}
|
|
225
|
+
const row = this.connection
|
|
226
|
+
.prepare(`SELECT grant_id, client_id, client_public_key, client_label, authorities,
|
|
227
|
+
created_at, revoked_at, last_used_at
|
|
228
|
+
FROM runtime_client_grants
|
|
229
|
+
WHERE grant_id = ?`)
|
|
230
|
+
.get(claim.consumed_grant_id);
|
|
231
|
+
if (!row) {
|
|
232
|
+
throw accessError("corrupt_state", "consumed pairing claim has no associated grant");
|
|
233
|
+
}
|
|
234
|
+
const grant = grantFromRow(row);
|
|
235
|
+
if (grant.clientId !== clientId || grant.clientPublicKey !== clientPublicKey) {
|
|
236
|
+
throw accessError("corrupt_state", "consumed pairing claim and grant identities differ");
|
|
237
|
+
}
|
|
238
|
+
if (grant.revokedAt !== null) {
|
|
239
|
+
throw accessError("grant_revoked", "runtime client grant has been revoked");
|
|
240
|
+
}
|
|
241
|
+
return grant;
|
|
242
|
+
}
|
|
243
|
+
deleteRetainedExpiredClaims(now) {
|
|
244
|
+
const cutoff = new Date(now.getTime() - EXPIRED_UNCONSUMED_CLAIM_RETENTION_MS).toISOString();
|
|
245
|
+
this.connection
|
|
246
|
+
.prepare(`DELETE FROM runtime_pairing_claims
|
|
247
|
+
WHERE claim_id IN (
|
|
248
|
+
SELECT claim_id
|
|
249
|
+
FROM runtime_pairing_claims
|
|
250
|
+
WHERE expires_at <= ? AND consumed_at IS NULL
|
|
251
|
+
ORDER BY expires_at
|
|
252
|
+
LIMIT ?
|
|
253
|
+
)`)
|
|
254
|
+
.run(cutoff, CLAIM_CLEANUP_BATCH_SIZE);
|
|
255
|
+
}
|
|
256
|
+
now() {
|
|
257
|
+
const value = this.clock();
|
|
258
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
|
|
259
|
+
throw accessError("corrupt_state", "runtime access clock returned an invalid date");
|
|
260
|
+
}
|
|
261
|
+
return new Date(value.getTime());
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function validTtl(value) {
|
|
265
|
+
if (!Number.isSafeInteger(value) || value < MIN_TTL_MS || value > MAX_TTL_MS) {
|
|
266
|
+
throw accessError("conflict", `ttlMs must be an integer between ${MIN_TTL_MS} and ${MAX_TTL_MS}`);
|
|
267
|
+
}
|
|
268
|
+
return value;
|
|
269
|
+
}
|
|
270
|
+
function validId(value, label, code) {
|
|
271
|
+
if (typeof value !== "string" ||
|
|
272
|
+
value.length === 0 ||
|
|
273
|
+
value.length > MAX_ID_LENGTH ||
|
|
274
|
+
value.trim() !== value ||
|
|
275
|
+
!ID_PATTERN.test(value)) {
|
|
276
|
+
throw accessError(code, `${label} is invalid`);
|
|
277
|
+
}
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
function validClaimSecret(value) {
|
|
281
|
+
if (typeof value !== "string" ||
|
|
282
|
+
value.length !== 43 ||
|
|
283
|
+
!BASE64URL_PATTERN.test(value) ||
|
|
284
|
+
Buffer.from(value, "base64url").length !== 32 ||
|
|
285
|
+
Buffer.from(value, "base64url").toString("base64url") !== value) {
|
|
286
|
+
throw accessError("invalid_claim", "pairing claim is invalid");
|
|
287
|
+
}
|
|
288
|
+
return value;
|
|
289
|
+
}
|
|
290
|
+
function optionalLabel(value, code) {
|
|
291
|
+
if (value === undefined)
|
|
292
|
+
return null;
|
|
293
|
+
if (typeof value !== "string" ||
|
|
294
|
+
value.length === 0 ||
|
|
295
|
+
value.length > MAX_LABEL_LENGTH ||
|
|
296
|
+
value.trim() !== value ||
|
|
297
|
+
/[\u0000-\u001f\u007f]/.test(value)) {
|
|
298
|
+
throw accessError(code, "client label is invalid");
|
|
299
|
+
}
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
function canonicalEd25519PublicKey(value, code) {
|
|
303
|
+
if (typeof value !== "string" ||
|
|
304
|
+
value.length === 0 ||
|
|
305
|
+
value.length > MAX_KEY_LENGTH ||
|
|
306
|
+
!BASE64URL_PATTERN.test(value)) {
|
|
307
|
+
throw accessError(code, "client public key is invalid");
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const bytes = Buffer.from(value, "base64url");
|
|
311
|
+
if (bytes.toString("base64url") !== value) {
|
|
312
|
+
throw new Error("non-canonical base64url");
|
|
313
|
+
}
|
|
314
|
+
const key = createPublicKey({ key: bytes, format: "der", type: "spki" });
|
|
315
|
+
if (key.asymmetricKeyType !== "ed25519") {
|
|
316
|
+
throw new Error("not an Ed25519 key");
|
|
317
|
+
}
|
|
318
|
+
return key.export({ format: "der", type: "spki" }).toString("base64url");
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
if (error instanceof RemoteRuntimeAccessError)
|
|
322
|
+
throw error;
|
|
323
|
+
throw accessError(code, "client public key is invalid", error);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function assertAuthorities(raw) {
|
|
327
|
+
try {
|
|
328
|
+
const parsed = JSON.parse(raw);
|
|
329
|
+
if (!Array.isArray(parsed) ||
|
|
330
|
+
parsed.length !== 1 ||
|
|
331
|
+
parsed[0] !== DEFAULT_AUTHORITIES[0]) {
|
|
332
|
+
throw new Error("unsupported authorities");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
throw accessError("corrupt_state", "runtime authorities are corrupt", error);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function assertClaimSecret(storedHash, claimSecret) {
|
|
340
|
+
const expected = hashClaimSecret(claimSecret);
|
|
341
|
+
let actual;
|
|
342
|
+
try {
|
|
343
|
+
if (!/^[0-9a-f]{64}$/.test(storedHash))
|
|
344
|
+
throw new Error("invalid secret hash");
|
|
345
|
+
actual = Buffer.from(storedHash, "hex");
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
throw accessError("corrupt_state", "pairing claim secret hash is corrupt", error);
|
|
349
|
+
}
|
|
350
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
|
|
351
|
+
throw accessError("invalid_claim", "pairing claim is invalid");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function assertUnconsumedClaimShape(claim) {
|
|
355
|
+
if (claim.consumed_client_id !== null ||
|
|
356
|
+
claim.consumed_client_public_key !== null ||
|
|
357
|
+
claim.consumed_grant_id !== null) {
|
|
358
|
+
throw accessError("corrupt_state", "unconsumed pairing claim has consumed identity state");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function hashClaimSecret(secret) {
|
|
362
|
+
return createHash("sha256").update(CLAIM_SECRET_HASH_DOMAIN).update(secret).digest();
|
|
363
|
+
}
|
|
364
|
+
function grantFromRow(row) {
|
|
365
|
+
validId(row.grant_id, "stored grantId", "corrupt_state");
|
|
366
|
+
validId(row.client_id, "stored clientId", "corrupt_state");
|
|
367
|
+
const clientPublicKey = canonicalEd25519PublicKey(row.client_public_key, "corrupt_state");
|
|
368
|
+
if (clientPublicKey !== row.client_public_key) {
|
|
369
|
+
throw accessError("corrupt_state", "stored client public key is not canonical");
|
|
370
|
+
}
|
|
371
|
+
const clientLabel = row.client_label === null ? null : optionalLabel(row.client_label, "corrupt_state");
|
|
372
|
+
assertAuthorities(row.authorities);
|
|
373
|
+
return {
|
|
374
|
+
grantId: row.grant_id,
|
|
375
|
+
clientId: row.client_id,
|
|
376
|
+
clientPublicKey,
|
|
377
|
+
clientLabel,
|
|
378
|
+
authorities: DEFAULT_AUTHORITIES,
|
|
379
|
+
createdAt: row.created_at,
|
|
380
|
+
revokedAt: row.revoked_at,
|
|
381
|
+
lastUsedAt: row.last_used_at,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function isSqliteConstraint(error) {
|
|
385
|
+
return (typeof error === "object" &&
|
|
386
|
+
error !== null &&
|
|
387
|
+
"code" in error &&
|
|
388
|
+
typeof error.code === "string" &&
|
|
389
|
+
error.code.startsWith("SQLITE_CONSTRAINT"));
|
|
390
|
+
}
|
|
391
|
+
function accessError(code, message, cause) {
|
|
392
|
+
return new RemoteRuntimeAccessError(code, message, cause === undefined ? undefined : { cause });
|
|
393
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
deleteRevokedClientGrant(grantId: string): {
|
|
33
|
+
deleted: boolean;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { DIRECT_RUNTIME_CONTROL_PATH, parsePairingOffer, } from "@rynx-ai/protocol/direct-runtime";
|
|
3
|
+
import { listRuntimeShareAddresses } from "./runtime-share-addresses.js";
|
|
4
|
+
const DEFAULT_PAIRING_TTL_MS = 5 * 60_000;
|
|
5
|
+
export class DaemonRemoteRuntimeAdmin {
|
|
6
|
+
identity;
|
|
7
|
+
encryption;
|
|
8
|
+
access;
|
|
9
|
+
directConfig;
|
|
10
|
+
connections;
|
|
11
|
+
constructor(identity, encryption, access, directConfig, connections) {
|
|
12
|
+
this.identity = identity;
|
|
13
|
+
this.encryption = encryption;
|
|
14
|
+
this.access = access;
|
|
15
|
+
this.directConfig = directConfig;
|
|
16
|
+
this.connections = connections;
|
|
17
|
+
}
|
|
18
|
+
listShareAddresses() {
|
|
19
|
+
return listRuntimeShareAddresses({ bindHost: this.directConfig.bindHost });
|
|
20
|
+
}
|
|
21
|
+
createPairingOffer(input = {}) {
|
|
22
|
+
const directEndpoint = advertisedEndpoint(input.address, this.directConfig.port);
|
|
23
|
+
const claim = this.access.createPairingClaim({
|
|
24
|
+
ttlMs: DEFAULT_PAIRING_TTL_MS,
|
|
25
|
+
...(input.clientLabel ? { label: input.clientLabel } : {}),
|
|
26
|
+
});
|
|
27
|
+
return parsePairingOffer({
|
|
28
|
+
type: "rynx.remote-runtime.pairing-offer",
|
|
29
|
+
enrollmentVersion: 2,
|
|
30
|
+
daemonId: this.identity.installationId,
|
|
31
|
+
identityAlgorithm: "Ed25519",
|
|
32
|
+
identityPublicKey: this.identity.publicKey,
|
|
33
|
+
security: "e2ee-v1",
|
|
34
|
+
encryptionAlgorithm: "X25519",
|
|
35
|
+
serverEncryptionPublicKey: this.encryption.publicKey,
|
|
36
|
+
directEndpoint,
|
|
37
|
+
claimId: claim.claimId,
|
|
38
|
+
claimSecret: claim.claimSecret,
|
|
39
|
+
expiresAt: claim.expiresAt,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
listClientGrants() {
|
|
43
|
+
return this.access.listClientGrants().map(redactGrant);
|
|
44
|
+
}
|
|
45
|
+
revokeClientGrant(grantId) {
|
|
46
|
+
const revoked = this.access.revokeClientGrant(grantId);
|
|
47
|
+
const closedConnections = revoked ? this.connections.revokeGrant(grantId) : 0;
|
|
48
|
+
return { revoked, closedConnections };
|
|
49
|
+
}
|
|
50
|
+
deleteRevokedClientGrant(grantId) {
|
|
51
|
+
return { deleted: this.access.deleteRevokedClientGrant(grantId) };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function advertisedEndpoint(address, listenerPort) {
|
|
55
|
+
const input = address?.trim() || "127.0.0.1";
|
|
56
|
+
let endpoint;
|
|
57
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(input)) {
|
|
58
|
+
try {
|
|
59
|
+
endpoint = new URL(input);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new Error("Direct Runtime address must be a host or ws(s) URL");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
if (/[/\\?#@\s]/.test(input)) {
|
|
67
|
+
throw new Error("Direct Runtime address must be a host or ws(s) URL");
|
|
68
|
+
}
|
|
69
|
+
const host = input.includes(":") ? `[${input}]` : input;
|
|
70
|
+
try {
|
|
71
|
+
endpoint = new URL(`ws://${host}:${listenerPort}`);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new Error("Direct Runtime address must be a host or ws(s) URL");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if ((endpoint.protocol !== "ws:" && endpoint.protocol !== "wss:") ||
|
|
78
|
+
endpoint.username ||
|
|
79
|
+
endpoint.password ||
|
|
80
|
+
endpoint.search ||
|
|
81
|
+
endpoint.hash ||
|
|
82
|
+
!endpoint.hostname ||
|
|
83
|
+
endpoint.hostname === "0.0.0.0" ||
|
|
84
|
+
endpoint.hostname === "[::]" ||
|
|
85
|
+
endpoint.hostname === "::") {
|
|
86
|
+
throw new Error("Direct Runtime address must be a credential-free dialable ws(s) route");
|
|
87
|
+
}
|
|
88
|
+
if (endpoint.pathname !== "/" && endpoint.pathname !== DIRECT_RUNTIME_CONTROL_PATH) {
|
|
89
|
+
throw new Error(`Direct Runtime address path must be / or ${DIRECT_RUNTIME_CONTROL_PATH}`);
|
|
90
|
+
}
|
|
91
|
+
endpoint.pathname = DIRECT_RUNTIME_CONTROL_PATH;
|
|
92
|
+
return endpoint.toString();
|
|
93
|
+
}
|
|
94
|
+
function redactGrant(grant) {
|
|
95
|
+
return {
|
|
96
|
+
grantId: grant.grantId,
|
|
97
|
+
clientId: grant.clientId,
|
|
98
|
+
clientLabel: grant.clientLabel,
|
|
99
|
+
clientKeyFingerprint: publicKeyFingerprint(grant.clientPublicKey),
|
|
100
|
+
authorities: [...grant.authorities],
|
|
101
|
+
createdAt: grant.createdAt,
|
|
102
|
+
revokedAt: grant.revokedAt,
|
|
103
|
+
lastUsedAt: grant.lastUsedAt,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function publicKeyFingerprint(publicKey) {
|
|
107
|
+
return `SHA256:${createHash("sha256")
|
|
108
|
+
.update(Buffer.from(publicKey, "base64url"))
|
|
109
|
+
.digest("base64url")}`;
|
|
110
|
+
}
|