@tokensmind/agent-network 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/openclaw.plugin.json +32 -0
- package/package.json +55 -0
- package/skills/tokensmind-agent-network-runtime/SKILL.md +55 -0
- package/skills/tokensmind-agent-network-runtime/scripts/action_executor.py +112 -0
- package/skills/tokensmind-agent-network-runtime/scripts/action_support.py +106 -0
- package/skills/tokensmind-agent-network-runtime/scripts/action_validation.py +38 -0
- package/skills/tokensmind-agent-network-runtime/scripts/agent-network-runtime.mjs +54 -0
- package/skills/tokensmind-agent-network-runtime/scripts/agent_network_runtime.py +236 -0
- package/skills/tokensmind-agent-network-runtime/scripts/governance_actions.py +57 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/action-context.js +23 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/action-errors.js +55 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/action-executor.js +126 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/action-validation.js +61 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/agent-actions.js +44 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/api-client.js +76 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/contact-action.js +154 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/governance-actions.js +66 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/messaging-actions.js +60 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/browser.js +26 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/connector.js +204 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/constants.js +10 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/credentialStore.js +162 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/crypto.js +25 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/deviceAuthorization.js +194 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/httpClient.js +54 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/portableStore.js +193 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/requestPolicy.js +55 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/systemCredentialStore.js +176 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/workflow-store.js +77 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/__init__.py +1 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/browser.py +27 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/connector.py +156 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/constants.py +12 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/credential_store.py +135 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/crypto.py +28 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/device_authorization.py +165 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/errors.py +9 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/http_client.py +50 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/portable_store.py +195 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/request_policy.py +85 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/system_credential_store.py +143 -0
- package/src/action-context.js +23 -0
- package/src/action-errors.js +55 -0
- package/src/action-executor.js +126 -0
- package/src/action-validation.js +61 -0
- package/src/agent-actions.js +44 -0
- package/src/api-client.js +76 -0
- package/src/contact-action.js +154 -0
- package/src/governance-actions.js +66 -0
- package/src/index.js +70 -0
- package/src/messaging-actions.js +60 -0
- package/src/runtime/browser.js +26 -0
- package/src/runtime/connector.js +204 -0
- package/src/runtime/constants.js +10 -0
- package/src/runtime/credentialStore.js +162 -0
- package/src/runtime/crypto.js +25 -0
- package/src/runtime/deviceAuthorization.js +194 -0
- package/src/runtime/httpClient.js +54 -0
- package/src/runtime/portableStore.js +193 -0
- package/src/runtime/requestPolicy.js +55 -0
- package/src/runtime/systemCredentialStore.js +176 -0
- package/src/workflow-store.js +77 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { promises as defaultFs } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const DIRECTORY_MODE = 0o700;
|
|
7
|
+
const FILE_MODE = 0o600;
|
|
8
|
+
const RECORD_NAMES = new Set(['active', 'pending', 'instance']);
|
|
9
|
+
const PENDING_PHASES = new Set(['prepared', 'authorizing', 'authorized']);
|
|
10
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
11
|
+
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
12
|
+
const DEVICE_CODE_PATTERN = /^tm_device_[A-Za-z0-9_-]{43}$/;
|
|
13
|
+
const AGENT_TOKEN_PATTERN = /^tm_agent_[A-Za-z0-9_-]{43}$/;
|
|
14
|
+
|
|
15
|
+
function requireHomeDirectory(homeDir) {
|
|
16
|
+
const value = String(homeDir || '').trim();
|
|
17
|
+
if (!value) throw new Error('Unable to resolve the current user home directory');
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolvePortableStateDir({
|
|
22
|
+
platform = process.platform,
|
|
23
|
+
homeDir = os.homedir(),
|
|
24
|
+
} = {}) {
|
|
25
|
+
const pathApi = platform === 'win32' ? path.win32 : path;
|
|
26
|
+
return pathApi.join(requireHomeDirectory(homeDir), '.tokensmind', 'agent-network');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isObject(value) {
|
|
30
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isNonEmptyString(value) {
|
|
34
|
+
return typeof value === 'string' && value.length > 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isOperation(value) {
|
|
38
|
+
return isObject(value)
|
|
39
|
+
&& ['GET', 'POST', 'PATCH', 'DELETE'].includes(value.method)
|
|
40
|
+
&& isNonEmptyString(value.path);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isInstance(value) {
|
|
44
|
+
return isObject(value) && UUID_PATTERN.test(value.id);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isActive(value) {
|
|
48
|
+
return isObject(value)
|
|
49
|
+
&& AGENT_TOKEN_PATTERN.test(value.token)
|
|
50
|
+
&& (value.agentId === null || isNonEmptyString(value.agentId))
|
|
51
|
+
&& isNonEmptyString(value.credentialId)
|
|
52
|
+
&& isNonEmptyString(value.tokenPrefix)
|
|
53
|
+
&& value.token.startsWith(value.tokenPrefix);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasPendingSecrets(value) {
|
|
57
|
+
return UUID_PATTERN.test(value.authorizationId)
|
|
58
|
+
&& UUID_PATTERN.test(value.instanceId)
|
|
59
|
+
&& UUID_PATTERN.test(value.createIdempotencyKey)
|
|
60
|
+
&& UUID_PATTERN.test(value.exchangeIdempotencyKey)
|
|
61
|
+
&& DEVICE_CODE_PATTERN.test(value.deviceCode)
|
|
62
|
+
&& SHA256_PATTERN.test(value.deviceCodeHash)
|
|
63
|
+
&& AGENT_TOKEN_PATTERN.test(value.token)
|
|
64
|
+
&& SHA256_PATTERN.test(value.tokenHash)
|
|
65
|
+
&& isNonEmptyString(value.tokenPrefix)
|
|
66
|
+
&& value.token.startsWith(value.tokenPrefix);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function hasAuthorization(value) {
|
|
70
|
+
return isObject(value.authorization)
|
|
71
|
+
&& value.authorization.authorizationId === value.authorizationId
|
|
72
|
+
&& isNonEmptyString(value.authorization.verificationUrl)
|
|
73
|
+
&& Number.isFinite(Date.parse(value.authorization.expiresAt))
|
|
74
|
+
&& Number.isInteger(value.authorization.intervalSeconds)
|
|
75
|
+
&& value.authorization.intervalSeconds > 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isPending(value) {
|
|
79
|
+
if (!isObject(value) || !PENDING_PHASES.has(value.phase)) return false;
|
|
80
|
+
if (!isOperation(value.operation) || !hasPendingSecrets(value)) return false;
|
|
81
|
+
return value.phase === 'prepared' || hasAuthorization(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function validatePortableRecord(name, value) {
|
|
85
|
+
const valid = name === 'active'
|
|
86
|
+
? isActive(value)
|
|
87
|
+
: name === 'pending'
|
|
88
|
+
? isPending(value)
|
|
89
|
+
: isInstance(value);
|
|
90
|
+
if (!valid) throw new Error(`Portable Agent Network ${name} state has an invalid structure`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validateRecordName(name) {
|
|
94
|
+
if (!RECORD_NAMES.has(name)) throw new Error(`Unsupported portable state record: ${name}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function originNamespace(origin) {
|
|
98
|
+
const url = new URL(origin);
|
|
99
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.origin !== origin) {
|
|
100
|
+
throw new Error('Portable Agent Network store requires a normalized HTTP origin');
|
|
101
|
+
}
|
|
102
|
+
return createHash('sha256').update(origin, 'utf8').digest('hex');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isMissing(error) {
|
|
106
|
+
return error?.code === 'ENOENT';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function ensurePrivateDirectory({ fs, directory, platform }) {
|
|
110
|
+
await fs.mkdir(directory, { recursive: true, mode: DIRECTORY_MODE });
|
|
111
|
+
if (platform !== 'win32') await fs.chmod(directory, DIRECTORY_MODE);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function removeTemporaryFile(fs, filePath) {
|
|
115
|
+
try {
|
|
116
|
+
await fs.unlink(filePath);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (!isMissing(error)) throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function createReadOperation({ fs, recordPath }) {
|
|
123
|
+
return async function read(name) {
|
|
124
|
+
validateRecordName(name);
|
|
125
|
+
let source;
|
|
126
|
+
try {
|
|
127
|
+
source = await fs.readFile(recordPath(name), 'utf8');
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (isMissing(error)) return null;
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
let value;
|
|
133
|
+
try {
|
|
134
|
+
value = JSON.parse(source);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
throw new Error(`Portable Agent Network ${name} state is invalid JSON`, { cause: error });
|
|
137
|
+
}
|
|
138
|
+
validatePortableRecord(name, value);
|
|
139
|
+
return value;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function createWriteOperation({ fs, directory, platform, randomId, recordPath }) {
|
|
144
|
+
return async function write(name, value) {
|
|
145
|
+
validateRecordName(name);
|
|
146
|
+
validatePortableRecord(name, value);
|
|
147
|
+
await ensurePrivateDirectory({ fs, directory, platform });
|
|
148
|
+
const target = recordPath(name);
|
|
149
|
+
const temporary = path.join(directory, `.${name}.${process.pid}.${randomId()}.tmp`);
|
|
150
|
+
try {
|
|
151
|
+
await fs.writeFile(temporary, JSON.stringify(value), {
|
|
152
|
+
encoding: 'utf8', mode: FILE_MODE, flag: 'wx',
|
|
153
|
+
});
|
|
154
|
+
if (platform !== 'win32') await fs.chmod(temporary, FILE_MODE);
|
|
155
|
+
await fs.rename(temporary, target);
|
|
156
|
+
if (platform !== 'win32') await fs.chmod(target, FILE_MODE);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
await removeTemporaryFile(fs, temporary);
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createRemoveOperation({ fs, recordPath }) {
|
|
165
|
+
return async function remove(name) {
|
|
166
|
+
validateRecordName(name);
|
|
167
|
+
try {
|
|
168
|
+
await fs.unlink(recordPath(name));
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (!isMissing(error)) throw error;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function createPortableStore({
|
|
176
|
+
stateDir,
|
|
177
|
+
origin,
|
|
178
|
+
platform = process.platform,
|
|
179
|
+
env = process.env,
|
|
180
|
+
homeDir = os.homedir(),
|
|
181
|
+
fs = defaultFs,
|
|
182
|
+
randomId = randomUUID,
|
|
183
|
+
} = {}) {
|
|
184
|
+
const baseDirectory = path.resolve(stateDir || resolvePortableStateDir({ platform, env, homeDir }));
|
|
185
|
+
const directory = path.join(baseDirectory, originNamespace(origin));
|
|
186
|
+
const recordPath = (name) => path.join(directory, `${name}.json`);
|
|
187
|
+
const context = { fs, directory, platform, randomId, recordPath };
|
|
188
|
+
return {
|
|
189
|
+
read: createReadOperation(context),
|
|
190
|
+
write: createWriteOperation(context),
|
|
191
|
+
remove: createRemoveOperation(context),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { INTERNAL_PATH_PATTERNS, MUTATION_METHODS } from './constants.js';
|
|
2
|
+
|
|
3
|
+
const SUPPORTED_METHODS = new Set(['GET', 'POST', 'PATCH', 'DELETE']);
|
|
4
|
+
const PUBLIC_AGENT_DIRECTORY_PATH = '/agent-network-api/agents';
|
|
5
|
+
|
|
6
|
+
export function normalizeBaseUrl(value) {
|
|
7
|
+
const url = new URL(value);
|
|
8
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
9
|
+
throw new Error('Agent Network baseUrl must use http or https');
|
|
10
|
+
}
|
|
11
|
+
url.pathname = '/';
|
|
12
|
+
url.search = '';
|
|
13
|
+
url.hash = '';
|
|
14
|
+
return url.origin;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function validateBusinessRequest({ method, path, idempotencyKey, reauthorize }) {
|
|
18
|
+
const normalizedMethod = String(method).toUpperCase();
|
|
19
|
+
if (!SUPPORTED_METHODS.has(normalizedMethod)) {
|
|
20
|
+
throw new Error(`Unsupported Agent Network method: ${normalizedMethod}`);
|
|
21
|
+
}
|
|
22
|
+
if (!path.startsWith('/agent-network-api/')) {
|
|
23
|
+
throw new Error('Agent Network path must start with /agent-network-api/');
|
|
24
|
+
}
|
|
25
|
+
if (INTERNAL_PATH_PATTERNS.some((pattern) => pattern.test(path))) {
|
|
26
|
+
throw new Error('This endpoint is not available through the ordinary-user connector');
|
|
27
|
+
}
|
|
28
|
+
if (MUTATION_METHODS.has(normalizedMethod) && !idempotencyKey?.trim()) {
|
|
29
|
+
throw new Error('Mutating Agent Network requests require a stable Idempotency-Key');
|
|
30
|
+
}
|
|
31
|
+
if (reauthorize !== undefined && typeof reauthorize !== 'boolean') {
|
|
32
|
+
throw new Error('Agent Network reauthorize must be a boolean');
|
|
33
|
+
}
|
|
34
|
+
return { method: normalizedMethod, path, idempotencyKey: idempotencyKey?.trim() };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildBusinessUrl(baseUrl, path) {
|
|
38
|
+
const url = new URL(path, `${baseUrl}/`);
|
|
39
|
+
if (url.origin !== baseUrl || !url.pathname.startsWith('/agent-network-api/')) {
|
|
40
|
+
throw new Error('Agent Network request must remain on the configured origin');
|
|
41
|
+
}
|
|
42
|
+
if (INTERNAL_PATH_PATTERNS.some((pattern) => pattern.test(url.pathname))) {
|
|
43
|
+
throw new Error('This endpoint is not available through the ordinary-user connector');
|
|
44
|
+
}
|
|
45
|
+
return url.toString();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isPublicDiscoveryRequest({ method, path }) {
|
|
49
|
+
if (method !== 'GET') return false;
|
|
50
|
+
const url = new URL(path, 'https://tokensmind.invalid');
|
|
51
|
+
const mineRequested = [...url.searchParams.entries()]
|
|
52
|
+
.some(([key, value]) => key === 'mine' && value === '1');
|
|
53
|
+
return url.pathname === PUBLIC_AGENT_DIRECTORY_PATH
|
|
54
|
+
&& !mineRequested;
|
|
55
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { spawn as defaultSpawn } from 'node:child_process';
|
|
2
|
+
import { constants as fsConstants, promises as defaultFs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { validatePortableRecord } from './portableStore.js';
|
|
5
|
+
|
|
6
|
+
const SERVICE_NAME = 'TokensMind Agent Network';
|
|
7
|
+
const APPLICATION_NAME = 'tokensmind-agent-network';
|
|
8
|
+
const RECORD_NAMES = new Set(['active', 'pending', 'instance']);
|
|
9
|
+
const MACOS_SECURITY_PATH = '/usr/bin/security';
|
|
10
|
+
const MACOS_ITEM_NOT_FOUND = 44;
|
|
11
|
+
|
|
12
|
+
function validateRecordName(name) {
|
|
13
|
+
if (!RECORD_NAMES.has(name)) throw new Error(`Unsupported portable state record: ${name}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function credentialAccount(namespace, name) {
|
|
17
|
+
validateRecordName(name);
|
|
18
|
+
return `${namespace}:${name}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseRecord(name, source) {
|
|
22
|
+
let value;
|
|
23
|
+
try {
|
|
24
|
+
value = JSON.parse(source);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new Error(`System Agent Network ${name} credential is invalid JSON`, { cause: error });
|
|
27
|
+
}
|
|
28
|
+
validatePortableRecord(name, value);
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function commandFailure(backend, operation, result) {
|
|
33
|
+
return new Error(
|
|
34
|
+
`${backend} ${operation} failed with exit code ${String(result.code)}`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createCommandRunner({ spawnImpl = defaultSpawn } = {}) {
|
|
39
|
+
return ({ command, args, input = '' }) => new Promise((resolve, reject) => {
|
|
40
|
+
const child = spawnImpl(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
41
|
+
const stdout = [];
|
|
42
|
+
const stderr = [];
|
|
43
|
+
let settled = false;
|
|
44
|
+
child.stdout.on('data', (chunk) => stdout.push(chunk));
|
|
45
|
+
child.stderr.on('data', (chunk) => stderr.push(chunk));
|
|
46
|
+
child.stdin.on('error', (error) => {
|
|
47
|
+
if (error.code === 'EPIPE' || settled) return;
|
|
48
|
+
settled = true;
|
|
49
|
+
reject(error);
|
|
50
|
+
});
|
|
51
|
+
child.on('error', (error) => {
|
|
52
|
+
if (settled) return;
|
|
53
|
+
settled = true;
|
|
54
|
+
reject(error);
|
|
55
|
+
});
|
|
56
|
+
child.on('close', (code) => {
|
|
57
|
+
if (settled) return;
|
|
58
|
+
resolve({
|
|
59
|
+
code,
|
|
60
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
61
|
+
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
child.stdin.end(input);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createMacOperations({ namespace, run }) {
|
|
69
|
+
return {
|
|
70
|
+
async read(name) {
|
|
71
|
+
const account = credentialAccount(namespace, name);
|
|
72
|
+
const result = await run({
|
|
73
|
+
command: MACOS_SECURITY_PATH,
|
|
74
|
+
args: ['find-generic-password', '-a', account, '-s', SERVICE_NAME, '-w'],
|
|
75
|
+
});
|
|
76
|
+
if (result.code === MACOS_ITEM_NOT_FOUND) return null;
|
|
77
|
+
if (result.code !== 0) throw commandFailure('macOS Keychain', 'read', result);
|
|
78
|
+
return parseRecord(name, result.stdout.trim());
|
|
79
|
+
},
|
|
80
|
+
async write(name, value) {
|
|
81
|
+
validateRecordName(name);
|
|
82
|
+
validatePortableRecord(name, value);
|
|
83
|
+
const account = credentialAccount(namespace, name);
|
|
84
|
+
const result = await run({
|
|
85
|
+
command: MACOS_SECURITY_PATH,
|
|
86
|
+
args: ['add-generic-password', '-a', account, '-s', SERVICE_NAME, '-U', '-w'],
|
|
87
|
+
input: `${JSON.stringify(value)}\n`,
|
|
88
|
+
});
|
|
89
|
+
if (result.code !== 0) throw commandFailure('macOS Keychain', 'write', result);
|
|
90
|
+
},
|
|
91
|
+
async remove(name) {
|
|
92
|
+
const account = credentialAccount(namespace, name);
|
|
93
|
+
const result = await run({
|
|
94
|
+
command: MACOS_SECURITY_PATH,
|
|
95
|
+
args: ['delete-generic-password', '-a', account, '-s', SERVICE_NAME],
|
|
96
|
+
});
|
|
97
|
+
if (![0, MACOS_ITEM_NOT_FOUND].includes(result.code)) {
|
|
98
|
+
throw commandFailure('macOS Keychain', 'remove', result);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function linuxAttributes(namespace, name) {
|
|
105
|
+
validateRecordName(name);
|
|
106
|
+
return ['application', APPLICATION_NAME, 'origin', namespace, 'record', name];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createLinuxOperations({ namespace, command, run }) {
|
|
110
|
+
async function lookup(name) {
|
|
111
|
+
const result = await run({
|
|
112
|
+
command,
|
|
113
|
+
args: ['lookup', ...linuxAttributes(namespace, name)],
|
|
114
|
+
});
|
|
115
|
+
const missing = result.code === 1 && !result.stdout.trim() && !result.stderr.trim();
|
|
116
|
+
if (missing) return null;
|
|
117
|
+
if (result.code !== 0) throw commandFailure('Linux Secret Service', 'read', result);
|
|
118
|
+
return parseRecord(name, result.stdout.trim());
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
read: lookup,
|
|
122
|
+
async write(name, value) {
|
|
123
|
+
validatePortableRecord(name, value);
|
|
124
|
+
const result = await run({
|
|
125
|
+
command,
|
|
126
|
+
args: ['store', `--label=${SERVICE_NAME}`, ...linuxAttributes(namespace, name)],
|
|
127
|
+
input: `${JSON.stringify(value)}\n`,
|
|
128
|
+
});
|
|
129
|
+
if (result.code !== 0) throw commandFailure('Linux Secret Service', 'write', result);
|
|
130
|
+
},
|
|
131
|
+
async remove(name) {
|
|
132
|
+
if (await lookup(name) === null) return;
|
|
133
|
+
const result = await run({
|
|
134
|
+
command,
|
|
135
|
+
args: ['clear', ...linuxAttributes(namespace, name)],
|
|
136
|
+
});
|
|
137
|
+
if (result.code !== 0) throw commandFailure('Linux Secret Service', 'remove', result);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function isExecutable(fs, filePath) {
|
|
143
|
+
try {
|
|
144
|
+
await fs.access(filePath, fsConstants.X_OK);
|
|
145
|
+
return true;
|
|
146
|
+
} catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function findExecutable({ name, env, fs }) {
|
|
152
|
+
const directories = String(env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
153
|
+
for (const directory of directories) {
|
|
154
|
+
const candidate = path.join(directory, name);
|
|
155
|
+
if (await isExecutable(fs, candidate)) return candidate;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function resolveSystemCredentialStore({
|
|
161
|
+
platform,
|
|
162
|
+
namespace,
|
|
163
|
+
env = process.env,
|
|
164
|
+
fs = defaultFs,
|
|
165
|
+
run = createCommandRunner(),
|
|
166
|
+
} = {}) {
|
|
167
|
+
if (platform === 'darwin') {
|
|
168
|
+
if (!await isExecutable(fs, MACOS_SECURITY_PATH)) return null;
|
|
169
|
+
return createMacOperations({ namespace, run });
|
|
170
|
+
}
|
|
171
|
+
if (platform === 'linux') {
|
|
172
|
+
const command = await findExecutable({ name: 'secret-tool', env, fs });
|
|
173
|
+
return command ? createLinuxOperations({ namespace, command, run }) : null;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { promises as defaultFs } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { originNamespace, resolvePortableStateDir } from './runtime/portableStore.js';
|
|
6
|
+
|
|
7
|
+
const DIRECTORY_MODE = 0o700;
|
|
8
|
+
const FILE_MODE = 0o600;
|
|
9
|
+
|
|
10
|
+
function recordPath({ stateDir, origin, platform, homeDir }) {
|
|
11
|
+
const base = stateDir || resolvePortableStateDir({ platform, homeDir });
|
|
12
|
+
return path.join(path.resolve(base), originNamespace(origin), 'workflow.json');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function ensureDirectory(fs, filePath, platform) {
|
|
16
|
+
const directory = path.dirname(filePath);
|
|
17
|
+
await fs.mkdir(directory, { recursive: true, mode: DIRECTORY_MODE });
|
|
18
|
+
if (platform !== 'win32') await fs.chmod(directory, DIRECTORY_MODE);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function writeCandidate({ fs, filePath, platform, value }) {
|
|
22
|
+
const temporary = path.join(path.dirname(filePath), `.${process.pid}.${randomUUID()}.tmp`);
|
|
23
|
+
try {
|
|
24
|
+
await fs.writeFile(temporary, JSON.stringify(value), {
|
|
25
|
+
encoding: 'utf8', mode: FILE_MODE, flag: 'wx',
|
|
26
|
+
});
|
|
27
|
+
if (platform !== 'win32') await fs.chmod(temporary, FILE_MODE);
|
|
28
|
+
try {
|
|
29
|
+
await fs.link(temporary, filePath);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
32
|
+
}
|
|
33
|
+
} finally {
|
|
34
|
+
try {
|
|
35
|
+
await fs.unlink(temporary);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createWorkflowStore({
|
|
43
|
+
stateDir,
|
|
44
|
+
origin,
|
|
45
|
+
platform = process.platform,
|
|
46
|
+
homeDir = os.homedir(),
|
|
47
|
+
fs = defaultFs,
|
|
48
|
+
} = {}) {
|
|
49
|
+
const filePath = recordPath({ stateDir, origin, platform, homeDir });
|
|
50
|
+
return {
|
|
51
|
+
async read(name = 'workflow') {
|
|
52
|
+
if (name !== 'workflow') throw new Error(`Unsupported workflow record: ${name}`);
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error?.code === 'ENOENT') return null;
|
|
57
|
+
if (error instanceof SyntaxError) throw new Error('Workflow state is invalid JSON', { cause: error });
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
async write(name, value) {
|
|
62
|
+
if (name !== 'workflow') throw new Error(`Unsupported workflow record: ${name}`);
|
|
63
|
+
await ensureDirectory(fs, filePath, platform);
|
|
64
|
+
await writeCandidate({ fs, filePath, platform, value });
|
|
65
|
+
},
|
|
66
|
+
async remove(name = 'workflow') {
|
|
67
|
+
if (name !== 'workflow') throw new Error(`Unsupported workflow record: ${name}`);
|
|
68
|
+
try {
|
|
69
|
+
await fs.unlink(filePath);
|
|
70
|
+
return true;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error?.code === 'ENOENT') return false;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|