@hasna/recordings 0.3.8 → 0.3.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.
- package/bun.lock +26 -26
- package/dist/cli/index.js +915 -164
- package/dist/http/client.d.ts +6 -4
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.js +1751 -44
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/mcp/index.js +872 -121
- package/dist/server/index.js +52 -192
- package/dist/storage.js +1744 -37
- package/dist/store.d.ts.map +1 -1
- package/package.json +2 -2
- package/scripts/release-suite-gate.ts +17 -24
- package/src/native/Recordings/RecordingsLib/Info.plist +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -903,7 +903,7 @@ import chalk from "chalk";
|
|
|
903
903
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
904
904
|
import {
|
|
905
905
|
existsSync as existsSync8,
|
|
906
|
-
readFileSync as
|
|
906
|
+
readFileSync as readFileSync5,
|
|
907
907
|
readdirSync as readdirSync2
|
|
908
908
|
} from "fs";
|
|
909
909
|
import { dirname as dirname5, join as pathJoin } from "path";
|
|
@@ -1928,7 +1928,7 @@ function setAgentFocus(idOrName, projectId, db) {
|
|
|
1928
1928
|
// package.json
|
|
1929
1929
|
var package_default = {
|
|
1930
1930
|
name: "@hasna/recordings",
|
|
1931
|
-
version: "0.3.
|
|
1931
|
+
version: "0.3.9",
|
|
1932
1932
|
type: "module",
|
|
1933
1933
|
description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
|
|
1934
1934
|
repository: {
|
|
@@ -2012,7 +2012,7 @@ var package_default = {
|
|
|
2012
2012
|
"LICENSE"
|
|
2013
2013
|
],
|
|
2014
2014
|
dependencies: {
|
|
2015
|
-
"@hasna/contracts": "0.13.
|
|
2015
|
+
"@hasna/contracts": "0.13.4",
|
|
2016
2016
|
"@hasna/events": "0.1.11",
|
|
2017
2017
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
2018
2018
|
chalk: "^5.4.1",
|
|
@@ -2044,27 +2044,207 @@ function saveFeedback(input) {
|
|
|
2044
2044
|
db.query("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(input.message, input.email ?? null, input.category ?? "general", input.version ?? VERSION);
|
|
2045
2045
|
}
|
|
2046
2046
|
|
|
2047
|
-
//
|
|
2047
|
+
// ../contracts/dist/client/transport.js
|
|
2048
|
+
import { isIP } from "net";
|
|
2049
|
+
import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
2050
|
+
import { join as join4 } from "path";
|
|
2048
2051
|
function envToken(name) {
|
|
2049
2052
|
return name.toUpperCase().replace(/-/g, "_");
|
|
2050
2053
|
}
|
|
2051
|
-
function
|
|
2052
|
-
const
|
|
2054
|
+
function clientTransportEnvKeys(name) {
|
|
2055
|
+
const envSegment = envToken(name);
|
|
2053
2056
|
return {
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
apiKeyKeys: [`HASNA_${token}_API_KEY`]
|
|
2057
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
2058
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
2057
2059
|
};
|
|
2058
2060
|
}
|
|
2059
|
-
function
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2061
|
+
function credentialOverrideEnvKey(name) {
|
|
2062
|
+
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
2063
|
+
}
|
|
2064
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
|
|
2065
|
+
|
|
2066
|
+
class CredentialResolutionError extends Error {
|
|
2067
|
+
appName;
|
|
2068
|
+
attempted;
|
|
2069
|
+
constructor(appName, message, attempted) {
|
|
2070
|
+
super(message);
|
|
2071
|
+
this.name = "CredentialResolutionError";
|
|
2072
|
+
this.appName = appName;
|
|
2073
|
+
this.attempted = attempted;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
var HASNA_STATE_DIR = ".hasna";
|
|
2077
|
+
var FLEET_CREDENTIAL_DIR = "cloud";
|
|
2078
|
+
var CONFIG_DIR = ".config";
|
|
2079
|
+
var CONFIG_NAMESPACE = "hasna";
|
|
2080
|
+
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
2081
|
+
var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
2082
|
+
var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
2083
|
+
var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
2084
|
+
function homeDir(env) {
|
|
2085
|
+
const home = env.HOME?.trim();
|
|
2086
|
+
return home ? home : null;
|
|
2087
|
+
}
|
|
2088
|
+
function credentialDiskSources(name, env) {
|
|
2089
|
+
return profileDiskSources(name, env, null);
|
|
2090
|
+
}
|
|
2091
|
+
function profileDiskSources(name, env, profile) {
|
|
2092
|
+
const home = homeDir(env);
|
|
2093
|
+
if (!home || !SAFE_APP_SLUG.test(name))
|
|
2094
|
+
return [];
|
|
2095
|
+
const stem = profile ? `${name}.${profile}` : name;
|
|
2096
|
+
const configStem = profile ? `${name}-${profile}` : name;
|
|
2097
|
+
return [
|
|
2098
|
+
join4(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
|
|
2099
|
+
join4(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`)
|
|
2100
|
+
];
|
|
2101
|
+
}
|
|
2102
|
+
function parseEnvFile(text) {
|
|
2103
|
+
const values = new Map;
|
|
2104
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
2105
|
+
const line = rawLine.trim();
|
|
2106
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
2107
|
+
continue;
|
|
2108
|
+
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
2109
|
+
const equals = withoutExport.indexOf("=");
|
|
2110
|
+
if (equals <= 0)
|
|
2111
|
+
continue;
|
|
2112
|
+
const key = withoutExport.slice(0, equals).trim();
|
|
2113
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
2114
|
+
continue;
|
|
2115
|
+
let value = withoutExport.slice(equals + 1).trim();
|
|
2116
|
+
const quote = value[0];
|
|
2117
|
+
if (quote === '"' || quote === "'") {
|
|
2118
|
+
if (value.length < 2 || !value.endsWith(quote))
|
|
2119
|
+
continue;
|
|
2120
|
+
value = value.slice(1, -1);
|
|
2121
|
+
}
|
|
2122
|
+
if (value.length === 0)
|
|
2123
|
+
continue;
|
|
2124
|
+
values.set(key, value);
|
|
2125
|
+
}
|
|
2126
|
+
return values;
|
|
2127
|
+
}
|
|
2128
|
+
function readAppConfigFile(path) {
|
|
2129
|
+
let text;
|
|
2130
|
+
try {
|
|
2131
|
+
const stats = statSync2(path);
|
|
2132
|
+
if (!stats.isFile() || stats.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
2133
|
+
return null;
|
|
2134
|
+
text = readFileSync2(path, "utf8");
|
|
2135
|
+
} catch {
|
|
2136
|
+
return null;
|
|
2137
|
+
}
|
|
2138
|
+
return parseEnvFile(text);
|
|
2066
2139
|
}
|
|
2067
|
-
function
|
|
2140
|
+
function readCredentialFile(path, apiKeyKeys) {
|
|
2141
|
+
const values = readAppConfigFile(path);
|
|
2142
|
+
if (!values)
|
|
2143
|
+
return null;
|
|
2144
|
+
for (const key of apiKeyKeys) {
|
|
2145
|
+
const value = values.get(key)?.trim();
|
|
2146
|
+
if (value)
|
|
2147
|
+
return value;
|
|
2148
|
+
}
|
|
2149
|
+
return null;
|
|
2150
|
+
}
|
|
2151
|
+
var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
|
|
2152
|
+
function appConfigDiskValue(name, env, keys) {
|
|
2153
|
+
const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
|
|
2154
|
+
if (wanted.length === 0)
|
|
2155
|
+
return null;
|
|
2156
|
+
for (const path of credentialDiskSources(name, env)) {
|
|
2157
|
+
const values = readAppConfigFile(path);
|
|
2158
|
+
if (!values)
|
|
2159
|
+
continue;
|
|
2160
|
+
for (const key of wanted) {
|
|
2161
|
+
const value = values.get(key)?.trim();
|
|
2162
|
+
if (value)
|
|
2163
|
+
return { key, value, path };
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
return null;
|
|
2167
|
+
}
|
|
2168
|
+
function assertUsableCredential(appName, source, value) {
|
|
2169
|
+
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
2170
|
+
return;
|
|
2171
|
+
throw new CredentialResolutionError(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
|
|
2172
|
+
}
|
|
2173
|
+
var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
2174
|
+
var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
2175
|
+
var CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE = "caller-supplied CredentialProvider";
|
|
2176
|
+
function sealCredential(fields) {
|
|
2177
|
+
const { apiKey } = fields;
|
|
2178
|
+
const visible = {
|
|
2179
|
+
tier: fields.tier,
|
|
2180
|
+
source: fields.source,
|
|
2181
|
+
deliberate: fields.deliberate,
|
|
2182
|
+
deprecated: fields.deprecated,
|
|
2183
|
+
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
2184
|
+
warning: fields.warning
|
|
2185
|
+
};
|
|
2186
|
+
const sealed = { ...visible };
|
|
2187
|
+
Object.defineProperty(sealed, "apiKey", {
|
|
2188
|
+
value: apiKey,
|
|
2189
|
+
enumerable: false,
|
|
2190
|
+
writable: false,
|
|
2191
|
+
configurable: false
|
|
2192
|
+
});
|
|
2193
|
+
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
2194
|
+
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
2195
|
+
enumerable: false,
|
|
2196
|
+
writable: false,
|
|
2197
|
+
configurable: false
|
|
2198
|
+
});
|
|
2199
|
+
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
2200
|
+
value: true,
|
|
2201
|
+
enumerable: false,
|
|
2202
|
+
writable: false,
|
|
2203
|
+
configurable: false
|
|
2204
|
+
});
|
|
2205
|
+
return Object.freeze(sealed);
|
|
2206
|
+
}
|
|
2207
|
+
function isSealedCredential(credential) {
|
|
2208
|
+
return credential[CREDENTIAL_SEAL] === true;
|
|
2209
|
+
}
|
|
2210
|
+
function explicitCredential(appName, apiKey) {
|
|
2211
|
+
const source = "explicit apiKey option";
|
|
2212
|
+
assertUsableCredential(appName, source, apiKey);
|
|
2213
|
+
return sealCredential({
|
|
2214
|
+
apiKey,
|
|
2215
|
+
tier: "argument",
|
|
2216
|
+
source,
|
|
2217
|
+
deliberate: true,
|
|
2218
|
+
deprecated: false,
|
|
2219
|
+
diskCandidates: [],
|
|
2220
|
+
warning: null
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
function validateAndSealResolvedCredential(appName, credential) {
|
|
2224
|
+
const apiKey = credential.apiKey;
|
|
2225
|
+
assertUsableCredential(appName, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE, apiKey);
|
|
2226
|
+
if (!isSealedCredential(credential)) {
|
|
2227
|
+
return sealCredential({
|
|
2228
|
+
apiKey,
|
|
2229
|
+
tier: "argument",
|
|
2230
|
+
source: CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE,
|
|
2231
|
+
deliberate: true,
|
|
2232
|
+
deprecated: false,
|
|
2233
|
+
diskCandidates: [],
|
|
2234
|
+
warning: null
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
return sealCredential({
|
|
2238
|
+
apiKey,
|
|
2239
|
+
tier: credential.tier,
|
|
2240
|
+
source: credential.source,
|
|
2241
|
+
deliberate: credential.deliberate,
|
|
2242
|
+
deprecated: credential.deprecated,
|
|
2243
|
+
diskCandidates: credential.diskCandidates,
|
|
2244
|
+
warning: credential.warning
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
function firstEnvValue(env, keys) {
|
|
2068
2248
|
for (const key of keys) {
|
|
2069
2249
|
const value = env[key]?.trim();
|
|
2070
2250
|
if (value)
|
|
@@ -2072,79 +2252,343 @@ function firstEnv(env, keys) {
|
|
|
2072
2252
|
}
|
|
2073
2253
|
return null;
|
|
2074
2254
|
}
|
|
2255
|
+
var DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
|
|
2256
|
+
function deprecationNotified() {
|
|
2257
|
+
const host = globalThis;
|
|
2258
|
+
const existing = host[DEPRECATION_REGISTRY];
|
|
2259
|
+
if (existing instanceof Set)
|
|
2260
|
+
return existing;
|
|
2261
|
+
const created = new Set;
|
|
2262
|
+
host[DEPRECATION_REGISTRY] = created;
|
|
2263
|
+
return created;
|
|
2264
|
+
}
|
|
2265
|
+
function defaultDeprecationSink(message) {
|
|
2266
|
+
if (typeof process !== "undefined" && process.stderr) {
|
|
2267
|
+
process.stderr.write(`${message}
|
|
2268
|
+
`);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
function resolveCredential(name, env, options = {}) {
|
|
2272
|
+
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
2273
|
+
const diskPaths = credentialDiskSources(name, env);
|
|
2274
|
+
const explicitKey = options.apiKey?.trim();
|
|
2275
|
+
if (explicitKey) {
|
|
2276
|
+
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
2277
|
+
return sealCredential({
|
|
2278
|
+
apiKey: explicitKey,
|
|
2279
|
+
tier: "argument",
|
|
2280
|
+
source: "explicit apiKey argument",
|
|
2281
|
+
deliberate: true,
|
|
2282
|
+
deprecated: false,
|
|
2283
|
+
diskCandidates: diskPaths,
|
|
2284
|
+
warning: null
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
2288
|
+
const overrideRaw = env[overrideKeyName];
|
|
2289
|
+
if (overrideRaw !== undefined) {
|
|
2290
|
+
const override = overrideRaw.trim();
|
|
2291
|
+
if (!override) {
|
|
2292
|
+
throw new CredentialResolutionError(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
|
|
2293
|
+
}
|
|
2294
|
+
assertUsableCredential(name, overrideKeyName, override);
|
|
2295
|
+
return sealCredential({
|
|
2296
|
+
apiKey: override,
|
|
2297
|
+
tier: "override",
|
|
2298
|
+
source: overrideKeyName,
|
|
2299
|
+
deliberate: true,
|
|
2300
|
+
deprecated: false,
|
|
2301
|
+
diskCandidates: diskPaths,
|
|
2302
|
+
warning: null
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
|
|
2306
|
+
if (profile) {
|
|
2307
|
+
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
2308
|
+
if (!SAFE_PROFILE.test(profile)) {
|
|
2309
|
+
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
2310
|
+
}
|
|
2311
|
+
const paths = profileDiskSources(name, env, profile);
|
|
2312
|
+
for (const path of paths) {
|
|
2313
|
+
const value = readCredentialFile(path, apiKeyKeys);
|
|
2314
|
+
if (value) {
|
|
2315
|
+
assertUsableCredential(name, path, value);
|
|
2316
|
+
return sealCredential({
|
|
2317
|
+
apiKey: value,
|
|
2318
|
+
tier: "profile",
|
|
2319
|
+
source: path,
|
|
2320
|
+
deliberate: true,
|
|
2321
|
+
deprecated: false,
|
|
2322
|
+
diskCandidates: paths,
|
|
2323
|
+
warning: null
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
|
|
2328
|
+
}
|
|
2329
|
+
const diskHits = diskPaths.map((path) => ({ path, value: readCredentialFile(path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
2330
|
+
if (diskHits.length > 0) {
|
|
2331
|
+
const winner = diskHits[0];
|
|
2332
|
+
assertUsableCredential(name, winner.path, winner.value);
|
|
2333
|
+
const divergentSources = [
|
|
2334
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
|
|
2335
|
+
...(() => {
|
|
2336
|
+
const legacyHit = firstEnvValue(env, apiKeyKeys);
|
|
2337
|
+
return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
|
|
2338
|
+
})()
|
|
2339
|
+
];
|
|
2340
|
+
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
|
|
2341
|
+
return sealCredential({
|
|
2342
|
+
apiKey: winner.value,
|
|
2343
|
+
tier: "disk",
|
|
2344
|
+
source: winner.path,
|
|
2345
|
+
deliberate: false,
|
|
2346
|
+
deprecated: false,
|
|
2347
|
+
diskCandidates: diskPaths,
|
|
2348
|
+
warning
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
const legacy = firstEnvValue(env, apiKeyKeys);
|
|
2352
|
+
if (legacy) {
|
|
2353
|
+
assertUsableCredential(name, legacy.key, legacy.value);
|
|
2354
|
+
const where = diskPaths.length > 0 ? `Put the current key in ${diskPaths[0]} \u2014 it is re-read on every call, so rotations take effect immediately.` : `This environment has no HOME, so no credential file could be consulted at all; the disk tier is ` + `unavailable here and this process will keep using the environment snapshot.`;
|
|
2355
|
+
const message = `[${name}] DEPRECATED: the API key came from ${legacy.key} in this process's environment. ` + `Environment variables are a snapshot taken when this process started, so a shell that started ` + `before a key rotation keeps using the old key until it exits. ${where}`;
|
|
2356
|
+
const sink = options.onDeprecation ?? defaultDeprecationSink;
|
|
2357
|
+
const notified = deprecationNotified();
|
|
2358
|
+
if (!notified.has(name)) {
|
|
2359
|
+
notified.add(name);
|
|
2360
|
+
sink(message);
|
|
2361
|
+
}
|
|
2362
|
+
return sealCredential({
|
|
2363
|
+
apiKey: legacy.value,
|
|
2364
|
+
tier: "legacy-env",
|
|
2365
|
+
source: legacy.key,
|
|
2366
|
+
deliberate: false,
|
|
2367
|
+
deprecated: true,
|
|
2368
|
+
diskCandidates: diskPaths,
|
|
2369
|
+
warning: message
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
return null;
|
|
2373
|
+
}
|
|
2374
|
+
var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
2375
|
+
var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
2376
|
+
function isValidDnsDomain(value) {
|
|
2377
|
+
if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
|
|
2378
|
+
return false;
|
|
2379
|
+
}
|
|
2380
|
+
return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
|
|
2381
|
+
}
|
|
2382
|
+
function firstEnv(env, keys, options = {}) {
|
|
2383
|
+
for (const key of keys) {
|
|
2384
|
+
const raw = env[key];
|
|
2385
|
+
const value = raw?.trim();
|
|
2386
|
+
if (value)
|
|
2387
|
+
return { key, value: options.preserveRaw ? raw : value };
|
|
2388
|
+
}
|
|
2389
|
+
return null;
|
|
2390
|
+
}
|
|
2391
|
+
function firstEnvDefinedKey(env, keys) {
|
|
2392
|
+
for (const key of keys) {
|
|
2393
|
+
if (env[key] !== undefined)
|
|
2394
|
+
return key;
|
|
2395
|
+
}
|
|
2396
|
+
return null;
|
|
2397
|
+
}
|
|
2398
|
+
function rawAuthority(value) {
|
|
2399
|
+
const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
|
|
2400
|
+
if (!match)
|
|
2401
|
+
throw new Error("API URL must be absolute.");
|
|
2402
|
+
const afterScheme = value.slice(match[0].length);
|
|
2403
|
+
const boundary = afterScheme.search(/[/?#]/);
|
|
2404
|
+
const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
|
|
2405
|
+
if (!authority)
|
|
2406
|
+
throw new Error("API URL must include a hostname.");
|
|
2407
|
+
return authority;
|
|
2408
|
+
}
|
|
2409
|
+
function assertCanonicalPort(port) {
|
|
2410
|
+
if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
|
|
2411
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
2412
|
+
}
|
|
2413
|
+
const numericPort = Number(port);
|
|
2414
|
+
if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
|
|
2415
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
function canonicalAuthorityHostname(authority) {
|
|
2419
|
+
let rawHostname;
|
|
2420
|
+
if (authority.startsWith("[")) {
|
|
2421
|
+
const closingBracket = authority.indexOf("]");
|
|
2422
|
+
if (closingBracket === -1) {
|
|
2423
|
+
throw new Error("API URL authority must contain a canonical hostname.");
|
|
2424
|
+
}
|
|
2425
|
+
rawHostname = authority.slice(0, closingBracket + 1);
|
|
2426
|
+
const portSuffix = authority.slice(closingBracket + 1);
|
|
2427
|
+
if (portSuffix) {
|
|
2428
|
+
if (!portSuffix.startsWith(":")) {
|
|
2429
|
+
throw new Error("API URL authority must contain a canonical hostname and port.");
|
|
2430
|
+
}
|
|
2431
|
+
assertCanonicalPort(portSuffix.slice(1));
|
|
2432
|
+
}
|
|
2433
|
+
if (isIP(rawHostname.slice(1, -1)) !== 6) {
|
|
2434
|
+
throw new Error("API URL authority must contain a canonical IPv6 literal.");
|
|
2435
|
+
}
|
|
2436
|
+
} else {
|
|
2437
|
+
const firstColon = authority.indexOf(":");
|
|
2438
|
+
const lastColon = authority.lastIndexOf(":");
|
|
2439
|
+
if (firstColon !== lastColon) {
|
|
2440
|
+
throw new Error("IPv6 API URL authorities must use brackets.");
|
|
2441
|
+
}
|
|
2442
|
+
if (lastColon !== -1) {
|
|
2443
|
+
const port = authority.slice(lastColon + 1);
|
|
2444
|
+
assertCanonicalPort(port);
|
|
2445
|
+
rawHostname = authority.slice(0, lastColon);
|
|
2446
|
+
} else {
|
|
2447
|
+
rawHostname = authority;
|
|
2448
|
+
}
|
|
2449
|
+
const ipVersion = isIP(rawHostname);
|
|
2450
|
+
const numericAddressParts = rawHostname.split(".");
|
|
2451
|
+
const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
|
|
2452
|
+
if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
|
|
2453
|
+
throw new Error("API URL authority must contain a canonical ASCII hostname.");
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
return rawHostname.toLowerCase();
|
|
2457
|
+
}
|
|
2458
|
+
function isDeliberateLoopbackHttpAuthority(authority) {
|
|
2459
|
+
return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
|
|
2460
|
+
}
|
|
2075
2461
|
function toV1BaseUrl(apiUrl) {
|
|
2076
|
-
|
|
2462
|
+
if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
|
|
2463
|
+
throw new Error("API URL must not contain ASCII control characters.");
|
|
2464
|
+
}
|
|
2465
|
+
const input = apiUrl.trim();
|
|
2466
|
+
const authority = rawAuthority(input);
|
|
2467
|
+
if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
|
|
2468
|
+
throw new Error("API URL authority must be canonical ASCII without credentials.");
|
|
2469
|
+
}
|
|
2470
|
+
const canonicalHostname = canonicalAuthorityHostname(authority);
|
|
2471
|
+
const url = new URL(input);
|
|
2077
2472
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
2078
2473
|
throw new Error("API URL must use http or https.");
|
|
2079
2474
|
}
|
|
2475
|
+
if (url.username || url.password) {
|
|
2476
|
+
throw new Error("API URL must not include credentials.");
|
|
2477
|
+
}
|
|
2478
|
+
if (!url.hostname || url.hostname.endsWith(".")) {
|
|
2479
|
+
throw new Error("API URL must include a canonical hostname.");
|
|
2480
|
+
}
|
|
2481
|
+
if (url.hostname.toLowerCase() !== canonicalHostname) {
|
|
2482
|
+
throw new Error("API URL authority must not rely on parser hostname normalization.");
|
|
2483
|
+
}
|
|
2484
|
+
if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
|
|
2485
|
+
throw new Error("API URL must not use IDN or punycode hostnames.");
|
|
2486
|
+
}
|
|
2487
|
+
if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
|
|
2488
|
+
throw new Error("API URL may use http only for an exact loopback authority.");
|
|
2489
|
+
}
|
|
2490
|
+
if (url.search || url.hash) {
|
|
2491
|
+
throw new Error("API URL must not include a query string or fragment.");
|
|
2492
|
+
}
|
|
2080
2493
|
let path = url.pathname.replace(/\/+$/, "");
|
|
2081
2494
|
if (path.endsWith("/v1"))
|
|
2082
2495
|
path = path.slice(0, -"/v1".length);
|
|
2083
2496
|
url.pathname = `${path}/v1`;
|
|
2084
|
-
url.search = "";
|
|
2085
|
-
url.hash = "";
|
|
2086
2497
|
return url.toString().replace(/\/+$/, "");
|
|
2087
2498
|
}
|
|
2088
|
-
function
|
|
2089
|
-
const keys =
|
|
2090
|
-
const
|
|
2091
|
-
const
|
|
2499
|
+
function resolveClientTransport(name, env = process.env, options = {}) {
|
|
2500
|
+
const keys = clientTransportEnvKeys(name);
|
|
2501
|
+
const envUrlHit = firstEnv(env, keys.apiUrlKeys, { preserveRaw: true });
|
|
2502
|
+
const explicitLocalKey = envUrlHit ? null : firstEnvDefinedKey(env, keys.apiUrlKeys);
|
|
2503
|
+
const diskUrlHit = envUrlHit || explicitLocalKey ? null : appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
2504
|
+
const urlHit = envUrlHit ?? (diskUrlHit ? { key: diskUrlHit.path, value: diskUrlHit.value } : null);
|
|
2092
2505
|
const keyHit = firstEnv(env, keys.apiKeyKeys);
|
|
2093
|
-
|
|
2094
|
-
let modeSource = "default";
|
|
2095
|
-
if (storeHit) {
|
|
2096
|
-
requested = normalizeClientStore(storeHit.value);
|
|
2097
|
-
modeSource = storeHit.key;
|
|
2098
|
-
} else if (urlHit && keyHit) {
|
|
2099
|
-
requested = "http";
|
|
2100
|
-
modeSource = "auto:api-url+api-key";
|
|
2101
|
-
} else if (urlHit || keyHit) {
|
|
2102
|
-
const missing = urlHit ? keys.apiKeyKeys[0] : keys.apiUrlKeys[0];
|
|
2103
|
-
const present = urlHit ? keys.apiUrlKeys[0] : keys.apiKeyKeys[0];
|
|
2104
|
-
return {
|
|
2105
|
-
transport: "sqlite",
|
|
2106
|
-
requested,
|
|
2107
|
-
modeSource,
|
|
2108
|
-
baseUrl: null,
|
|
2109
|
-
apiKeyPresent: Boolean(keyHit),
|
|
2110
|
-
misconfigured: true,
|
|
2111
|
-
warning: `${present} is set but ${missing} is not: the hosted API is only ` + `selected when BOTH are present. Set ${missing}, or unset ${present} to ` + `use the on-box store.`
|
|
2112
|
-
};
|
|
2113
|
-
}
|
|
2114
|
-
if (requested === "sqlite") {
|
|
2115
|
-
return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: Boolean(keyHit), misconfigured: false, warning: null };
|
|
2116
|
-
}
|
|
2506
|
+
const warnings = [];
|
|
2117
2507
|
if (!urlHit) {
|
|
2508
|
+
if (explicitLocalKey) {
|
|
2509
|
+
const overriddenPointer = appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
2510
|
+
if (overriddenPointer) {
|
|
2511
|
+
warnings.push(`${explicitLocalKey} is defined but blank, which selects the local store. ` + `The server URL in ${overriddenPointer.path} was NOT selected: an explicit blank wins over a disk pointer.`);
|
|
2512
|
+
}
|
|
2513
|
+
return {
|
|
2514
|
+
transport: "sqlite",
|
|
2515
|
+
transportSource: explicitLocalKey,
|
|
2516
|
+
baseUrl: null,
|
|
2517
|
+
apiUrlSource: null,
|
|
2518
|
+
apiKeyPresent: Boolean(keyHit),
|
|
2519
|
+
apiKeySource: keyHit ? keyHit.key : null,
|
|
2520
|
+
apiKeyTier: null,
|
|
2521
|
+
misconfigured: false,
|
|
2522
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2118
2525
|
return {
|
|
2119
2526
|
transport: "sqlite",
|
|
2120
|
-
|
|
2121
|
-
modeSource,
|
|
2527
|
+
transportSource: "default",
|
|
2122
2528
|
baseUrl: null,
|
|
2529
|
+
apiUrlSource: null,
|
|
2123
2530
|
apiKeyPresent: Boolean(keyHit),
|
|
2124
|
-
|
|
2125
|
-
|
|
2531
|
+
apiKeySource: keyHit ? keyHit.key : null,
|
|
2532
|
+
apiKeyTier: null,
|
|
2533
|
+
misconfigured: false,
|
|
2534
|
+
warning: null
|
|
2126
2535
|
};
|
|
2127
2536
|
}
|
|
2128
|
-
if (
|
|
2537
|
+
if (diskUrlHit) {
|
|
2538
|
+
warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${diskUrlHit.path} was used, so this client connects to the server. ` + `Unset the pointer or remove the file to stay on the local store.`);
|
|
2539
|
+
}
|
|
2540
|
+
const credential = resolveCredential(name, env, options.credentials);
|
|
2541
|
+
if (!credential) {
|
|
2542
|
+
const diskHint = credentialDiskSourcesForMessage(name, env);
|
|
2543
|
+
warnings.push(`${urlHit.key} selects the HTTP server for '${name}', but no API key could be resolved; ` + `refusing to route and leaving the local sqlite store selected. ` + `Looked for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
|
|
2129
2544
|
return {
|
|
2130
2545
|
transport: "sqlite",
|
|
2131
|
-
|
|
2132
|
-
modeSource,
|
|
2546
|
+
transportSource: urlHit.key,
|
|
2133
2547
|
baseUrl: null,
|
|
2548
|
+
apiUrlSource: urlHit.key,
|
|
2134
2549
|
apiKeyPresent: false,
|
|
2550
|
+
apiKeySource: null,
|
|
2551
|
+
apiKeyTier: null,
|
|
2135
2552
|
misconfigured: true,
|
|
2136
|
-
warning:
|
|
2553
|
+
warning: warnings.join(" ")
|
|
2137
2554
|
};
|
|
2138
2555
|
}
|
|
2139
|
-
|
|
2556
|
+
if (credential.warning)
|
|
2557
|
+
warnings.push(credential.warning);
|
|
2558
|
+
const apiUrlSource = urlHit.key;
|
|
2140
2559
|
let baseUrl;
|
|
2141
2560
|
try {
|
|
2142
|
-
baseUrl = toV1BaseUrl(
|
|
2561
|
+
baseUrl = toV1BaseUrl(urlHit.value);
|
|
2143
2562
|
} catch (error) {
|
|
2144
2563
|
const message = error instanceof Error ? error.message : String(error);
|
|
2145
|
-
|
|
2564
|
+
warnings.push(`Invalid API URL from ${apiUrlSource}: ${message}. Using local store.`);
|
|
2565
|
+
return {
|
|
2566
|
+
transport: "sqlite",
|
|
2567
|
+
transportSource: urlHit.key,
|
|
2568
|
+
baseUrl: null,
|
|
2569
|
+
apiUrlSource: urlHit.key,
|
|
2570
|
+
apiKeyPresent: true,
|
|
2571
|
+
apiKeySource: credential.source,
|
|
2572
|
+
apiKeyTier: credential.tier,
|
|
2573
|
+
misconfigured: true,
|
|
2574
|
+
warning: warnings.join(" ")
|
|
2575
|
+
};
|
|
2146
2576
|
}
|
|
2147
|
-
return {
|
|
2577
|
+
return {
|
|
2578
|
+
transport: "http",
|
|
2579
|
+
transportSource: urlHit.key,
|
|
2580
|
+
baseUrl,
|
|
2581
|
+
apiUrlSource,
|
|
2582
|
+
apiKeyPresent: true,
|
|
2583
|
+
apiKeySource: credential.source,
|
|
2584
|
+
apiKeyTier: credential.tier,
|
|
2585
|
+
misconfigured: false,
|
|
2586
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
function credentialDiskSourcesForMessage(name, env) {
|
|
2590
|
+
const paths = credentialDiskSources(name, env);
|
|
2591
|
+
return paths.length > 0 ? paths.join(" or ") : "<no HOME set in this environment, so no credential file was consulted>";
|
|
2148
2592
|
}
|
|
2149
2593
|
|
|
2150
2594
|
class HasnaHttpError extends Error {
|
|
@@ -2152,49 +2596,113 @@ class HasnaHttpError extends Error {
|
|
|
2152
2596
|
method;
|
|
2153
2597
|
path;
|
|
2154
2598
|
body;
|
|
2155
|
-
|
|
2156
|
-
|
|
2599
|
+
credentialSource;
|
|
2600
|
+
credentialTier;
|
|
2601
|
+
constructor(method, path, status, body, credential) {
|
|
2602
|
+
const guidance = credential ? `. ${credential.guidance}` : "";
|
|
2603
|
+
super(`Hasna cloud request failed: ${method} ${path} -> ${status}${guidance}`);
|
|
2157
2604
|
this.name = "HasnaHttpError";
|
|
2158
2605
|
this.status = status;
|
|
2159
2606
|
this.method = method;
|
|
2160
2607
|
this.path = path;
|
|
2161
2608
|
this.body = body;
|
|
2609
|
+
this.credentialSource = credential?.source ?? null;
|
|
2610
|
+
this.credentialTier = credential?.tier ?? null;
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
function currentCredential(name, apiKey) {
|
|
2614
|
+
if (typeof apiKey === "function") {
|
|
2615
|
+
return validateAndSealResolvedCredential(name, apiKey());
|
|
2616
|
+
}
|
|
2617
|
+
return explicitCredential(name, apiKey);
|
|
2618
|
+
}
|
|
2619
|
+
function authFailureGuidance(credential) {
|
|
2620
|
+
const origin = `The API key for this request came from ${credential.source}`;
|
|
2621
|
+
if (credential.deliberate) {
|
|
2622
|
+
const remedy = credential.source === CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE ? `Fix that provider so it returns the current key, or replace it with resolveCredential() ` + `so diagnostics can name the original source.` : `Rotate that key, or unset the override to use the credential on disk.`;
|
|
2623
|
+
return `${origin} \u2014 a credential you selected deliberately. It was NOT substituted with any other key: ` + `falling back here would authenticate as a different principal than the one you named, which is ` + `exactly the failure an override exists to prevent. ${remedy}`;
|
|
2624
|
+
}
|
|
2625
|
+
if (credential.deprecated) {
|
|
2626
|
+
const target = credential.diskCandidates[0];
|
|
2627
|
+
const remedy = target ? `Write the CURRENT key to ${target} \u2014 that file is re-read on every call, so rotations take ` + `effect immediately and in every shell. Do not simply unset ${credential.source}: nothing was ` + `found on disk, so that would leave this client with no credential at all.` : `This environment has no HOME, so no credential file could be consulted; the disk tier is ` + `unavailable here and there is nothing to fall back to. Set HOME, or supply the key explicitly.`;
|
|
2628
|
+
return `${origin}, a variable in this process's environment \u2014 which is a snapshot taken when the process ` + `started. A STALE SHELL is the most common cause of this error: this shell exported the key before ` + `it was rotated, and will keep sending the old one until it exits. ${remedy}`;
|
|
2629
|
+
}
|
|
2630
|
+
return `${origin}, which was re-read from disk on this very call \u2014 so a stale shell is NOT the cause here. ` + `The stored credential is genuinely being rejected: rotate it, or re-run the fleet key distribution ` + `so this machine gets the current key.`;
|
|
2631
|
+
}
|
|
2632
|
+
var DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
|
|
2633
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
2634
|
+
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
2635
|
+
"host",
|
|
2636
|
+
":authority",
|
|
2637
|
+
"forwarded",
|
|
2638
|
+
"x-forwarded-host",
|
|
2639
|
+
"x-original-host"
|
|
2640
|
+
]);
|
|
2641
|
+
function assertNoAuthorityOverrideHeaders(headers, source) {
|
|
2642
|
+
if (!headers)
|
|
2643
|
+
return;
|
|
2644
|
+
const forbidden = Object.keys(headers).find((name) => AUTHORITY_OVERRIDE_HEADERS.has(name.trim().toLowerCase()));
|
|
2645
|
+
if (forbidden) {
|
|
2646
|
+
throw new Error(`Authenticated ${source} headers must not set authority header '${forbidden}'.`);
|
|
2162
2647
|
}
|
|
2163
2648
|
}
|
|
2164
2649
|
function appendQuery(path, query) {
|
|
2165
2650
|
if (!query)
|
|
2166
2651
|
return path;
|
|
2167
|
-
const params = new URLSearchParams;
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2652
|
+
const params = query instanceof URLSearchParams ? query : new URLSearchParams;
|
|
2653
|
+
if (!(query instanceof URLSearchParams)) {
|
|
2654
|
+
for (const [key, value] of Object.entries(query)) {
|
|
2655
|
+
if (value === null || value === undefined)
|
|
2656
|
+
continue;
|
|
2657
|
+
if (Array.isArray(value)) {
|
|
2658
|
+
for (const v of value)
|
|
2659
|
+
params.append(key, String(v));
|
|
2660
|
+
} else {
|
|
2661
|
+
params.append(key, String(value));
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2176
2664
|
}
|
|
2177
2665
|
const qs = params.toString();
|
|
2178
|
-
|
|
2666
|
+
if (!qs)
|
|
2667
|
+
return path;
|
|
2668
|
+
return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
|
|
2179
2669
|
}
|
|
2180
|
-
var
|
|
2181
|
-
|
|
2182
|
-
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2183
|
-
function createHttpTransport(options) {
|
|
2670
|
+
var defaultSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2671
|
+
function createHasnaHttpTransport(options) {
|
|
2184
2672
|
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
2185
|
-
const base = options.baseUrl
|
|
2673
|
+
const base = toV1BaseUrl(options.baseUrl);
|
|
2186
2674
|
const timeoutMs = options.timeoutMs ?? 30000;
|
|
2187
2675
|
const sleep = options.sleepImpl ?? defaultSleep;
|
|
2188
|
-
|
|
2676
|
+
const defaultRetry = options.retry;
|
|
2677
|
+
function resolveRetry(callRetry) {
|
|
2678
|
+
const chosen = callRetry !== undefined ? callRetry : defaultRetry;
|
|
2679
|
+
if (chosen === false)
|
|
2680
|
+
return null;
|
|
2681
|
+
const r = chosen ?? {};
|
|
2682
|
+
return {
|
|
2683
|
+
retries: r.retries ?? 2,
|
|
2684
|
+
baseDelayMs: r.baseDelayMs ?? 200,
|
|
2685
|
+
maxDelayMs: r.maxDelayMs ?? 2000,
|
|
2686
|
+
retryStatuses: r.retryStatuses ?? [...DEFAULT_RETRY_STATUSES]
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
async function once(method, rel, url, body, opts, credential) {
|
|
2690
|
+
assertNoAuthorityOverrideHeaders(options.headers, "transport");
|
|
2691
|
+
assertNoAuthorityOverrideHeaders(opts.headers, "request");
|
|
2189
2692
|
const headers = {
|
|
2190
|
-
"x-api-key":
|
|
2191
|
-
Authorization: `Bearer ${
|
|
2693
|
+
"x-api-key": credential.apiKey,
|
|
2694
|
+
Authorization: `Bearer ${credential.apiKey}`,
|
|
2192
2695
|
Accept: "application/json",
|
|
2696
|
+
...options.headers ?? {},
|
|
2193
2697
|
...opts.headers ?? {}
|
|
2194
2698
|
};
|
|
2195
2699
|
if (opts.idempotencyKey)
|
|
2196
2700
|
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
2197
|
-
const init = {
|
|
2701
|
+
const init = {
|
|
2702
|
+
method,
|
|
2703
|
+
headers,
|
|
2704
|
+
redirect: "manual"
|
|
2705
|
+
};
|
|
2198
2706
|
if (body !== undefined) {
|
|
2199
2707
|
headers["Content-Type"] = "application/json";
|
|
2200
2708
|
init.body = JSON.stringify(body);
|
|
@@ -2232,7 +2740,27 @@ function createHttpTransport(options) {
|
|
|
2232
2740
|
}
|
|
2233
2741
|
}
|
|
2234
2742
|
if (!response.ok) {
|
|
2235
|
-
|
|
2743
|
+
if (response.status >= 300 && response.status < 400) {
|
|
2744
|
+
return {
|
|
2745
|
+
ok: false,
|
|
2746
|
+
retryable: false,
|
|
2747
|
+
error: new HasnaHttpError(method, rel, response.status, parsed)
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
if (response.status === 401 || response.status === 403) {
|
|
2751
|
+
return {
|
|
2752
|
+
ok: false,
|
|
2753
|
+
retryable: false,
|
|
2754
|
+
error: new HasnaHttpError(method, rel, response.status, parsed, {
|
|
2755
|
+
source: credential.source,
|
|
2756
|
+
tier: credential.tier,
|
|
2757
|
+
guidance: authFailureGuidance(credential)
|
|
2758
|
+
})
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
const retry = resolveRetry(opts.retry);
|
|
2762
|
+
const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
|
|
2763
|
+
return { ok: false, retryable, error: new HasnaHttpError(method, rel, response.status, parsed) };
|
|
2236
2764
|
}
|
|
2237
2765
|
return { ok: true, value: parsed };
|
|
2238
2766
|
}
|
|
@@ -2240,24 +2768,23 @@ function createHttpTransport(options) {
|
|
|
2240
2768
|
const upper = method.toUpperCase();
|
|
2241
2769
|
const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
|
|
2242
2770
|
const url = `${base}${rel}`;
|
|
2243
|
-
const
|
|
2244
|
-
const
|
|
2245
|
-
const maxAttempts = methodRetryable ?
|
|
2771
|
+
const retry = resolveRetry(opts.retry);
|
|
2772
|
+
const methodRetryable = IDEMPOTENT_METHODS.has(upper) || Boolean(opts.idempotencyKey);
|
|
2773
|
+
const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
|
|
2774
|
+
const credential = currentCredential(options.name, options.apiKey);
|
|
2246
2775
|
let last = null;
|
|
2247
2776
|
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
2248
|
-
const result = await once(upper, rel, url, body, opts);
|
|
2777
|
+
const result = await once(upper, rel, url, body, opts, credential);
|
|
2249
2778
|
if (result.ok)
|
|
2250
2779
|
return result.value;
|
|
2251
2780
|
last = result;
|
|
2252
|
-
const canRetry = methodRetryable && result.retryable && attempt < maxAttempts;
|
|
2781
|
+
const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
|
|
2253
2782
|
if (!canRetry)
|
|
2254
2783
|
break;
|
|
2255
|
-
const backoff = Math.min(
|
|
2784
|
+
const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
|
|
2256
2785
|
const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
|
|
2257
2786
|
await sleep(backoff + jitter);
|
|
2258
2787
|
}
|
|
2259
|
-
if (last === null)
|
|
2260
|
-
throw new Error(`Request to ${rel} completed without a result`);
|
|
2261
2788
|
throw last.error;
|
|
2262
2789
|
}
|
|
2263
2790
|
return {
|
|
@@ -2265,81 +2792,289 @@ function createHttpTransport(options) {
|
|
|
2265
2792
|
request,
|
|
2266
2793
|
get: (path, opts) => request("GET", path, undefined, opts),
|
|
2267
2794
|
post: (path, body, opts) => request("POST", path, body, opts),
|
|
2268
|
-
patch: (path, body, opts) => request("PATCH", path, body, opts),
|
|
2269
2795
|
put: (path, body, opts) => request("PUT", path, body, opts),
|
|
2796
|
+
patch: (path, body, opts) => request("PATCH", path, body, opts),
|
|
2270
2797
|
del: (path, body, opts) => request("DELETE", path, body, opts)
|
|
2271
2798
|
};
|
|
2272
2799
|
}
|
|
2800
|
+
function createClientTransport(name, env = process.env, overrides) {
|
|
2801
|
+
const credentialOptions = overrides?.credentials;
|
|
2802
|
+
const resolution = resolveClientTransport(name, env, { ...credentialOptions ? { credentials: credentialOptions } : {} });
|
|
2803
|
+
if (resolution.misconfigured) {
|
|
2804
|
+
throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for the API client.`);
|
|
2805
|
+
}
|
|
2806
|
+
if (resolution.transport === "sqlite" || !resolution.baseUrl) {
|
|
2807
|
+
return { transport: "sqlite", client: null, resolution };
|
|
2808
|
+
}
|
|
2809
|
+
const credentialProvider = () => {
|
|
2810
|
+
const resolved = resolveCredential(name, env, credentialOptions);
|
|
2811
|
+
if (!resolved) {
|
|
2812
|
+
throw new Error(`Client for '${name}' resolved to the http transport but no API key is available any more. ` + `Looked at ${credentialDiskSourcesForMessage(name, env)}, then the environment. ` + `A credential file that was removed after this client was built is the usual cause.`);
|
|
2813
|
+
}
|
|
2814
|
+
return resolved;
|
|
2815
|
+
};
|
|
2816
|
+
return {
|
|
2817
|
+
transport: "http",
|
|
2818
|
+
client: createHasnaHttpTransport({
|
|
2819
|
+
name,
|
|
2820
|
+
baseUrl: resolution.baseUrl,
|
|
2821
|
+
apiKey: credentialProvider,
|
|
2822
|
+
...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
|
|
2823
|
+
...overrides?.headers ? { headers: overrides.headers } : {},
|
|
2824
|
+
...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
|
|
2825
|
+
...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
|
|
2826
|
+
...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
|
|
2827
|
+
}),
|
|
2828
|
+
resolution
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
// ../contracts/dist/client/storage.js
|
|
2833
|
+
var MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
|
|
2834
|
+
var INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
|
|
2835
|
+
var CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
|
|
2836
|
+
var DEPRECATION_REGISTRY2 = Symbol.for("hasna:contracts:credentialDeprecationNotices");
|
|
2837
|
+
var IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
2838
|
+
var AUTHORITY_OVERRIDE_HEADERS2 = new Set([
|
|
2839
|
+
"host",
|
|
2840
|
+
":authority",
|
|
2841
|
+
"forwarded",
|
|
2842
|
+
"x-forwarded-host",
|
|
2843
|
+
"x-original-host"
|
|
2844
|
+
]);
|
|
2845
|
+
function resourcePath(resource) {
|
|
2846
|
+
const trimmed = resource.replace(/^\/+|\/+$/g, "");
|
|
2847
|
+
if (!trimmed)
|
|
2848
|
+
throw new Error("resource must be a non-empty path segment");
|
|
2849
|
+
return `/${trimmed}`;
|
|
2850
|
+
}
|
|
2851
|
+
function entityPath(resource, id) {
|
|
2852
|
+
if (id === undefined || id === null || `${id}`.length === 0) {
|
|
2853
|
+
throw new Error("id must be a non-empty string");
|
|
2854
|
+
}
|
|
2855
|
+
return `${resourcePath(resource)}/${encodeURIComponent(String(id))}`;
|
|
2856
|
+
}
|
|
2273
2857
|
function newIdempotencyKey() {
|
|
2274
2858
|
const g = globalThis;
|
|
2275
2859
|
if (g.crypto?.randomUUID)
|
|
2276
2860
|
return g.crypto.randomUUID();
|
|
2277
2861
|
return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
2278
2862
|
}
|
|
2279
|
-
function extractItems(raw
|
|
2863
|
+
function extractItems(raw) {
|
|
2280
2864
|
if (Array.isArray(raw))
|
|
2281
2865
|
return raw;
|
|
2282
2866
|
if (raw && typeof raw === "object") {
|
|
2283
2867
|
const obj = raw;
|
|
2284
|
-
for (const key of [
|
|
2868
|
+
for (const key of ["items", "data", "results", "rows", "records"]) {
|
|
2285
2869
|
if (Array.isArray(obj[key]))
|
|
2286
2870
|
return obj[key];
|
|
2287
2871
|
}
|
|
2288
2872
|
}
|
|
2289
2873
|
return [];
|
|
2290
2874
|
}
|
|
2291
|
-
function
|
|
2292
|
-
|
|
2293
|
-
|
|
2875
|
+
function extractTotal(raw) {
|
|
2876
|
+
if (raw && typeof raw === "object") {
|
|
2877
|
+
const obj = raw;
|
|
2878
|
+
for (const key of ["total", "count", "totalCount", "total_count"]) {
|
|
2879
|
+
if (typeof obj[key] === "number")
|
|
2880
|
+
return obj[key];
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
return null;
|
|
2884
|
+
}
|
|
2885
|
+
function extractCursor(raw) {
|
|
2886
|
+
if (raw && typeof raw === "object") {
|
|
2887
|
+
const obj = raw;
|
|
2888
|
+
for (const key of ["cursor", "nextCursor", "next_cursor", "next"]) {
|
|
2889
|
+
if (typeof obj[key] === "string")
|
|
2890
|
+
return obj[key];
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
return null;
|
|
2894
|
+
}
|
|
2895
|
+
function isNotFoundHttpError(error) {
|
|
2896
|
+
return typeof error === "object" && error !== null && error.name === "HasnaHttpError" && error.status === 404;
|
|
2897
|
+
}
|
|
2898
|
+
function createHasnaStorageClient(name, transport) {
|
|
2294
2899
|
return {
|
|
2295
2900
|
name,
|
|
2296
2901
|
baseUrl: transport.baseUrl,
|
|
2297
2902
|
transport,
|
|
2298
|
-
async list(resource,
|
|
2299
|
-
const raw = await transport.get(
|
|
2300
|
-
return {
|
|
2903
|
+
async list(resource, options = {}) {
|
|
2904
|
+
const raw = await transport.get(resourcePath(resource), options);
|
|
2905
|
+
return {
|
|
2906
|
+
items: extractItems(raw),
|
|
2907
|
+
total: extractTotal(raw),
|
|
2908
|
+
cursor: extractCursor(raw),
|
|
2909
|
+
raw
|
|
2910
|
+
};
|
|
2301
2911
|
},
|
|
2302
|
-
async get(resource, id) {
|
|
2912
|
+
async get(resource, id, options = {}) {
|
|
2303
2913
|
try {
|
|
2304
|
-
return await transport.get(
|
|
2914
|
+
return await transport.get(entityPath(resource, id), options);
|
|
2305
2915
|
} catch (error) {
|
|
2306
|
-
if (error
|
|
2916
|
+
if (isNotFoundHttpError(error))
|
|
2307
2917
|
return null;
|
|
2308
2918
|
throw error;
|
|
2309
2919
|
}
|
|
2310
2920
|
},
|
|
2311
|
-
async create(resource, body,
|
|
2312
|
-
|
|
2921
|
+
async create(resource, body, options = {}) {
|
|
2922
|
+
const { idempotencyKey, ...rest } = options;
|
|
2923
|
+
return transport.post(resourcePath(resource), body, {
|
|
2924
|
+
...rest,
|
|
2925
|
+
idempotencyKey: idempotencyKey ?? newIdempotencyKey()
|
|
2926
|
+
});
|
|
2313
2927
|
},
|
|
2314
|
-
async update(resource, id, patch,
|
|
2928
|
+
async update(resource, id, patch, options = {}) {
|
|
2929
|
+
const { method = "PATCH", idempotencyKey, ...rest } = options;
|
|
2315
2930
|
const call = method === "PUT" ? transport.put : transport.patch;
|
|
2316
|
-
return call(
|
|
2931
|
+
return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
|
|
2317
2932
|
},
|
|
2318
|
-
async delete(resource, id) {
|
|
2933
|
+
async delete(resource, id, options = {}) {
|
|
2319
2934
|
try {
|
|
2320
|
-
await transport.del(
|
|
2935
|
+
await transport.del(entityPath(resource, id), undefined, options);
|
|
2321
2936
|
} catch (error) {
|
|
2322
|
-
if (error
|
|
2937
|
+
if (isNotFoundHttpError(error))
|
|
2323
2938
|
return;
|
|
2324
2939
|
throw error;
|
|
2325
2940
|
}
|
|
2326
2941
|
}
|
|
2327
2942
|
};
|
|
2328
2943
|
}
|
|
2329
|
-
|
|
2944
|
+
|
|
2945
|
+
// src/http/client.ts
|
|
2946
|
+
function envToken2(name) {
|
|
2947
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
2948
|
+
}
|
|
2949
|
+
function envKeys(name) {
|
|
2950
|
+
const token = envToken2(name);
|
|
2951
|
+
return {
|
|
2952
|
+
storeKeys: [`HASNA_${token}_CLIENT_STORE`, `${token}_CLIENT_STORE`],
|
|
2953
|
+
apiUrlKeys: [`HASNA_${token}_API_URL`],
|
|
2954
|
+
apiKeyKeys: [`HASNA_${token}_API_KEY`]
|
|
2955
|
+
};
|
|
2956
|
+
}
|
|
2957
|
+
function normalizeClientStore(value) {
|
|
2958
|
+
const normalized = value.trim().toLowerCase();
|
|
2959
|
+
if (normalized === "sqlite")
|
|
2960
|
+
return "sqlite";
|
|
2961
|
+
if (normalized === "http" || normalized === "https")
|
|
2962
|
+
return "http";
|
|
2963
|
+
throw new Error(`Unknown client store: ${value}. Use sqlite or http.`);
|
|
2964
|
+
}
|
|
2965
|
+
function firstEnv2(env, keys) {
|
|
2966
|
+
for (const key of keys) {
|
|
2967
|
+
const value = env[key]?.trim();
|
|
2968
|
+
if (value)
|
|
2969
|
+
return { key, value };
|
|
2970
|
+
}
|
|
2971
|
+
return null;
|
|
2972
|
+
}
|
|
2973
|
+
function toV1BaseUrl2(apiUrl) {
|
|
2974
|
+
const url = new URL(apiUrl);
|
|
2975
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
2976
|
+
throw new Error("API URL must use http or https.");
|
|
2977
|
+
}
|
|
2978
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
2979
|
+
if (path.endsWith("/v1"))
|
|
2980
|
+
path = path.slice(0, -"/v1".length);
|
|
2981
|
+
url.pathname = `${path}/v1`;
|
|
2982
|
+
url.search = "";
|
|
2983
|
+
url.hash = "";
|
|
2984
|
+
return url.toString().replace(/\/+$/, "");
|
|
2985
|
+
}
|
|
2986
|
+
function resolveTransport(name, env = process.env) {
|
|
2987
|
+
const keys = envKeys(name);
|
|
2988
|
+
const storeHit = firstEnv2(env, keys.storeKeys);
|
|
2989
|
+
const urlHit = firstEnv2(env, keys.apiUrlKeys);
|
|
2990
|
+
const keyHit = firstEnv2(env, keys.apiKeyKeys);
|
|
2991
|
+
let requested = "sqlite";
|
|
2992
|
+
let modeSource = "default";
|
|
2993
|
+
if (storeHit) {
|
|
2994
|
+
requested = normalizeClientStore(storeHit.value);
|
|
2995
|
+
modeSource = storeHit.key;
|
|
2996
|
+
} else if (urlHit && keyHit) {
|
|
2997
|
+
requested = "http";
|
|
2998
|
+
modeSource = "auto:api-url+api-key";
|
|
2999
|
+
} else if (urlHit || keyHit) {
|
|
3000
|
+
const missing = urlHit ? keys.apiKeyKeys[0] : keys.apiUrlKeys[0];
|
|
3001
|
+
const present = urlHit ? keys.apiUrlKeys[0] : keys.apiKeyKeys[0];
|
|
3002
|
+
return {
|
|
3003
|
+
transport: "sqlite",
|
|
3004
|
+
requested,
|
|
3005
|
+
modeSource,
|
|
3006
|
+
baseUrl: null,
|
|
3007
|
+
apiKeyPresent: Boolean(keyHit),
|
|
3008
|
+
misconfigured: true,
|
|
3009
|
+
warning: `${present} is set but ${missing} is not: the hosted API is only ` + `selected when BOTH are present. Set ${missing}, or unset ${present} to ` + `use the on-box store.`
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
3012
|
+
if (requested === "sqlite") {
|
|
3013
|
+
return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: Boolean(keyHit), misconfigured: false, warning: null };
|
|
3014
|
+
}
|
|
3015
|
+
if (!urlHit) {
|
|
3016
|
+
return {
|
|
3017
|
+
transport: "sqlite",
|
|
3018
|
+
requested,
|
|
3019
|
+
modeSource,
|
|
3020
|
+
baseUrl: null,
|
|
3021
|
+
apiKeyPresent: Boolean(keyHit),
|
|
3022
|
+
misconfigured: true,
|
|
3023
|
+
warning: `${modeSource}=http but no API URL is set (${keys.apiUrlKeys[0]}). Refusing to route to the API.`
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
3026
|
+
if (!keyHit) {
|
|
3027
|
+
return {
|
|
3028
|
+
transport: "sqlite",
|
|
3029
|
+
requested,
|
|
3030
|
+
modeSource,
|
|
3031
|
+
baseUrl: null,
|
|
3032
|
+
apiKeyPresent: false,
|
|
3033
|
+
misconfigured: true,
|
|
3034
|
+
warning: `${modeSource}=http but no API key is set (${keys.apiKeyKeys[0]}). Refusing to route to the API.`
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
const rawUrl = urlHit.value;
|
|
3038
|
+
let baseUrl;
|
|
3039
|
+
try {
|
|
3040
|
+
baseUrl = toV1BaseUrl2(rawUrl);
|
|
3041
|
+
} catch (error) {
|
|
3042
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3043
|
+
return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: true, misconfigured: true, warning: `Invalid API URL: ${message}.` };
|
|
3044
|
+
}
|
|
3045
|
+
return { transport: "http", requested, modeSource, baseUrl, apiKeyPresent: true, misconfigured: false, warning: null };
|
|
3046
|
+
}
|
|
3047
|
+
var RETRY_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
3048
|
+
var IDEMPOTENT = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
3049
|
+
function resolveStoreClient(name, env = process.env) {
|
|
2330
3050
|
const resolution = resolveTransport(name, env);
|
|
2331
3051
|
if (resolution.misconfigured) {
|
|
3052
|
+
const wired2 = createClientTransport(name, env);
|
|
3053
|
+
if (wired2.transport === "http") {
|
|
3054
|
+
return {
|
|
3055
|
+
transport: "http",
|
|
3056
|
+
client: createHasnaStorageClient(name, wired2.client),
|
|
3057
|
+
resolution: {
|
|
3058
|
+
transport: "http",
|
|
3059
|
+
requested: "http",
|
|
3060
|
+
modeSource: resolution.modeSource === "default" ? "auto:api-url+seam-credential" : resolution.modeSource,
|
|
3061
|
+
baseUrl: wired2.resolution.baseUrl,
|
|
3062
|
+
apiKeyPresent: true,
|
|
3063
|
+
misconfigured: false,
|
|
3064
|
+
warning: null
|
|
3065
|
+
}
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
2332
3068
|
throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for the /v1 API.`);
|
|
2333
3069
|
}
|
|
2334
3070
|
if (resolution.transport === "sqlite" || !resolution.baseUrl) {
|
|
2335
3071
|
return { transport: "sqlite", client: null, resolution };
|
|
2336
3072
|
}
|
|
2337
|
-
const
|
|
2338
|
-
|
|
2339
|
-
if (!apiKey)
|
|
3073
|
+
const wired = createClientTransport(name, env);
|
|
3074
|
+
if (wired.transport !== "http") {
|
|
2340
3075
|
throw new Error(`Client for '${name}' resolved to the /v1 API without an API key.`);
|
|
2341
|
-
|
|
2342
|
-
return { transport: "http", client:
|
|
3076
|
+
}
|
|
3077
|
+
return { transport: "http", client: createHasnaStorageClient(name, wired.client), resolution };
|
|
2343
3078
|
}
|
|
2344
3079
|
|
|
2345
3080
|
// src/store.ts
|
|
@@ -2422,6 +3157,22 @@ var localStore = {
|
|
|
2422
3157
|
await withLocalStoreReaderLease(() => saveFeedback(input));
|
|
2423
3158
|
}
|
|
2424
3159
|
};
|
|
3160
|
+
async function listResource(client, resource, query) {
|
|
3161
|
+
const raw = await client.transport.get(`/${resource}`, query ? { query } : undefined);
|
|
3162
|
+
return { items: extractEnvelopeItems(raw, resource), raw };
|
|
3163
|
+
}
|
|
3164
|
+
function extractEnvelopeItems(raw, resource) {
|
|
3165
|
+
if (Array.isArray(raw))
|
|
3166
|
+
return raw;
|
|
3167
|
+
if (raw && typeof raw === "object") {
|
|
3168
|
+
const obj = raw;
|
|
3169
|
+
for (const key of [resource, "items", "data", "results", "rows", "records"]) {
|
|
3170
|
+
if (Array.isArray(obj[key]))
|
|
3171
|
+
return obj[key];
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
return [];
|
|
3175
|
+
}
|
|
2425
3176
|
function apiStore(client) {
|
|
2426
3177
|
return {
|
|
2427
3178
|
mode: "http",
|
|
@@ -2429,7 +3180,7 @@ function apiStore(client) {
|
|
|
2429
3180
|
async createRecording(input, idempotencyKey) {
|
|
2430
3181
|
const keyCandidate = idempotencyKey === undefined && (input.id === undefined || input.id === null) ? randomUUID5() : idempotencyKey;
|
|
2431
3182
|
const identity = recordingCreateIdentity(input, keyCandidate, { bindIdempotencyKeyToId: false });
|
|
2432
|
-
const res = await client.create("recordings", identity.input, identity.idempotencyKey);
|
|
3183
|
+
const res = await client.create("recordings", identity.input, { idempotencyKey: identity.idempotencyKey });
|
|
2433
3184
|
return unwrap(res, "recording");
|
|
2434
3185
|
},
|
|
2435
3186
|
async getRecording(id) {
|
|
@@ -2437,7 +3188,7 @@ function apiStore(client) {
|
|
|
2437
3188
|
return res ? unwrap(res, "recording") : null;
|
|
2438
3189
|
},
|
|
2439
3190
|
async listRecordings(filter) {
|
|
2440
|
-
const { items } = await client
|
|
3191
|
+
const { items } = await listResource(client, "recordings", listQuery(filter));
|
|
2441
3192
|
return items;
|
|
2442
3193
|
},
|
|
2443
3194
|
async countRecordings(filter) {
|
|
@@ -2448,7 +3199,7 @@ function apiStore(client) {
|
|
|
2448
3199
|
const seenPageKeys = new Set;
|
|
2449
3200
|
while (pageRequests < maxPageRequests) {
|
|
2450
3201
|
pageRequests += 1;
|
|
2451
|
-
const { items, raw } = await client
|
|
3202
|
+
const { items, raw } = await listResource(client, "recordings", {
|
|
2452
3203
|
...listQuery(filter),
|
|
2453
3204
|
limit: pageLimit,
|
|
2454
3205
|
offset
|
|
@@ -2471,7 +3222,7 @@ function apiStore(client) {
|
|
|
2471
3222
|
throw new Error(`Recordings API exceeded ${maxPageRequests} pages while counting legacy results`);
|
|
2472
3223
|
},
|
|
2473
3224
|
async searchRecordings(query, filter) {
|
|
2474
|
-
const { items } = await client
|
|
3225
|
+
const { items } = await listResource(client, "recordings", listQuery({ ...filter ?? {}, search: query }));
|
|
2475
3226
|
return items;
|
|
2476
3227
|
},
|
|
2477
3228
|
async deleteRecording(id) {
|
|
@@ -2503,7 +3254,7 @@ function apiStore(client) {
|
|
|
2503
3254
|
return res ? unwrap(res, "agent") : null;
|
|
2504
3255
|
},
|
|
2505
3256
|
async listAgents() {
|
|
2506
|
-
const { items } = await client
|
|
3257
|
+
const { items } = await listResource(client, "agents");
|
|
2507
3258
|
return items;
|
|
2508
3259
|
},
|
|
2509
3260
|
async heartbeatAgent(idOrName) {
|
|
@@ -2543,7 +3294,7 @@ function apiStore(client) {
|
|
|
2543
3294
|
return res ? unwrap(res, "project") : null;
|
|
2544
3295
|
},
|
|
2545
3296
|
async listProjects() {
|
|
2546
|
-
const { items } = await client
|
|
3297
|
+
const { items } = await listResource(client, "projects");
|
|
2547
3298
|
return items;
|
|
2548
3299
|
},
|
|
2549
3300
|
async saveFeedback(input) {
|
|
@@ -2591,7 +3342,7 @@ var cached = null;
|
|
|
2591
3342
|
function getStore(env = process.env) {
|
|
2592
3343
|
if (env === process.env && cached)
|
|
2593
3344
|
return cached;
|
|
2594
|
-
const resolved =
|
|
3345
|
+
const resolved = resolveStoreClient(APP, env);
|
|
2595
3346
|
const store = resolved.transport === "http" ? apiStore(resolved.client) : localStore;
|
|
2596
3347
|
if (env === process.env)
|
|
2597
3348
|
cached = store;
|
|
@@ -2600,7 +3351,7 @@ function getStore(env = process.env) {
|
|
|
2600
3351
|
|
|
2601
3352
|
// src/lib/recorder.ts
|
|
2602
3353
|
import { spawn as spawn2 } from "child_process";
|
|
2603
|
-
import { join as
|
|
3354
|
+
import { join as join5 } from "path";
|
|
2604
3355
|
import { existsSync as existsSync3 } from "fs";
|
|
2605
3356
|
|
|
2606
3357
|
// src/types/index.ts
|
|
@@ -2651,7 +3402,7 @@ function startRecording(config) {
|
|
|
2651
3402
|
}
|
|
2652
3403
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2653
3404
|
const filename = `recording-${timestamp}.${config.audio_format}`;
|
|
2654
|
-
const filepath =
|
|
3405
|
+
const filepath = join5(config.audio_dir, filename);
|
|
2655
3406
|
const args = buildRecordArgs(filepath, config);
|
|
2656
3407
|
const [command, ...commandArgs] = args;
|
|
2657
3408
|
if (command === undefined) {
|
|
@@ -2723,7 +3474,7 @@ function buildRecordArgs(filepath, config) {
|
|
|
2723
3474
|
async function recordDuration(seconds, config) {
|
|
2724
3475
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2725
3476
|
const filename = `recording-${timestamp}.${config.audio_format}`;
|
|
2726
|
-
const filepath =
|
|
3477
|
+
const filepath = join5(config.audio_dir, filename);
|
|
2727
3478
|
const args = [
|
|
2728
3479
|
"rec",
|
|
2729
3480
|
"-r",
|
|
@@ -2896,8 +3647,8 @@ ${trimmed}`;
|
|
|
2896
3647
|
|
|
2897
3648
|
// src/lib/capture-probe.ts
|
|
2898
3649
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
2899
|
-
import { existsSync as existsSync4, readFileSync as
|
|
2900
|
-
import { join as
|
|
3650
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
|
|
3651
|
+
import { join as join6 } from "path";
|
|
2901
3652
|
import { tmpdir } from "os";
|
|
2902
3653
|
|
|
2903
3654
|
// src/lib/macos-bundle.ts
|
|
@@ -2913,7 +3664,7 @@ var WAVE_FORMAT_PCM = 1;
|
|
|
2913
3664
|
var WAVE_FORMAT_EXTENSIBLE = 65534;
|
|
2914
3665
|
var SUBFORMAT_OFFSET_IN_EXTENSION = 8;
|
|
2915
3666
|
function readWavPeak(filepath) {
|
|
2916
|
-
const buf =
|
|
3667
|
+
const buf = readFileSync3(filepath);
|
|
2917
3668
|
if (buf.length < RIFF_HEADER_BYTES) {
|
|
2918
3669
|
throw new Error(`not a RIFF file (${buf.length} bytes): ${filepath}`);
|
|
2919
3670
|
}
|
|
@@ -2988,7 +3739,7 @@ function probeMicrophoneCapture(config, options = {}) {
|
|
|
2988
3739
|
peak: 0,
|
|
2989
3740
|
silent: null
|
|
2990
3741
|
};
|
|
2991
|
-
const filepath =
|
|
3742
|
+
const filepath = join6(tmpdir(), `recordings-capture-probe-${process.pid}-${Date.now()}.wav`);
|
|
2992
3743
|
try {
|
|
2993
3744
|
const result = spawnSync2(executable, [
|
|
2994
3745
|
"-q",
|
|
@@ -3160,7 +3911,7 @@ function microphoneGrantInstruction(options) {
|
|
|
3160
3911
|
steps.push(`AMBIGUOUS: ${candidates.length} HasnaRecordings.app bundles exist (${candidates.join(", ")}). ` + "A TCC grant is bound to the bundle's code signature, so granting one does not grant the " + "other, and the toggle in Settings does not say which is which. Remove the bundles you are " + "not running before granting, or the grant may attach to the wrong one.");
|
|
3161
3912
|
}
|
|
3162
3913
|
steps.push(`At the keyboard on the machine itself (not over SSH), launch ${bundlePath} and start a ` + "recording once. macOS shows the consent sheet titled " + `"\u201CRecordings\u201D would like to access the microphone" \u2014 click Allow.`);
|
|
3163
|
-
steps.push("If no sheet appears, open System Settings \u2192 Privacy & Security \u2192 Microphone " + "and switch ON the row named \u201CRecordings\u201D. " + `That row is bundle ${RECORDINGS_BUNDLE_IDENTIFIER} at ${bundlePath}; the binary that ` + `receives the grant is ${
|
|
3914
|
+
steps.push("If no sheet appears, open System Settings \u2192 Privacy & Security \u2192 Microphone " + "and switch ON the row named \u201CRecordings\u201D. " + `That row is bundle ${RECORDINGS_BUNDLE_IDENTIFIER} at ${bundlePath}; the binary that ` + `receives the grant is ${join6(bundlePath, "Contents", "MacOS", "Recordings")}.`);
|
|
3164
3915
|
if (options.requestState === "never_requested") {
|
|
3165
3916
|
steps.push("Note: the app has never requested microphone access on this machine (no TCC entry exists), " + "so the Microphone list will NOT contain a \u201CRecordings\u201D row until the app asks once. " + "Do the launch-and-record step first; the Settings toggle only exists afterwards.");
|
|
3166
3917
|
} else if (options.requestState === "unknown") {
|
|
@@ -3693,12 +4444,12 @@ ${block}` : block;
|
|
|
3693
4444
|
|
|
3694
4445
|
// src/cli/macos-permissions.ts
|
|
3695
4446
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
3696
|
-
import { existsSync as existsSync6, mkdtempSync, rmSync as rmSync3, statSync as
|
|
4447
|
+
import { existsSync as existsSync6, mkdtempSync, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
3697
4448
|
import { tmpdir as tmpdir2 } from "os";
|
|
3698
|
-
import { join as
|
|
4449
|
+
import { join as join7 } from "path";
|
|
3699
4450
|
var defaultPermissionHelperRunner = (executable, arguments_, options) => spawnSync3(executable, arguments_, options);
|
|
3700
4451
|
function runMacOSPermissionRequest(appPath, runner = defaultPermissionHelperRunner) {
|
|
3701
|
-
const executable =
|
|
4452
|
+
const executable = join7(appPath, "Contents", "MacOS", "Recordings");
|
|
3702
4453
|
const result = runner(executable, ["--request-permissions", "--open-permission-settings"], { stdio: "inherit" });
|
|
3703
4454
|
return {
|
|
3704
4455
|
exitCode: result.error ? 1 : result.status ?? 1,
|
|
@@ -3709,8 +4460,8 @@ var CODESIGN_REQUIREMENT_SATISFIED_STATUS = 0;
|
|
|
3709
4460
|
var CODESIGN_REQUIREMENT_UNSATISFIED_STATUS = 3;
|
|
3710
4461
|
function tccDatabasePaths(home) {
|
|
3711
4462
|
return [
|
|
3712
|
-
|
|
3713
|
-
|
|
4463
|
+
join7(home, "Library", "Application Support", "com.apple.TCC", "TCC.db"),
|
|
4464
|
+
join7("/", "Library", "Application Support", "com.apple.TCC", "TCC.db")
|
|
3714
4465
|
];
|
|
3715
4466
|
}
|
|
3716
4467
|
function tccAuthValueLabel(value) {
|
|
@@ -3737,8 +4488,8 @@ function verifyStoredRequirementWithCodesign(csreqHex, appPath, runner = default
|
|
|
3737
4488
|
return "unverifiable";
|
|
3738
4489
|
let scratchDirectory = null;
|
|
3739
4490
|
try {
|
|
3740
|
-
scratchDirectory = mkdtempSync(
|
|
3741
|
-
const requirementPath =
|
|
4491
|
+
scratchDirectory = mkdtempSync(join7(tmpdir2(), "recordings-tcc-csreq-"));
|
|
4492
|
+
const requirementPath = join7(scratchDirectory, "tcc-requirement.bin");
|
|
3742
4493
|
writeFileSync2(requirementPath, Buffer.from(normalized, "hex"));
|
|
3743
4494
|
const result = runner(requirementPath, appPath);
|
|
3744
4495
|
if (result.error)
|
|
@@ -3758,7 +4509,7 @@ function verifyStoredRequirementWithCodesign(csreqHex, appPath, runner = default
|
|
|
3758
4509
|
var defaultTccPermissionProbe = {
|
|
3759
4510
|
databasePresence: (dbPath) => {
|
|
3760
4511
|
try {
|
|
3761
|
-
|
|
4512
|
+
statSync3(dbPath);
|
|
3762
4513
|
return "present";
|
|
3763
4514
|
} catch (error) {
|
|
3764
4515
|
return error.code === "ENOENT" ? "absent" : "indeterminate";
|
|
@@ -3797,8 +4548,8 @@ function describeStoredRequirementWithCsreq(csreqHex, runner = (requirementPath)
|
|
|
3797
4548
|
}
|
|
3798
4549
|
let scratchDirectory = null;
|
|
3799
4550
|
try {
|
|
3800
|
-
scratchDirectory = mkdtempSync(
|
|
3801
|
-
const requirementPath =
|
|
4551
|
+
scratchDirectory = mkdtempSync(join7(tmpdir2(), "recordings-tcc-decode-"));
|
|
4552
|
+
const requirementPath = join7(scratchDirectory, "tcc-requirement.bin");
|
|
3802
4553
|
writeFileSync2(requirementPath, Buffer.from(normalized, "hex"));
|
|
3803
4554
|
const result = runner(requirementPath);
|
|
3804
4555
|
if (result.error || result.status !== 0)
|
|
@@ -4200,7 +4951,7 @@ function writeUseFnKey(enabled) {
|
|
|
4200
4951
|
}
|
|
4201
4952
|
|
|
4202
4953
|
// src/cli/trigger-probe.ts
|
|
4203
|
-
import { closeSync, openSync, readSync, statSync as
|
|
4954
|
+
import { closeSync, openSync, readSync, statSync as statSync4 } from "fs";
|
|
4204
4955
|
var FN_BLOCKING_ACCESSIBILITY_STATES = [
|
|
4205
4956
|
"denied",
|
|
4206
4957
|
"stale_allowed_for_previous_app_build"
|
|
@@ -4319,7 +5070,7 @@ function readAppLogTail(logPath, maxBytes = APP_LOG_TAIL_BYTES) {
|
|
|
4319
5070
|
return null;
|
|
4320
5071
|
let handle = null;
|
|
4321
5072
|
try {
|
|
4322
|
-
const size =
|
|
5073
|
+
const size = statSync4(logPath).size;
|
|
4323
5074
|
if (size === 0)
|
|
4324
5075
|
return null;
|
|
4325
5076
|
const length = Math.min(size, maxBytes);
|
|
@@ -4439,7 +5190,7 @@ function currentMachineId(env = process.env, hostName = hostname()) {
|
|
|
4439
5190
|
}
|
|
4440
5191
|
|
|
4441
5192
|
// src/lib/bun-runtime.ts
|
|
4442
|
-
import { accessSync, constants as fsConstants, realpathSync as realpathSync3, statSync as
|
|
5193
|
+
import { accessSync, constants as fsConstants, realpathSync as realpathSync3, statSync as statSync5 } from "fs";
|
|
4443
5194
|
import { isAbsolute as isAbsolute2 } from "path";
|
|
4444
5195
|
import { randomBytes } from "crypto";
|
|
4445
5196
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
@@ -4461,7 +5212,7 @@ function validateBunExecutable(candidate) {
|
|
|
4461
5212
|
let executable;
|
|
4462
5213
|
try {
|
|
4463
5214
|
executable = realpathSync3(candidate);
|
|
4464
|
-
if (!
|
|
5215
|
+
if (!statSync5(executable).isFile())
|
|
4465
5216
|
return { reason: "resolved path is not a regular file" };
|
|
4466
5217
|
accessSync(executable, fsConstants.X_OK);
|
|
4467
5218
|
} catch {
|
|
@@ -4541,11 +5292,11 @@ import {
|
|
|
4541
5292
|
fsyncSync,
|
|
4542
5293
|
mkdtempSync as mkdtempSync2,
|
|
4543
5294
|
openSync as openSync2,
|
|
4544
|
-
readFileSync as
|
|
5295
|
+
readFileSync as readFileSync4,
|
|
4545
5296
|
rmSync as rmSync4,
|
|
4546
5297
|
writeFileSync as writeFileSync3
|
|
4547
5298
|
} from "fs";
|
|
4548
|
-
import { isAbsolute as isAbsolute3, join as
|
|
5299
|
+
import { isAbsolute as isAbsolute3, join as join8 } from "path";
|
|
4549
5300
|
var LOWER_SHA256 = /^[a-f0-9]{64}$/;
|
|
4550
5301
|
var LOWER_SOURCE_SHA = /^[a-f0-9]{40}$/;
|
|
4551
5302
|
var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
|
|
@@ -4618,7 +5369,7 @@ function prepareReleaseInstallInputs(input) {
|
|
|
4618
5369
|
if (!isAbsolute3(snapshotRoot)) {
|
|
4619
5370
|
throw new Error("release snapshot root must be absolute");
|
|
4620
5371
|
}
|
|
4621
|
-
const snapshotDirectory = mkdtempSync2(
|
|
5372
|
+
const snapshotDirectory = mkdtempSync2(join8(snapshotRoot, "recordings-release-install."));
|
|
4622
5373
|
let cleaned = false;
|
|
4623
5374
|
const cleanup = () => {
|
|
4624
5375
|
if (cleaned)
|
|
@@ -4630,9 +5381,9 @@ function prepareReleaseInstallInputs(input) {
|
|
|
4630
5381
|
rmSync4(snapshotDirectory, { recursive: true, force: true });
|
|
4631
5382
|
};
|
|
4632
5383
|
try {
|
|
4633
|
-
const manifestSnapshot =
|
|
5384
|
+
const manifestSnapshot = join8(snapshotDirectory, `${input.manifestSha256}.manifest.json`);
|
|
4634
5385
|
const envelopeDigest = createHash2("sha256").update(envelopeBytes).digest("hex");
|
|
4635
|
-
const envelopeSnapshot =
|
|
5386
|
+
const envelopeSnapshot = join8(snapshotDirectory, `${envelopeDigest}.envelope.json`);
|
|
4636
5387
|
writeSnapshot(manifestSnapshot, manifestBytes);
|
|
4637
5388
|
writeSnapshot(envelopeSnapshot, envelopeBytes);
|
|
4638
5389
|
chmodSync2(snapshotDirectory, 320);
|
|
@@ -4653,7 +5404,7 @@ function readBoundedRegularFile(path, label, maximum) {
|
|
|
4653
5404
|
if (!metadata.isFile() || metadata.size < 1 || metadata.size > maximum) {
|
|
4654
5405
|
throw new Error(`${label} must be a non-empty bounded regular file`);
|
|
4655
5406
|
}
|
|
4656
|
-
return
|
|
5407
|
+
return readFileSync4(descriptor);
|
|
4657
5408
|
} finally {
|
|
4658
5409
|
closeSync2(descriptor);
|
|
4659
5410
|
}
|
|
@@ -4744,8 +5495,8 @@ function writeSnapshot(path, bytes) {
|
|
|
4744
5495
|
}
|
|
4745
5496
|
|
|
4746
5497
|
// src/cli/desktop-snapshot.ts
|
|
4747
|
-
import { chmodSync as chmodSync3, mkdirSync as mkdirSync4, mkdtempSync as mkdtempSync3, renameSync as renameSync2, rmSync as rmSync5, statSync as
|
|
4748
|
-
import { dirname as dirname4, join as
|
|
5498
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync4, mkdtempSync as mkdtempSync3, renameSync as renameSync2, rmSync as rmSync5, statSync as statSync6 } from "fs";
|
|
5499
|
+
import { dirname as dirname4, join as join9, resolve as resolve3 } from "path";
|
|
4749
5500
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
4750
5501
|
var SCREEN_CAPTURE_EXECUTABLE = "/usr/sbin/screencapture";
|
|
4751
5502
|
var DEFAULT_DESKTOP_SNAPSHOT = "desktop-snapshot.png";
|
|
@@ -4767,9 +5518,9 @@ function exportDesktopSnapshot(output = DEFAULT_DESKTOP_SNAPSHOT, dependencies =
|
|
|
4767
5518
|
const destination = resolve3(dependencies.cwd ?? process.cwd(), output);
|
|
4768
5519
|
const destinationDirectory = dirname4(destination);
|
|
4769
5520
|
mkdirSync4(destinationDirectory, { recursive: true });
|
|
4770
|
-
const stagingDirectory = mkdtempSync3(
|
|
5521
|
+
const stagingDirectory = mkdtempSync3(join9(destinationDirectory, ".recordings-desktop-"));
|
|
4771
5522
|
chmodSync3(stagingDirectory, 448);
|
|
4772
|
-
const stagingPath =
|
|
5523
|
+
const stagingPath = join9(stagingDirectory, "snapshot.png");
|
|
4773
5524
|
try {
|
|
4774
5525
|
const capture = dependencies.capture ?? runScreenCapture;
|
|
4775
5526
|
const result = capture(SCREEN_CAPTURE_EXECUTABLE, [
|
|
@@ -4789,7 +5540,7 @@ function exportDesktopSnapshot(output = DEFAULT_DESKTOP_SNAPSHOT, dependencies =
|
|
|
4789
5540
|
}
|
|
4790
5541
|
let size = 0;
|
|
4791
5542
|
try {
|
|
4792
|
-
const snapshot =
|
|
5543
|
+
const snapshot = statSync6(stagingPath);
|
|
4793
5544
|
if (snapshot.isFile())
|
|
4794
5545
|
size = snapshot.size;
|
|
4795
5546
|
} catch {}
|
|
@@ -5238,10 +5989,10 @@ program.command("projects").description("List registered projects").option("-n,
|
|
|
5238
5989
|
});
|
|
5239
5990
|
program.command("init").description("Initialize .recordings/ in current directory").action(() => {
|
|
5240
5991
|
const { mkdirSync: mkdirSync5, writeFileSync: writeFileSync4, existsSync: existsSync9 } = __require("fs");
|
|
5241
|
-
const { join:
|
|
5242
|
-
const dir =
|
|
5243
|
-
const audioDir =
|
|
5244
|
-
const configFile =
|
|
5992
|
+
const { join: join10 } = __require("path");
|
|
5993
|
+
const dir = join10(process.cwd(), ".recordings");
|
|
5994
|
+
const audioDir = join10(dir, "audio");
|
|
5995
|
+
const configFile = join10(dir, "config.json");
|
|
5245
5996
|
mkdirSync5(audioDir, { recursive: true });
|
|
5246
5997
|
if (!existsSync9(configFile)) {
|
|
5247
5998
|
const defaultConf = {
|
|
@@ -6261,7 +7012,7 @@ async function readSaveTextInput(text, opts) {
|
|
|
6261
7012
|
}
|
|
6262
7013
|
let rawText;
|
|
6263
7014
|
if (opts.textFile !== undefined) {
|
|
6264
|
-
rawText =
|
|
7015
|
+
rawText = readFileSync5(opts.textFile, "utf8");
|
|
6265
7016
|
} else if (opts.stdin) {
|
|
6266
7017
|
rawText = await Bun.stdin.text();
|
|
6267
7018
|
} else {
|
|
@@ -6475,7 +7226,7 @@ function relativeHint(value) {
|
|
|
6475
7226
|
return `${Math.floor(hours / 24)}d ago`;
|
|
6476
7227
|
}
|
|
6477
7228
|
program.command("mcp").description("Install recordings MCP server into Claude Code, Codex, or Gemini").option("--claude", "Install into Claude Code (via `claude mcp add`)").option("--codex", "Install into Codex (~/.codex/config.toml)").option("--gemini", "Install into Gemini (~/.gemini/settings.json)").option("--all", "Install into all supported agents").option("--uninstall", "Remove recordings MCP from config").action(async (opts) => {
|
|
6478
|
-
const { readFileSync:
|
|
7229
|
+
const { readFileSync: readFileSync6, writeFileSync: writeFileSync4, existsSync: fileExists } = __require("fs");
|
|
6479
7230
|
const { join: pathJoin2 } = __require("path");
|
|
6480
7231
|
const { homedir: getHome } = __require("os");
|
|
6481
7232
|
const { execSync } = __require("child_process");
|
|
@@ -6508,7 +7259,7 @@ program.command("mcp").description("Install recordings MCP server into Claude Co
|
|
|
6508
7259
|
if (target === "codex") {
|
|
6509
7260
|
const configPath = pathJoin2(home, ".codex", "config.toml");
|
|
6510
7261
|
if (fileExists(configPath)) {
|
|
6511
|
-
const content =
|
|
7262
|
+
const content = readFileSync6(configPath, "utf-8");
|
|
6512
7263
|
if (opts.uninstall) {
|
|
6513
7264
|
const { content: next, removed } = removeCodexServerBlock(content, "recordings");
|
|
6514
7265
|
writeFileSync4(configPath, next, "utf-8");
|
|
@@ -6526,7 +7277,7 @@ program.command("mcp").description("Install recordings MCP server into Claude Co
|
|
|
6526
7277
|
const configPath = pathJoin2(home, ".gemini", "settings.json");
|
|
6527
7278
|
let config = {};
|
|
6528
7279
|
if (fileExists(configPath)) {
|
|
6529
|
-
config = JSON.parse(
|
|
7280
|
+
config = JSON.parse(readFileSync6(configPath, "utf-8"));
|
|
6530
7281
|
}
|
|
6531
7282
|
const servers = config["mcpServers"] || {};
|
|
6532
7283
|
if (opts.uninstall) {
|
|
@@ -6714,7 +7465,7 @@ function findPackageRoot() {
|
|
|
6714
7465
|
const packagePath = pathJoin(current, "package.json");
|
|
6715
7466
|
if (existsSync8(packagePath)) {
|
|
6716
7467
|
try {
|
|
6717
|
-
const pkg = JSON.parse(
|
|
7468
|
+
const pkg = JSON.parse(readFileSync5(packagePath, "utf8"));
|
|
6718
7469
|
if (pkg.name === "@hasna/recordings") {
|
|
6719
7470
|
return current;
|
|
6720
7471
|
}
|