aptly 0.1.0 → 0.1.1
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.en.md +21 -5
- package/README.ja.md +21 -5
- package/README.md +21 -5
- package/README.zh-CN.md +21 -5
- package/dist/bin/aptly.js +57 -5
- package/dist/src/cli-input.d.ts +5 -0
- package/dist/src/cli-input.js +49 -0
- package/dist/src/core.js +9 -0
- package/dist/src/credential-setup.d.ts +14 -0
- package/dist/src/credential-setup.js +56 -0
- package/dist/src/files.js +10 -3
- package/dist/src/provider.js +6 -3
- package/dist/src/runtime.d.ts +5 -1
- package/dist/src/runtime.js +19 -3
- package/dist/src/secret-store.d.ts +27 -0
- package/dist/src/secret-store.js +263 -0
- package/dist/src/server.js +2 -1
- package/dist/src/setup.d.ts +5 -0
- package/dist/src/setup.js +201 -22
- package/dist/src/types.d.ts +4 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/windows-powershell.d.ts +2 -0
- package/dist/src/windows-powershell.js +11 -0
- package/dist/src/windows-private.d.ts +11 -0
- package/dist/src/windows-private.js +98 -0
- package/docs/guide/en.md +105 -15
- package/docs/guide/ja.md +105 -15
- package/docs/guide/ko.md +105 -15
- package/docs/guide/zh-CN.md +105 -15
- package/package.json +3 -2
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { windowsPowerShellPath } from './windows-powershell.js';
|
|
3
|
+
const fail = (code) => { throw new Error(code); };
|
|
4
|
+
const validName = (value) => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,254}$/.test(value);
|
|
5
|
+
const validValue = (value) => typeof value === 'string' && !!value && value.trim() === value && Buffer.byteLength(value) <= 8192 && !/[\x00-\x1f\x7f]/.test(value);
|
|
6
|
+
const runner = (command, args, options) => spawnSync(command, args, options);
|
|
7
|
+
// Keychain Services generic-password dictionary keys are stable API values.
|
|
8
|
+
// Literal NSString keys avoid JXA's opaque CFString global-constant wrappers.
|
|
9
|
+
// Rebinding CopyMatching's output as id* bridges its NSData result correctly.
|
|
10
|
+
const keychainScript = String.raw `ObjC.import('Foundation');
|
|
11
|
+
ObjC.import('Security');
|
|
12
|
+
ObjC.bindFunction('SecItemCopyMatching', ['int', ['id', 'id*']]);
|
|
13
|
+
const input = JSON.parse(ObjC.unwrap($.NSString.alloc.initWithDataEncoding($.NSFileHandle.fileHandleWithStandardInput.readDataToEndOfFile, $.NSUTF8StringEncoding)));
|
|
14
|
+
const query = $.NSMutableDictionary.alloc.init;
|
|
15
|
+
query.setObjectForKey($('genp'), $('class'));
|
|
16
|
+
query.setObjectForKey($(input.service), $('svce'));
|
|
17
|
+
query.setObjectForKey($(input.account), $('acct'));
|
|
18
|
+
let status = 0;
|
|
19
|
+
let value = null;
|
|
20
|
+
if (input.action === 'store') {
|
|
21
|
+
const data = $(input.value).dataUsingEncoding($.NSUTF8StringEncoding);
|
|
22
|
+
const changes = $.NSMutableDictionary.alloc.init;
|
|
23
|
+
changes.setObjectForKey(data, $('v_Data'));
|
|
24
|
+
status = $.SecItemUpdate(query, changes);
|
|
25
|
+
if (status === -25300) {
|
|
26
|
+
query.setObjectForKey(data, $('v_Data'));
|
|
27
|
+
status = $.SecItemAdd(query, null);
|
|
28
|
+
}
|
|
29
|
+
} else if (input.action === 'load') {
|
|
30
|
+
query.setObjectForKey($(true), $('r_Data'));
|
|
31
|
+
query.setObjectForKey($('m_LimitOne'), $('m_Limit'));
|
|
32
|
+
const result = Ref();
|
|
33
|
+
status = $.SecItemCopyMatching(query, result);
|
|
34
|
+
if (status === 0) value = ObjC.unwrap($.NSString.alloc.initWithDataEncoding(result[0], $.NSUTF8StringEncoding));
|
|
35
|
+
} else if (input.action === 'delete') {
|
|
36
|
+
status = $.SecItemDelete(query);
|
|
37
|
+
} else {
|
|
38
|
+
status = -50;
|
|
39
|
+
}
|
|
40
|
+
const output = $(JSON.stringify({ status, value })).dataUsingEncoding($.NSUTF8StringEncoding);
|
|
41
|
+
$.NSFileHandle.fileHandleWithStandardOutput.writeData(output);`;
|
|
42
|
+
// Windows generic credentials belong to the current token's credential set.
|
|
43
|
+
// The script is constant: target metadata and secret values arrive over stdin.
|
|
44
|
+
// CREDENTIALW layout/blob limit: learn.microsoft.com/windows/win32/api/wincred/ns-wincred-credentialw
|
|
45
|
+
const windowsScript = String.raw `$ErrorActionPreference = 'Stop'
|
|
46
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
47
|
+
[Console]::InputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
48
|
+
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
49
|
+
$status = -1
|
|
50
|
+
$value = $null
|
|
51
|
+
try {
|
|
52
|
+
$request = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
53
|
+
Add-Type -TypeDefinition @'
|
|
54
|
+
using System;
|
|
55
|
+
using System.Text;
|
|
56
|
+
using System.Runtime.InteropServices;
|
|
57
|
+
public static class AptlyCredentialStore {
|
|
58
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
59
|
+
private struct Credential {
|
|
60
|
+
public uint Flags;
|
|
61
|
+
public uint Type;
|
|
62
|
+
[MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
|
|
63
|
+
[MarshalAs(UnmanagedType.LPWStr)] public string Comment;
|
|
64
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
65
|
+
public uint CredentialBlobSize;
|
|
66
|
+
public IntPtr CredentialBlob;
|
|
67
|
+
public uint Persist;
|
|
68
|
+
public uint AttributeCount;
|
|
69
|
+
public IntPtr Attributes;
|
|
70
|
+
[MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias;
|
|
71
|
+
[MarshalAs(UnmanagedType.LPWStr)] public string UserName;
|
|
72
|
+
}
|
|
73
|
+
[DllImport("Advapi32.dll", EntryPoint="CredWriteW", CharSet=CharSet.Unicode, ExactSpelling=true, SetLastError=true)]
|
|
74
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
75
|
+
private static extern bool CredWrite(ref Credential credential, uint flags);
|
|
76
|
+
[DllImport("Advapi32.dll", EntryPoint="CredReadW", CharSet=CharSet.Unicode, ExactSpelling=true, SetLastError=true)]
|
|
77
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
78
|
+
private static extern bool CredRead(string target, uint type, uint flags, out IntPtr credential);
|
|
79
|
+
[DllImport("Advapi32.dll", EntryPoint="CredDeleteW", CharSet=CharSet.Unicode, ExactSpelling=true, SetLastError=true)]
|
|
80
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
81
|
+
private static extern bool CredDelete(string target, uint type, uint flags);
|
|
82
|
+
[DllImport("Advapi32.dll", ExactSpelling=true)]
|
|
83
|
+
private static extern void CredFree(IntPtr credential);
|
|
84
|
+
private static readonly UTF8Encoding Utf8 = new UTF8Encoding(false, true);
|
|
85
|
+
public static int Store(string target, string value) {
|
|
86
|
+
byte[] bytes = Utf8.GetBytes(value);
|
|
87
|
+
IntPtr blob = IntPtr.Zero;
|
|
88
|
+
try {
|
|
89
|
+
if (bytes.Length == 0 || bytes.Length > 2560) return 87;
|
|
90
|
+
blob = Marshal.AllocHGlobal(bytes.Length);
|
|
91
|
+
Marshal.Copy(bytes, 0, blob, bytes.Length);
|
|
92
|
+
Credential credential = new Credential();
|
|
93
|
+
credential.Type = 1;
|
|
94
|
+
credential.TargetName = target;
|
|
95
|
+
credential.UserName = "Aptly";
|
|
96
|
+
credential.CredentialBlobSize = (uint)bytes.Length;
|
|
97
|
+
credential.CredentialBlob = blob;
|
|
98
|
+
credential.Persist = 2;
|
|
99
|
+
return CredWrite(ref credential, 0) ? 0 : Marshal.GetLastWin32Error();
|
|
100
|
+
} finally {
|
|
101
|
+
if (blob != IntPtr.Zero) {
|
|
102
|
+
for (int i = 0; i < bytes.Length; i++) Marshal.WriteByte(blob, i, 0);
|
|
103
|
+
Marshal.FreeHGlobal(blob);
|
|
104
|
+
}
|
|
105
|
+
Array.Clear(bytes, 0, bytes.Length);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
public static int Load(string target, out string value) {
|
|
109
|
+
value = null;
|
|
110
|
+
IntPtr pointer = IntPtr.Zero;
|
|
111
|
+
byte[] bytes = null;
|
|
112
|
+
try {
|
|
113
|
+
if (!CredRead(target, 1, 0, out pointer)) return Marshal.GetLastWin32Error();
|
|
114
|
+
Credential credential = (Credential)Marshal.PtrToStructure(pointer, typeof(Credential));
|
|
115
|
+
if (credential.CredentialBlobSize == 0 || credential.CredentialBlobSize > 2560 || credential.CredentialBlob == IntPtr.Zero) return 87;
|
|
116
|
+
bytes = new byte[(int)credential.CredentialBlobSize];
|
|
117
|
+
Marshal.Copy(credential.CredentialBlob, bytes, 0, bytes.Length);
|
|
118
|
+
value = Utf8.GetString(bytes);
|
|
119
|
+
return 0;
|
|
120
|
+
} finally {
|
|
121
|
+
if (bytes != null) Array.Clear(bytes, 0, bytes.Length);
|
|
122
|
+
if (pointer != IntPtr.Zero) CredFree(pointer);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
public static int Delete(string target) {
|
|
126
|
+
return CredDelete(target, 1, 0) ? 0 : Marshal.GetLastWin32Error();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
'@
|
|
130
|
+
$target = [string]$request.service + ':' + [string]$request.account
|
|
131
|
+
switch ($request.action) {
|
|
132
|
+
'store' { $status = [AptlyCredentialStore]::Store($target, [string]$request.value) }
|
|
133
|
+
'load' { $status = [AptlyCredentialStore]::Load($target, [ref]$value) }
|
|
134
|
+
'delete' { $status = [AptlyCredentialStore]::Delete($target) }
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
$status = -1
|
|
138
|
+
$value = $null
|
|
139
|
+
}
|
|
140
|
+
[Console]::Out.WriteLine((@{status=$status;value=$value} | ConvertTo-Json -Compress))`;
|
|
141
|
+
function execute(command, args, input, options) {
|
|
142
|
+
let result;
|
|
143
|
+
try {
|
|
144
|
+
result = (options.exec ?? runner)(command, args, { input, encoding: 'utf8', timeout: 15000, maxBuffer: 65536, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return fail('secure_store_failed');
|
|
148
|
+
}
|
|
149
|
+
const errorCode = result.error && 'code' in result.error ? result.error.code : undefined;
|
|
150
|
+
if (errorCode === 'ENOENT')
|
|
151
|
+
fail('secure_store_unavailable');
|
|
152
|
+
if (errorCode === 'ETIMEDOUT')
|
|
153
|
+
fail('secure_store_timeout');
|
|
154
|
+
if (result.error)
|
|
155
|
+
fail('secure_store_failed');
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
function operate(action, value, options) {
|
|
159
|
+
const service = options.service ?? 'dev.aptly.mcp', account = options.account ?? 'OPENAI_API_KEY';
|
|
160
|
+
if (!validName(service) || !validName(account))
|
|
161
|
+
fail('secure_store_invalid_options');
|
|
162
|
+
if (action === 'store' && !validValue(value))
|
|
163
|
+
fail('secure_store_invalid_value');
|
|
164
|
+
const platform = options.platform ?? process.platform;
|
|
165
|
+
if (platform === 'win32') {
|
|
166
|
+
if (action === 'store' && Buffer.byteLength(value) > 2560)
|
|
167
|
+
fail('secure_store_invalid_value');
|
|
168
|
+
const input = JSON.stringify({ action, service, account, ...(action === 'store' ? { value } : {}) });
|
|
169
|
+
let executable;
|
|
170
|
+
try {
|
|
171
|
+
executable = windowsPowerShellPath();
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return fail('secure_store_unavailable');
|
|
175
|
+
}
|
|
176
|
+
const result = execute(executable, ['-NoProfile', '-NonInteractive', '-Command', windowsScript], input, options);
|
|
177
|
+
if (result.status !== 0)
|
|
178
|
+
fail('secure_store_failed');
|
|
179
|
+
let parsed;
|
|
180
|
+
try {
|
|
181
|
+
parsed = JSON.parse(String(result.stdout ?? ''));
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return fail('secure_store_failed');
|
|
185
|
+
}
|
|
186
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
187
|
+
fail('secure_store_failed');
|
|
188
|
+
const response = parsed;
|
|
189
|
+
if (response.status === 1168 && action !== 'store')
|
|
190
|
+
return null;
|
|
191
|
+
if (response.status === 5)
|
|
192
|
+
fail('secure_store_denied');
|
|
193
|
+
if (response.status === 1223)
|
|
194
|
+
fail('secure_store_cancelled');
|
|
195
|
+
if (response.status === 1312 || response.status === 50)
|
|
196
|
+
fail('secure_store_unavailable');
|
|
197
|
+
if (response.status !== 0)
|
|
198
|
+
fail('secure_store_failed');
|
|
199
|
+
if (action !== 'load')
|
|
200
|
+
return null;
|
|
201
|
+
const loaded = response.value;
|
|
202
|
+
if (!validValue(loaded) || Buffer.byteLength(loaded) > 2560)
|
|
203
|
+
return fail('secure_store_invalid_value');
|
|
204
|
+
return loaded;
|
|
205
|
+
}
|
|
206
|
+
if (platform === 'darwin') {
|
|
207
|
+
const input = JSON.stringify({ action, service, account, ...(action === 'store' ? { value } : {}) });
|
|
208
|
+
const result = execute('/usr/bin/osascript', ['-l', 'JavaScript', '-e', keychainScript], input, options);
|
|
209
|
+
if (result.status !== 0)
|
|
210
|
+
fail('secure_store_failed');
|
|
211
|
+
let parsed;
|
|
212
|
+
try {
|
|
213
|
+
parsed = JSON.parse(String(result.stdout ?? ''));
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return fail('secure_store_failed');
|
|
217
|
+
}
|
|
218
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
219
|
+
fail('secure_store_failed');
|
|
220
|
+
const response = parsed;
|
|
221
|
+
if (response.status === -25300) {
|
|
222
|
+
if (action === 'store')
|
|
223
|
+
fail('secure_store_unavailable');
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
if (response.status === -128)
|
|
227
|
+
fail('secure_store_cancelled');
|
|
228
|
+
if (response.status === -25293)
|
|
229
|
+
fail('secure_store_denied');
|
|
230
|
+
if (response.status === -25308)
|
|
231
|
+
fail('secure_store_locked');
|
|
232
|
+
if (response.status === -34018)
|
|
233
|
+
fail('secure_store_unavailable');
|
|
234
|
+
if (response.status !== 0)
|
|
235
|
+
fail('secure_store_failed');
|
|
236
|
+
if (action !== 'load')
|
|
237
|
+
return null;
|
|
238
|
+
const loaded = response.value;
|
|
239
|
+
if (!validValue(loaded))
|
|
240
|
+
return fail('secure_store_invalid_value');
|
|
241
|
+
return loaded;
|
|
242
|
+
}
|
|
243
|
+
if (platform === 'linux') {
|
|
244
|
+
const args = action === 'store' ? ['store', '--label=Aptly MCP classification key', 'service', service, 'account', account] : [action === 'load' ? 'lookup' : 'clear', 'service', service, 'account', account];
|
|
245
|
+
const result = execute('secret-tool', args, action === 'store' ? value + '\n' : undefined, options);
|
|
246
|
+
if (result.status === 1 && !String(result.stderr ?? '').trim() && (action === 'delete' || (action === 'load' && !String(result.stdout ?? '').trim())))
|
|
247
|
+
return null;
|
|
248
|
+
if (result.status !== 0)
|
|
249
|
+
fail('secure_store_failed');
|
|
250
|
+
if (action !== 'load')
|
|
251
|
+
return null;
|
|
252
|
+
const loaded = String(result.stdout ?? '').replace(/\r?\n$/, '');
|
|
253
|
+
if (!validValue(loaded))
|
|
254
|
+
fail('secure_store_invalid_value');
|
|
255
|
+
return loaded;
|
|
256
|
+
}
|
|
257
|
+
return fail('secure_store_unsupported_platform');
|
|
258
|
+
}
|
|
259
|
+
/** Secret values are sent to OS tools only over stdin, never command arguments. */
|
|
260
|
+
export function storeSecret(value, options = {}) { operate('store', value, options); }
|
|
261
|
+
/** Internal runtime API: callers must never echo the returned value. */
|
|
262
|
+
export function loadSecret(options = {}) { return operate('load', undefined, options); }
|
|
263
|
+
export function deleteSecret(options = {}) { operate('delete', undefined, options); }
|
package/dist/src/server.js
CHANGED
|
@@ -13,7 +13,7 @@ const constraintsSchema = z.object({ model_pinned: z.boolean().optional(), effor
|
|
|
13
13
|
const hookSchema = { client: z.enum(['codex', 'claude']), event: z.record(z.string(), z.unknown()) };
|
|
14
14
|
const content = (result) => ({ content: [{ type: 'text', text: JSON.stringify(result) }], structuredContent: result });
|
|
15
15
|
export function createServer({ env = process.env } = {}) {
|
|
16
|
-
const { router, mode, modelInfo, disabledClients } = runtime(env);
|
|
16
|
+
const { router, mode, modelInfo, credentialStatus, disabledClients } = runtime(env);
|
|
17
17
|
let lastAutomaticHook = null;
|
|
18
18
|
const server = new McpServer({ name: 'aptly', version: VERSION });
|
|
19
19
|
const annotations = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: mode === 'decisions' };
|
|
@@ -28,6 +28,7 @@ export function createServer({ env = process.env } = {}) {
|
|
|
28
28
|
precedence: 'per_call_effective_host_evidence_required', automatic_settings_discovery: false,
|
|
29
29
|
recommendation: { application: 'advisory_only', host_evidence_required: false, constraints: 'caller_declared_pins' },
|
|
30
30
|
model_information: { status: modelInfo.status, refresh: 'explicit_only', account_access_verified: false }, disabled_clients: disabledClients,
|
|
31
|
+
credential_status: credentialStatus,
|
|
31
32
|
automatic_hook: { status: loadAutomaticHookPolicy(env.APTLY_HOOK_POLICY_PATH).reason, mode: loadAutomaticHookPolicy(env.APTLY_HOOK_POLICY_PATH).policy?.mode ?? 'disabled', host_settings_verified: false, last_result: lastAutomaticHook },
|
|
32
33
|
}));
|
|
33
34
|
server.registerTool('recommend_agent', {
|
package/dist/src/setup.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export interface SetupResult {
|
|
|
17
17
|
status: 'complete' | 'partial' | 'skipped';
|
|
18
18
|
clients: SetupClientResult[];
|
|
19
19
|
}
|
|
20
|
+
export declare const configurableEnvironmentKeys: readonly string[];
|
|
20
21
|
export declare function setupHosts(options?: SetupOptions): SetupResult;
|
|
21
22
|
export declare function removeSetup(options?: SetupOptions): SetupResult;
|
|
23
|
+
export declare function setDefaults(options?: SetupOptions): SetupResult;
|
|
24
|
+
export declare function setEnvironment(key: string, value: string, options?: SetupOptions): SetupResult;
|
|
25
|
+
/** Store only the nonsecret source reference; credential values never enter setup. */
|
|
26
|
+
export declare function setCredentialReference(options?: SetupOptions): SetupResult;
|
|
22
27
|
export {};
|
package/dist/src/setup.js
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import { lstatSync, readFileSync, mkdirSync, chmodSync, openSync, writeFileSync, closeSync, renameSync, unlinkSync, rmdirSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { isDeepStrictEqual } from 'node:util';
|
|
7
7
|
import { parse } from 'smol-toml';
|
|
8
|
+
import { resolveCatalog } from './catalog.js';
|
|
9
|
+
import { assertPrivateStatePath } from './files.js';
|
|
10
|
+
import { assertWindowsPrivatePath, protectNewWindowsPrivateDirectory, copyWindowsFilePermissions } from './windows-private.js';
|
|
8
11
|
const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
9
12
|
const stringify = (value) => JSON.stringify(value, null, 2) + '\n';
|
|
10
13
|
const fail = (code) => { throw new Error(code); };
|
|
14
|
+
function privatePermissions(path, code) {
|
|
15
|
+
if (process.platform === 'win32') {
|
|
16
|
+
try {
|
|
17
|
+
assertWindowsPrivatePath(path);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
fail(code);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else if ((lstatSync(path).mode & 0o077) !== 0)
|
|
24
|
+
fail(code);
|
|
25
|
+
}
|
|
11
26
|
const checkedDirectory = (path) => { const s = lstatSync(path); if (!s.isDirectory() || s.isSymbolicLink())
|
|
12
27
|
fail('unsafe_directory'); };
|
|
13
28
|
function noSymlinks(path) {
|
|
@@ -140,6 +155,85 @@ function removeTomlEnv(text, additions) {
|
|
|
140
155
|
fail('managed_entry_changed');
|
|
141
156
|
return text;
|
|
142
157
|
}
|
|
158
|
+
export const configurableEnvironmentKeys = Object.freeze(['ROUTER_PROVIDER', 'APTLY_CLAUDE_MODELS', 'APTLY_CODEX_MODELS', 'CLAUDE_MODELS', 'CODEX_MODELS', 'APTLY_MODEL_INFO_PATH']);
|
|
159
|
+
const defaultEnvironment = { ROUTER_PROVIDER: 'decisions', APTLY_CLAUDE_MODELS: '["haiku","sonnet","opus"]', APTLY_CODEX_MODELS: '["gpt-6-luna","gpt-6.1-terra","gpt-6.1-sol"]' };
|
|
160
|
+
function validateEnvironmentValue(key, value) {
|
|
161
|
+
if (!configurableEnvironmentKeys.includes(key))
|
|
162
|
+
fail(/KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL/i.test(key) ? 'environment_secret_key_unsupported' : 'environment_key_unsupported');
|
|
163
|
+
if (typeof value !== 'string' || !value || Buffer.byteLength(value) > 8192 || /[\x00-\x1f\x7f]/.test(value))
|
|
164
|
+
fail('environment_invalid_value');
|
|
165
|
+
if (/\bsk-[A-Za-z0-9_-]{8,}|\bBearer\s+\S+|https?:\/\/[^/\s]+:[^/\s]+@/i.test(value))
|
|
166
|
+
fail('environment_sensitive_value');
|
|
167
|
+
if (key === 'ROUTER_PROVIDER' && !['fixture', 'decisions'].includes(value))
|
|
168
|
+
fail('environment_invalid_value');
|
|
169
|
+
if (key === 'APTLY_MODEL_INFO_PATH') {
|
|
170
|
+
try {
|
|
171
|
+
assertPrivateStatePath(value);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
fail('environment_invalid_value');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function editEnvironment(current, edit, client) {
|
|
179
|
+
const updates = {};
|
|
180
|
+
if ('defaults' in edit) {
|
|
181
|
+
for (const [key, value] of Object.entries(defaultEnvironment)) {
|
|
182
|
+
const alias = key === 'APTLY_CLAUDE_MODELS' ? 'CLAUDE_MODELS' : key === 'APTLY_CODEX_MODELS' ? 'CODEX_MODELS' : null;
|
|
183
|
+
if (current[key] === undefined && (!alias || current[alias] === undefined))
|
|
184
|
+
updates[key] = value;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else if ('credential' in edit) {
|
|
188
|
+
updates.APTLY_SECRET_BACKEND = 'os';
|
|
189
|
+
updates.APTLY_SECRET_CLIENT = client;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
validateEnvironmentValue(edit.key, edit.value);
|
|
193
|
+
updates[edit.key] = edit.value;
|
|
194
|
+
}
|
|
195
|
+
const effective = { ...current, ...updates };
|
|
196
|
+
if (effective.ROUTER_PROVIDER !== undefined && !['fixture', 'decisions'].includes(effective.ROUTER_PROVIDER))
|
|
197
|
+
fail('environment_invalid_value');
|
|
198
|
+
try {
|
|
199
|
+
resolveCatalog(effective);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
fail('environment_invalid_value');
|
|
203
|
+
}
|
|
204
|
+
return Object.fromEntries(Object.entries(updates).filter(([key, value]) => current[key] !== value));
|
|
205
|
+
}
|
|
206
|
+
/** Change only selected single-line scalar env assignments; refuse ambiguous TOML layouts. */
|
|
207
|
+
function updateTomlEnv(text, updates) {
|
|
208
|
+
const parsed = toml(text), existing = parsed.mcp_servers?.aptly?.env ?? {};
|
|
209
|
+
const expected = structuredClone(parsed);
|
|
210
|
+
Object.assign(map(map(map(expected, 'mcp_servers'), 'aptly'), 'env'), updates);
|
|
211
|
+
const headers = [...text.matchAll(/^\s*\[([^\n]+)\][ \t]*(?:#[^\n]*)?\r?$/gm)];
|
|
212
|
+
const header = headers.find(h => { try {
|
|
213
|
+
return toml(h[0] + '\naptly_probe = true\n').mcp_servers?.aptly?.env?.aptly_probe === true;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return false;
|
|
217
|
+
} });
|
|
218
|
+
const present = Object.entries(updates).filter(([key]) => existing[key] !== undefined);
|
|
219
|
+
if (present.length) {
|
|
220
|
+
if (!header)
|
|
221
|
+
fail('unsupported_toml_layout');
|
|
222
|
+
const end = headers.find(h => h.index > header.index)?.index ?? text.length;
|
|
223
|
+
let body = text.slice(header.index + header[0].length, end);
|
|
224
|
+
for (const [key, value] of present) {
|
|
225
|
+
const line = new RegExp('^[ \t]*' + key + '[ \t]*=[^\n]*(?:\n|$)', 'gm');
|
|
226
|
+
if ([...body.matchAll(line)].length !== 1)
|
|
227
|
+
fail('unsupported_toml_layout');
|
|
228
|
+
body = body.replace(line, () => key + ' = ' + q(value) + '\n');
|
|
229
|
+
}
|
|
230
|
+
text = text.slice(0, header.index + header[0].length) + body + text.slice(end);
|
|
231
|
+
}
|
|
232
|
+
text = addTomlEnv(text, Object.fromEntries(Object.entries(updates).filter(([key]) => existing[key] === undefined)));
|
|
233
|
+
if (!isDeepStrictEqual(toml(text), expected))
|
|
234
|
+
fail('unsupported_toml_layout');
|
|
235
|
+
return text;
|
|
236
|
+
}
|
|
143
237
|
function resolved(options, client) {
|
|
144
238
|
const home = resolve(options.home ?? homedir()), env = options.env ?? process.env;
|
|
145
239
|
const override = client === 'codex' ? env.CODEX_HOME : env.CLAUDE_CONFIG_DIR;
|
|
@@ -154,7 +248,23 @@ function ownership(value, client) {
|
|
|
154
248
|
fail('invalid_ownership');
|
|
155
249
|
return value;
|
|
156
250
|
}
|
|
157
|
-
|
|
251
|
+
/** Compatibility restriction only: this does not authenticate arbitrary launcher code. */
|
|
252
|
+
function requireCredentialRuntime(entry, root) {
|
|
253
|
+
const allowed = [join(root, 'dist', 'src', 'server.js'), join(root, 'src', 'server.ts')];
|
|
254
|
+
if (entry.command !== process.execPath || !Array.isArray(entry.args) || entry.args.length !== 1 || !allowed.includes(entry.args[0]))
|
|
255
|
+
fail('credential_launcher_unverified');
|
|
256
|
+
try {
|
|
257
|
+
const extension = entry.args[0].endsWith('.ts') ? 'ts' : 'js';
|
|
258
|
+
const server = read(entry.args[0]).text;
|
|
259
|
+
const runtime = read(join(dirname(entry.args[0]), 'runtime.' + extension)).text;
|
|
260
|
+
if (!server?.includes('./runtime.' + extension) || !runtime?.includes('APTLY_SECRET_BACKEND') || !runtime.includes('APTLY_SECRET_CLIENT'))
|
|
261
|
+
fail('credential_launcher_unverified');
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
fail('credential_launcher_unverified');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function prepare(options, client, removing, edit) {
|
|
158
268
|
const env = options.env ?? process.env, paths = resolved(options, client), { dir, mcpPath, hooksPath, privateDir } = paths;
|
|
159
269
|
const changes = [], dirs = [];
|
|
160
270
|
noSymlinks(dir);
|
|
@@ -162,8 +272,7 @@ function prepare(options, client, removing) {
|
|
|
162
272
|
for (const path of [privateDir, join(privateDir, 'state')]) {
|
|
163
273
|
try {
|
|
164
274
|
checkedDirectory(path);
|
|
165
|
-
|
|
166
|
-
fail('private_directory_permissions');
|
|
275
|
+
privatePermissions(path, 'private_directory_permissions');
|
|
167
276
|
}
|
|
168
277
|
catch (e) {
|
|
169
278
|
if (e.code !== 'ENOENT')
|
|
@@ -186,8 +295,8 @@ function prepare(options, client, removing) {
|
|
|
186
295
|
const snap = (path) => { const source = read(path); return { path, before: source.text, after: source.text, mode: source.mode }; };
|
|
187
296
|
const mcp = snap(mcpPath), hooks = snap(hooksPath), policy = snap(policyPath), manifest = snap(manifestPath);
|
|
188
297
|
for (const privateFile of [policy, manifest])
|
|
189
|
-
if (privateFile.before !== null
|
|
190
|
-
|
|
298
|
+
if (privateFile.before !== null)
|
|
299
|
+
privatePermissions(privateFile.path, 'private_file_permissions');
|
|
191
300
|
const oldOwner = manifest.before === null ? undefined : ownership(json(manifest.before), client);
|
|
192
301
|
if (removing && !oldOwner)
|
|
193
302
|
return { changes, dirs, result: { client, status: 'skipped', codes: ['no_owned_setup'] } };
|
|
@@ -247,17 +356,13 @@ function prepare(options, client, removing) {
|
|
|
247
356
|
let created = false, tomlBlock;
|
|
248
357
|
if (servers.aptly === undefined) {
|
|
249
358
|
created = true;
|
|
250
|
-
servers.aptly = { ...(client === 'claude' ? { type: 'stdio' } : {}), command: process.execPath, args: [join(root, 'dist', 'src', 'server.js')], ...(client === 'codex' ? { env_vars: ['OPENAI_API_KEY'] } : {}), env: { ROUTER_PROVIDER: env.ROUTER_PROVIDER === 'fixture' ? 'fixture' : 'decisions', ...additions } };
|
|
251
|
-
if (client === 'codex') {
|
|
252
|
-
tomlBlock = newTomlBlock(servers.aptly);
|
|
253
|
-
mcp.after = (mcp.before ?? '') + ((mcp.before ?? '').endsWith('\n') || !mcp.before ? '\n' : '\n\n') + tomlBlock;
|
|
254
|
-
}
|
|
255
|
-
else
|
|
256
|
-
mcp.after = stringify(config);
|
|
359
|
+
servers.aptly = { ...(client === 'claude' ? { type: 'stdio' } : {}), command: process.execPath, args: [join(root, 'dist', 'src', 'server.js')], ...(client === 'codex' ? { env_vars: ['OPENAI_API_KEY'] } : {}), env: { ROUTER_PROVIDER: edit && 'defaults' in edit ? 'decisions' : env.ROUTER_PROVIDER === 'fixture' ? 'fixture' : 'decisions', ...additions } };
|
|
257
360
|
}
|
|
258
361
|
else {
|
|
259
362
|
if (!object(servers.aptly))
|
|
260
363
|
fail('mcp_entry_conflict');
|
|
364
|
+
if (edit && oldOwner?.mcp_created && !isDeepStrictEqual(servers.aptly, oldOwner.mcp))
|
|
365
|
+
fail('managed_mcp_changed');
|
|
261
366
|
const currentEnv = map(servers.aptly, 'env');
|
|
262
367
|
if (currentEnv.APTLY_HOOK_POLICY_PATH !== undefined && currentEnv.APTLY_HOOK_POLICY_PATH !== policyPath)
|
|
263
368
|
fail('mcp_environment_conflict');
|
|
@@ -265,13 +370,29 @@ function prepare(options, client, removing) {
|
|
|
265
370
|
Object.assign(currentEnv, missing);
|
|
266
371
|
mcp.after = client === 'codex' ? addTomlEnv(mcp.before ?? '', missing) : stringify(config);
|
|
267
372
|
}
|
|
373
|
+
if (edit && 'credential' in edit)
|
|
374
|
+
requireCredentialRuntime(servers.aptly, root);
|
|
375
|
+
const currentEnv = map(servers.aptly, 'env');
|
|
376
|
+
const updates = edit ? editEnvironment(currentEnv, edit, client) : {};
|
|
377
|
+
Object.assign(currentEnv, updates);
|
|
378
|
+
if (created) {
|
|
379
|
+
if (client === 'codex') {
|
|
380
|
+
tomlBlock = newTomlBlock(servers.aptly);
|
|
381
|
+
mcp.after = (mcp.before ?? '') + ((mcp.before ?? '').endsWith('\n') || !mcp.before ? '\n' : '\n\n') + tomlBlock;
|
|
382
|
+
}
|
|
383
|
+
else
|
|
384
|
+
mcp.after = stringify(config);
|
|
385
|
+
}
|
|
386
|
+
else if (edit)
|
|
387
|
+
mcp.after = client === 'codex' ? updateTomlEnv(mcp.after, updates) : stringify(config);
|
|
268
388
|
const existingRoute = entries.filter(routeHook);
|
|
269
389
|
if (existingRoute.some(entry => !isDeepStrictEqual(entry, hookValue)))
|
|
270
390
|
fail('existing_routing_hook_conflict');
|
|
271
391
|
if (existingRoute.length === 0)
|
|
272
392
|
entries.push(hookValue);
|
|
273
393
|
hooks.after = stringify(hooksObject);
|
|
274
|
-
const
|
|
394
|
+
const preservedMode = edit && policy.before !== null ? json(policy.before).mode : undefined;
|
|
395
|
+
const policyValue = { schema_version: 1, kind: 'aptly-auto-hook', mode: options.mode ?? (['defaults', 'advisory'].includes(preservedMode) ? preservedMode : 'defaults'), issuer: 'operator_installation', enabled: true, state_directory: stateDir };
|
|
275
396
|
if (policy.before !== null) {
|
|
276
397
|
const p = json(policy.before);
|
|
277
398
|
if (!isDeepStrictEqual(p, policyValue))
|
|
@@ -286,6 +407,36 @@ function prepare(options, client, removing) {
|
|
|
286
407
|
if (existingRoute.length && !oldOwner)
|
|
287
408
|
fail('existing_hook_not_owned');
|
|
288
409
|
const owner = oldOwner ?? { schema_version: 1, kind: 'aptly-setup-ownership', client, hook: hookValue, policy: policyValue, policy_created: policy.before === null, mcp_created: created, ...(created ? { mcp: servers.aptly } : {}), env_added: envAdded, ...(tomlBlock ? { toml_block: tomlBlock } : {}) };
|
|
410
|
+
if (edit && oldOwner) {
|
|
411
|
+
if (oldOwner.mcp_created) {
|
|
412
|
+
owner.mcp = structuredClone(servers.aptly);
|
|
413
|
+
if (client === 'codex') {
|
|
414
|
+
const oldBlock = oldOwner.toml_block;
|
|
415
|
+
if (!oldBlock || !(mcp.before ?? '').includes(oldBlock))
|
|
416
|
+
fail('managed_mcp_changed');
|
|
417
|
+
const nextBlock = newTomlBlock(servers.aptly);
|
|
418
|
+
const rendered = (mcp.before ?? '').replace(oldBlock, nextBlock);
|
|
419
|
+
if (!isDeepStrictEqual(toml(rendered), config))
|
|
420
|
+
fail('managed_mcp_changed');
|
|
421
|
+
mcp.after = rendered;
|
|
422
|
+
owner.toml_block = nextBlock;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
const prior = client === 'codex' ? toml(mcp.before ?? '') : json(mcp.before);
|
|
427
|
+
const priorEnv = prior[client === 'codex' ? 'mcp_servers' : 'mcpServers']?.aptly?.env ?? {};
|
|
428
|
+
for (const [key, value] of Object.entries(updates))
|
|
429
|
+
if (Object.hasOwn(owner.env_added, key) || priorEnv[key] === undefined)
|
|
430
|
+
owner.env_added[key] = value;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else if (edit && !created) {
|
|
434
|
+
const prior = client === 'codex' ? toml(mcp.before ?? '') : json(mcp.before);
|
|
435
|
+
const priorEnv = prior[client === 'codex' ? 'mcp_servers' : 'mcpServers']?.aptly?.env ?? {};
|
|
436
|
+
for (const [key, value] of Object.entries(updates))
|
|
437
|
+
if (priorEnv[key] === undefined)
|
|
438
|
+
owner.env_added[key] = value;
|
|
439
|
+
}
|
|
289
440
|
manifest.after = stringify(owner);
|
|
290
441
|
if (client === 'codex')
|
|
291
442
|
result.requires_hook_review = true;
|
|
@@ -316,13 +467,21 @@ function replace(snapshot, forward) {
|
|
|
316
467
|
let fd;
|
|
317
468
|
try {
|
|
318
469
|
fd = openSync(stage, 'wx', 0o600);
|
|
470
|
+
if (process.platform === 'win32' && expected !== null) {
|
|
471
|
+
try {
|
|
472
|
+
copyWindowsFilePermissions(snapshot.path, stage);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
fail('private_file_permissions');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
319
478
|
writeFileSync(fd, next);
|
|
320
479
|
closeSync(fd);
|
|
321
480
|
fd = undefined;
|
|
322
481
|
if (read(snapshot.path).text !== expected)
|
|
323
482
|
fail('concurrent_configuration_change');
|
|
324
483
|
renameSync(stage, snapshot.path);
|
|
325
|
-
if (snapshot.mode !== 0o600)
|
|
484
|
+
if (process.platform !== 'win32' && snapshot.mode !== 0o600)
|
|
326
485
|
chmodSync(snapshot.path, snapshot.mode);
|
|
327
486
|
}
|
|
328
487
|
finally {
|
|
@@ -337,7 +496,7 @@ function replace(snapshot, forward) {
|
|
|
337
496
|
}
|
|
338
497
|
}
|
|
339
498
|
}
|
|
340
|
-
function execute(options, removing) {
|
|
499
|
+
function execute(options, removing, edit) {
|
|
341
500
|
const clients = [...new Set(options.clients ?? ['codex', 'claude'])];
|
|
342
501
|
if (clients.some(client => client !== 'codex' && client !== 'claude'))
|
|
343
502
|
return { status: 'partial', clients: [] };
|
|
@@ -346,13 +505,15 @@ function execute(options, removing) {
|
|
|
346
505
|
return { status: 'skipped', clients: clients.map(client => ({ client, status: 'skipped', codes: [env.APTLY_SKIP_SETUP === '1' ? 'setup_disabled' : 'ci_environment'] })) };
|
|
347
506
|
const prepared = clients.map(client => {
|
|
348
507
|
try {
|
|
349
|
-
return prepare(options, client, removing);
|
|
508
|
+
return prepare(options, client, removing, edit);
|
|
350
509
|
}
|
|
351
510
|
catch (e) {
|
|
352
511
|
return { changes: [], dirs: [], result: { client, status: 'error', codes: [safeCode(e)] } };
|
|
353
512
|
}
|
|
354
513
|
});
|
|
355
514
|
const results = prepared.map(plan => plan.result);
|
|
515
|
+
if (edit && results.some(result => result.status === 'error'))
|
|
516
|
+
return { status: 'partial', clients: results.map(result => result.status === 'error' ? result : { ...result, status: 'skipped', codes: ['other_host_validation_failed'] }) };
|
|
356
517
|
// Each host is independent; prepare every host before committing any host.
|
|
357
518
|
for (const plan of prepared) {
|
|
358
519
|
if (!plan.changes.length)
|
|
@@ -369,11 +530,17 @@ function execute(options, removing) {
|
|
|
369
530
|
throw e;
|
|
370
531
|
mkdirSync(path, { mode: 0o700 });
|
|
371
532
|
madeDirs.push(path);
|
|
533
|
+
if (process.platform === 'win32') {
|
|
534
|
+
try {
|
|
535
|
+
protectNewWindowsPrivateDirectory(path);
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
fail('private_directory_permissions');
|
|
539
|
+
}
|
|
540
|
+
}
|
|
372
541
|
}
|
|
373
|
-
if (path
|
|
374
|
-
|
|
375
|
-
fail('private_directory_permissions');
|
|
376
|
-
}
|
|
542
|
+
if (basename(path) === 'aptly' || (basename(path) === 'state' && basename(dirname(path)) === 'aptly'))
|
|
543
|
+
privatePermissions(path, 'private_directory_permissions');
|
|
377
544
|
}
|
|
378
545
|
for (const snapshot of plan.changes) {
|
|
379
546
|
replace(snapshot, true);
|
|
@@ -402,7 +569,19 @@ function execute(options, removing) {
|
|
|
402
569
|
}
|
|
403
570
|
return { status: results.some(result => result.status === 'error') ? 'partial' : results.every(result => result.status === 'skipped') ? 'skipped' : 'complete', clients: results };
|
|
404
571
|
}
|
|
405
|
-
const codes = new Set(['unsafe_directory', 'unsafe_symlink', 'unsafe_configuration_file', 'invalid_json', 'invalid_json_object', 'configuration_shape_conflict', 'hook_shape_conflict', 'invalid_toml', 'unsupported_toml_layout', 'relative_configuration_directory', 'invalid_ownership', 'managed_hook_changed', 'managed_mcp_changed', 'managed_entry_changed', 'managed_policy_changed', 'mcp_entry_conflict', 'mcp_environment_conflict', 'existing_routing_hook_conflict', 'policy_conflict', 'existing_hook_not_owned', 'concurrent_configuration_change', 'private_directory_permissions', 'private_file_permissions']);
|
|
572
|
+
const codes = new Set(['credential_launcher_unverified', 'environment_invalid_value', 'environment_sensitive_value', 'environment_key_unsupported', 'environment_secret_key_unsupported', 'unsafe_directory', 'unsafe_symlink', 'unsafe_configuration_file', 'invalid_json', 'invalid_json_object', 'configuration_shape_conflict', 'hook_shape_conflict', 'invalid_toml', 'unsupported_toml_layout', 'relative_configuration_directory', 'invalid_ownership', 'managed_hook_changed', 'managed_mcp_changed', 'managed_entry_changed', 'managed_policy_changed', 'mcp_entry_conflict', 'mcp_environment_conflict', 'existing_routing_hook_conflict', 'policy_conflict', 'existing_hook_not_owned', 'concurrent_configuration_change', 'private_directory_permissions', 'private_file_permissions']);
|
|
406
573
|
function safeCode(error) { return error instanceof Error && codes.has(error.message) ? error.message : 'configuration_io_error'; }
|
|
407
574
|
export function setupHosts(options = {}) { return execute(options, false); }
|
|
408
575
|
export function removeSetup(options = {}) { return execute(options, true); }
|
|
576
|
+
export function setDefaults(options = {}) { return execute(options, false, { defaults: true }); }
|
|
577
|
+
export function setEnvironment(key, value, options = {}) {
|
|
578
|
+
try {
|
|
579
|
+
validateEnvironmentValue(key, value);
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
return { status: 'partial', clients: (options.clients ?? ['codex', 'claude']).map(client => ({ client, status: 'error', codes: [safeCode(error)] })) };
|
|
583
|
+
}
|
|
584
|
+
return execute(options, false, { key, value });
|
|
585
|
+
}
|
|
586
|
+
/** Store only the nonsecret source reference; credential values never enter setup. */
|
|
587
|
+
export function setCredentialReference(options = {}) { return execute(options, false, { credential: true }); }
|
package/dist/src/types.d.ts
CHANGED
|
@@ -71,6 +71,9 @@ export interface ProviderAnswer {
|
|
|
71
71
|
confidence?: number;
|
|
72
72
|
usage?: unknown;
|
|
73
73
|
}
|
|
74
|
-
export type RoutingProvider = (args: ProviderArguments) => Promise<ProviderAnswer
|
|
74
|
+
export type RoutingProvider = ((args: ProviderArguments) => Promise<ProviderAnswer>) & {
|
|
75
|
+
/** Synchronous local readiness check only; must not initiate transport. */
|
|
76
|
+
preflight?: () => void;
|
|
77
|
+
};
|
|
75
78
|
export declare function isRecord(value: unknown): value is UnknownRecord;
|
|
76
79
|
export declare function errorCode(error: unknown): string | undefined;
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.1";
|
package/dist/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.1.
|
|
1
|
+
export const VERSION = '0.1.1';
|