@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.
Files changed (89) hide show
  1. package/dist/browser-cli-args.d.ts +20 -0
  2. package/dist/browser-cli-args.js +100 -0
  3. package/dist/browser-runner-session-context.d.ts +24 -0
  4. package/dist/browser-runner-session-context.js +113 -0
  5. package/dist/cdp-keyboard-input.d.ts +6 -0
  6. package/dist/cdp-keyboard-input.js +209 -0
  7. package/dist/channel-store.d.ts +0 -6
  8. package/dist/channel-store.js +2 -18
  9. package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
  10. package/dist/chrome-for-testing-release-resolver.js +214 -0
  11. package/dist/chrome-for-testing-store.d.ts +85 -0
  12. package/dist/chrome-for-testing-store.js +836 -0
  13. package/dist/cli.js +467 -30
  14. package/dist/control-client.d.ts +113 -0
  15. package/dist/control-client.js +792 -0
  16. package/dist/control-deps.js +37 -6
  17. package/dist/control-endpoint.d.ts +14 -0
  18. package/dist/control-endpoint.js +124 -0
  19. package/dist/control-link-store.d.ts +16 -0
  20. package/dist/control-link-store.js +53 -0
  21. package/dist/daemon-owner.d.ts +14 -0
  22. package/dist/daemon-owner.js +104 -0
  23. package/dist/daemon-server.d.ts +12 -2
  24. package/dist/daemon-server.js +559 -28
  25. package/dist/db.js +140 -10
  26. package/dist/desktop-browser-host-client.d.ts +34 -0
  27. package/dist/desktop-browser-host-client.js +413 -0
  28. package/dist/direct-runtime-authenticator.d.ts +13 -0
  29. package/dist/direct-runtime-authenticator.js +127 -0
  30. package/dist/direct-runtime-config.d.ts +20 -0
  31. package/dist/direct-runtime-config.js +101 -0
  32. package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
  33. package/dist/embedded-browser-profile-cleanup.js +338 -0
  34. package/dist/headless-browser-host.d.ts +14 -0
  35. package/dist/headless-browser-host.js +2156 -0
  36. package/dist/index-daemon.js +28 -5
  37. package/dist/installation-id.d.ts +33 -0
  38. package/dist/installation-id.js +692 -0
  39. package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
  40. package/dist/migrations/cleanup-legacy-sessions.js +10 -28
  41. package/dist/plugin-cli.d.ts +30 -0
  42. package/dist/plugin-cli.js +270 -0
  43. package/dist/plugin-host-rpc.d.ts +85 -0
  44. package/dist/plugin-host-rpc.js +878 -0
  45. package/dist/plugin-install-restart.d.ts +13 -0
  46. package/dist/plugin-install-restart.js +24 -0
  47. package/dist/plugin-installer.d.ts +36 -15
  48. package/dist/plugin-installer.js +420 -155
  49. package/dist/plugin-package.d.ts +38 -0
  50. package/dist/plugin-package.js +553 -0
  51. package/dist/plugin-runtime-control.d.ts +2 -0
  52. package/dist/plugin-runtime-control.js +37 -0
  53. package/dist/plugin-store.d.ts +37 -8
  54. package/dist/plugin-store.js +80 -15
  55. package/dist/plugin-supervisor.d.ts +160 -0
  56. package/dist/plugin-supervisor.js +1143 -0
  57. package/dist/pm2.d.ts +3 -1
  58. package/dist/pm2.js +55 -13
  59. package/dist/provider-cli-service.d.ts +46 -0
  60. package/dist/provider-cli-service.js +510 -0
  61. package/dist/registry.d.ts +6 -19
  62. package/dist/registry.js +22 -87
  63. package/dist/remote-runtime-access-store.d.ts +61 -0
  64. package/dist/remote-runtime-access-store.js +386 -0
  65. package/dist/remote-runtime-admin.d.ts +32 -0
  66. package/dist/remote-runtime-admin.js +107 -0
  67. package/dist/remote-runtime-connection-manager.d.ts +92 -0
  68. package/dist/remote-runtime-connection-manager.js +1409 -0
  69. package/dist/remote-runtime-credential-store.d.ts +45 -0
  70. package/dist/remote-runtime-credential-store.js +639 -0
  71. package/dist/remote-runtime-target-control.d.ts +47 -0
  72. package/dist/remote-runtime-target-control.js +433 -0
  73. package/dist/remote-runtime-target-store.d.ts +53 -0
  74. package/dist/remote-runtime-target-store.js +307 -0
  75. package/dist/runtime-share-addresses.d.ts +21 -0
  76. package/dist/runtime-share-addresses.js +50 -0
  77. package/dist/session-browser-capability-store.d.ts +13 -0
  78. package/dist/session-browser-capability-store.js +58 -0
  79. package/dist/session-emulator-binding-store.d.ts +10 -0
  80. package/dist/session-emulator-binding-store.js +61 -0
  81. package/dist/session-launch-operations.d.ts +41 -0
  82. package/dist/session-launch-operations.js +124 -0
  83. package/dist/session-meta-store.d.ts +3 -2
  84. package/dist/session-meta-store.js +57 -7
  85. package/dist/setup.js +9 -4
  86. package/dist/skills-catalog.js +8 -4
  87. package/dist/update.d.ts +11 -2
  88. package/dist/update.js +0 -27
  89. package/package.json +18 -10
@@ -1,23 +1,10 @@
1
- import { type ChannelInstanceDescriptor, type ChannelTypeDescriptor, type RynxPlugin } from "@rynx-ai/core";
2
- import { type PluginRecord } from "./plugin-store.js";
3
- /** Directory npm-installed plugins live under (override via `RYNX_PLUGINS_DIR`). */
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
- /** Absolute package directory a plugin record resolves to. */
9
- export declare function pluginPkgDir(rec: Pick<PluginRecord, "source" | "spec">): string;
10
- /** Import a package directory's `plugin` manifest export (or null if absent). */
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
- * Plugin loader + channel-type catalog.
2
+ * Static plugin catalog.
3
3
  *
4
- * There are no built-in channels: the daemon imports whatever plugins are
5
- * recorded (enabled) in the `plugins` table, reads each one's `plugin` manifest,
6
- * and collects the channel types they contribute into an in-memory catalog.
7
- * `loadPlugins()` (re)builds the catalog from the registry; everything else
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
- import { listEnabledPlugins } from "./plugin-store.js";
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
- /** Resolve a plugin record to the file URL of its entry module. */
31
- function entryUrl(pkgDir) {
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 rec of listEnabledPlugins()) {
71
- let plugin;
72
- try {
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
- for (const channel of plugin.channels ?? []) {
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 d of listEnabledDescriptors()) {
100
- const descriptor = catalog.get(d.type);
101
- if (!descriptor) {
102
- warn(`unknown channel type "${d.type}" (instance "${d.instanceId}") — no installed plugin provides it; skipped`);
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: d.instanceId,
107
- type: d.type,
108
- factory: descriptor.factory,
109
- options: d.options,
110
- agent: d.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
+ }