@falari/harness 0.0.0-stage → 0.8.10
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/CHANGELOG.md +456 -0
- package/LICENSE +21 -0
- package/README.md +219 -2
- package/client.js +3303 -0
- package/cordis.patch.yml +6 -0
- package/icon.png +0 -0
- package/index.js +52 -0
- package/lib/config.js +103 -0
- package/lib/errors.js +40 -0
- package/lib/falari-sdk.cjs +39775 -0
- package/lib/pin.js +126 -0
- package/lib/routes.js +311 -0
- package/lib/sdk.js +13 -0
- package/lib/service.js +1429 -0
- package/lib/tools.js +193 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +61 -4
package/cordis.patch.yml
ADDED
package/icon.png
ADDED
|
Binary file
|
package/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half of the Falari Harness bundle.
|
|
3
|
+
*
|
|
4
|
+
* M2 scope: Agent Key status, upload quotes, the SDK-backed upload queue and
|
|
5
|
+
* its panel routes. Downloads arrive in M3.
|
|
6
|
+
*/
|
|
7
|
+
import { resolveConfig } from './lib/config.js';
|
|
8
|
+
import { registerRoutes } from './lib/routes.js';
|
|
9
|
+
import { FalariDataService } from './lib/service.js';
|
|
10
|
+
import { registerTools } from './lib/tools.js';
|
|
11
|
+
|
|
12
|
+
const API_PREFIX = '/api/falari-harness';
|
|
13
|
+
|
|
14
|
+
export const inject = ['tools', 'credentials'];
|
|
15
|
+
|
|
16
|
+
export function apply(ctx, config) {
|
|
17
|
+
const resolved = resolveConfig(config);
|
|
18
|
+
const service = new FalariDataService(ctx, resolved, ctx.credentials);
|
|
19
|
+
|
|
20
|
+
ctx.effect(() => () => service.dispose(), 'falari-harness: service');
|
|
21
|
+
ctx.effect(() => {
|
|
22
|
+
service.start().catch((error) => {
|
|
23
|
+
ctx.logger?.warn?.(`falari-harness: startup failed: ${error && error.message ? error.message : error}`);
|
|
24
|
+
});
|
|
25
|
+
return () => {};
|
|
26
|
+
}, 'falari-harness: startup');
|
|
27
|
+
|
|
28
|
+
// Tell the Settings service that this plugin ships its own configuration
|
|
29
|
+
// page, so no schema-generated form is offered for it. Optional: profiles
|
|
30
|
+
// without the Settings service keep the business plugin working.
|
|
31
|
+
if (typeof ctx.inject === 'function') {
|
|
32
|
+
try {
|
|
33
|
+
ctx.inject(['settings'], (child) => {
|
|
34
|
+
const settings = child.settings;
|
|
35
|
+
if (!settings || typeof settings.configure !== 'function') return;
|
|
36
|
+
child.effect(() => {
|
|
37
|
+
try {
|
|
38
|
+
return settings.configure({ auto: false }, ctx.fiber);
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}, 'falari-harness: own config page');
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
ctx.logger?.debug?.(`falari-harness: settings policy skipped: ${error && error.message ? error.message : error}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
registerTools(ctx, service);
|
|
50
|
+
registerRoutes(ctx, service, resolved, API_PREFIX);
|
|
51
|
+
ctx.logger?.debug?.(`falari-harness: host ready on ${resolved.network}`);
|
|
52
|
+
}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Plugin configuration resolution. Secrets never live here — see lib/service.js. */
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_TIMEOUT_MS = 60_000;
|
|
6
|
+
export const DEFAULT_MAX_FILE_SIZE = 2 * 1024 ** 3;
|
|
7
|
+
const DEFAULT_LOCK_IDLE_MINUTES = 60;
|
|
8
|
+
export const DEFAULT_DURATION = 'permanent';
|
|
9
|
+
|
|
10
|
+
export const DURATION_NAMES = ['permanent', '30d', '90d', '1y', '5y', '10y'];
|
|
11
|
+
|
|
12
|
+
export function defaultDataDir() {
|
|
13
|
+
const profile = process.env.DSH_PROFILE_DIR;
|
|
14
|
+
return join(profile || join(homedir(), '.dsh', 'profiles', 'desktop'), 'falari-harness');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function text(value) {
|
|
18
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function nonNegativeNumber(value, fallback) {
|
|
22
|
+
const number = Number(value);
|
|
23
|
+
if (!Number.isFinite(number) || number < 0) return fallback;
|
|
24
|
+
return Math.trunc(number);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function positiveNumber(value, fallback) {
|
|
28
|
+
return Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallback;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const NETWORKS = ['testnet', 'mainnet'];
|
|
32
|
+
const PRIVACIES = ['public', 'private'];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate an edit coming from the settings page. Only fields the panel owns are
|
|
36
|
+
* accepted; unknown keys are refused instead of silently dropped.
|
|
37
|
+
*/
|
|
38
|
+
export function validateConfigPatch(patch) {
|
|
39
|
+
const input = patch && typeof patch === 'object' ? patch : {};
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [key, value] of Object.entries(input)) {
|
|
42
|
+
if (key === 'network') {
|
|
43
|
+
if (!NETWORKS.includes(value)) throw new Error('network must be testnet or mainnet');
|
|
44
|
+
out.network = value;
|
|
45
|
+
} else if (key === 'defaultPrivacy') {
|
|
46
|
+
if (!PRIVACIES.includes(value)) throw new Error('defaultPrivacy must be public or private');
|
|
47
|
+
out.defaultPrivacy = value;
|
|
48
|
+
} else if (key === 'defaultDuration') {
|
|
49
|
+
if (!DURATION_NAMES.includes(value)) throw new Error(`defaultDuration must be one of ${DURATION_NAMES.join(', ')}`);
|
|
50
|
+
out.defaultDuration = value;
|
|
51
|
+
} else if (key === 'maxFileSizeBytes') {
|
|
52
|
+
const size = Number(value);
|
|
53
|
+
if (!Number.isSafeInteger(size) || size < 1024) throw new Error('maxFileSizeBytes must be an integer of at least 1024');
|
|
54
|
+
out.maxFileSizeBytes = size;
|
|
55
|
+
} else if (key === 'lockIdleMinutes') {
|
|
56
|
+
const minutes = Number(value);
|
|
57
|
+
if (!Number.isInteger(minutes) || minutes < 0 || minutes > 7 * 24 * 60) {
|
|
58
|
+
throw new Error('lockIdleMinutes must be an integer between 0 and 10080 (7 days)');
|
|
59
|
+
}
|
|
60
|
+
out.lockIdleMinutes = minutes;
|
|
61
|
+
} else if (key === 'timeoutMs') {
|
|
62
|
+
const ms = Number(value);
|
|
63
|
+
if (!Number.isSafeInteger(ms) || ms < 1000 || ms > 600000) throw new Error('timeoutMs must be between 1000 and 600000');
|
|
64
|
+
out.timeoutMs = ms;
|
|
65
|
+
} else if (key === 'downloadDir') {
|
|
66
|
+
if (value === null || value === '') out.downloadDir = '';
|
|
67
|
+
else if (typeof value === 'string' && value.startsWith('/')) out.downloadDir = value.trim();
|
|
68
|
+
else throw new Error('downloadDir must be an absolute path or empty');
|
|
69
|
+
} else if (key === 'gatewayUrl' || key === 'retrievalUrl' || key === 'explorerUrl') {
|
|
70
|
+
if (value === null || value === '') out[key] = '';
|
|
71
|
+
else if (typeof value === 'string' && /^https?:\/\//.test(value)) out[key] = value.trim();
|
|
72
|
+
else throw new Error(`${key} must be an http(s) URL or empty`);
|
|
73
|
+
} else if (key === 'allowUploadOutsideWorkspace' || key === 'allowRemoteRoutes') {
|
|
74
|
+
if (typeof value !== 'boolean') throw new Error(`${key} must be a boolean`);
|
|
75
|
+
out[key] = value;
|
|
76
|
+
} else {
|
|
77
|
+
throw new Error(`unsupported configuration field: ${key}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resolveConfig(input) {
|
|
84
|
+
const config = input && typeof input === 'object' ? input : {};
|
|
85
|
+
const privacy = config.defaultPrivacy === 'private' ? 'private' : 'public';
|
|
86
|
+
const duration = DURATION_NAMES.includes(config.defaultDuration) ? config.defaultDuration : DEFAULT_DURATION;
|
|
87
|
+
return {
|
|
88
|
+
network: config.network === 'mainnet' ? 'mainnet' : 'testnet',
|
|
89
|
+
gatewayUrl: text(config.gatewayUrl),
|
|
90
|
+
retrievalUrl: text(config.retrievalUrl),
|
|
91
|
+
explorerUrl: text(config.explorerUrl),
|
|
92
|
+
timeoutMs: positiveNumber(config.timeoutMs, DEFAULT_TIMEOUT_MS),
|
|
93
|
+
dataDir: text(config.dataDir) || defaultDataDir(),
|
|
94
|
+
downloadDir: text(config.downloadDir),
|
|
95
|
+
defaultPrivacy: privacy,
|
|
96
|
+
defaultDuration: duration,
|
|
97
|
+
maxFileSizeBytes: positiveNumber(config.maxFileSizeBytes, DEFAULT_MAX_FILE_SIZE),
|
|
98
|
+
// Minutes an unlocked Agent Key stays in memory; 0 keeps it until lock/exit.
|
|
99
|
+
lockIdleMinutes: nonNegativeNumber(config.lockIdleMinutes, DEFAULT_LOCK_IDLE_MINUTES),
|
|
100
|
+
allowUploadOutsideWorkspace: config.allowUploadOutsideWorkspace === true,
|
|
101
|
+
allowRemoteRoutes: config.allowRemoteRoutes === true,
|
|
102
|
+
};
|
|
103
|
+
}
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Error shape shared by the tools and the HTTP routes. */
|
|
2
|
+
import { sdk } from './sdk.js';
|
|
3
|
+
|
|
4
|
+
export class ServiceError extends Error {
|
|
5
|
+
constructor(code, message, hint = '') {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'ServiceError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.hint = hint;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function errorText(error) {
|
|
14
|
+
const message = error && typeof error.message === 'string' ? error.message : String(error);
|
|
15
|
+
return message.trim() || 'unknown error';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function publicError(error) {
|
|
19
|
+
if (error instanceof ServiceError) {
|
|
20
|
+
return { code: error.code, message: error.message, hint: error.hint || undefined };
|
|
21
|
+
}
|
|
22
|
+
if (sdk.FalariError && error instanceof sdk.FalariError) {
|
|
23
|
+
return { code: error.code || 'falari_error', message: error.message, hint: error.hint || undefined };
|
|
24
|
+
}
|
|
25
|
+
if (sdk.ApiError && error instanceof sdk.ApiError) {
|
|
26
|
+
if (error.status === 404 && /account .* does not exist/i.test(error.message)) {
|
|
27
|
+
return {
|
|
28
|
+
code: 'master_account_not_found',
|
|
29
|
+
message: error.message,
|
|
30
|
+
hint: 'The key belongs to a master account that does not exist on this network. Check the network and the wallet that exported the key.',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
code: error.code || (error.status === 404 ? 'not_found' : 'gateway_error'),
|
|
35
|
+
message: error.message,
|
|
36
|
+
hint: error.status === 401 || error.status === 403 ? 'The gateway rejected this request.' : undefined,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return { code: 'internal_error', message: errorText(error) };
|
|
40
|
+
}
|