@hasna/recordings 0.3.2 → 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/README.md +38 -34
- package/bun.lock +96 -36
- package/dist/__tests__/helpers/source-assertions.d.ts +6 -4
- package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -1
- package/dist/cli/index.js +1059 -219
- package/dist/cli/macos-shortcut.d.ts +2 -2
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/http/client.d.ts +6 -14
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.js +1971 -175
- package/dist/lib/capture-probe.d.ts +3 -3
- package/dist/lib/capture-probe.d.ts.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/macos-bundle.d.ts +1 -1
- package/dist/lib/release-install-policy.d.ts +21 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -1
- package/dist/mcp/index.js +978 -142
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +7 -3
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +4 -19
- package/dist/server/cloud-config.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +1 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +217 -299
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +1881 -90
- package/dist/store.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/package.json +4 -3
- package/packaging/macos/build_release_pkg.sh +10 -4
- package/packaging/macos/managed_bootstrap.sh +4 -4
- package/packaging/macos/scripts/postinstall +2 -2
- package/packaging/macos/scripts/preinstall +2 -0
- package/scripts/generate-sdk.ts +17 -16
- package/scripts/install_macos_app.sh +22 -22
- package/scripts/macos_artifact.ts +48 -39
- package/scripts/migrate.ts +2 -2
- package/scripts/release-suite-gate.ts +174 -0
- package/scripts/set-version.ts +5 -5
- package/scripts/smoke_macos_app.sh +21 -21
- package/scripts/vacuity-manifests/version-sites.tsv +8 -4
- package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +2 -2
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
- package/src/native/Recordings/build.sh +14 -20
- package/dist/lib/retired-deployment-modes.d.ts +0 -24
- package/dist/lib/retired-deployment-modes.d.ts.map +0 -1
package/dist/mcp/index.js
CHANGED
|
@@ -5054,9 +5054,118 @@ function setAgentFocus(idOrName, projectId, db) {
|
|
|
5054
5054
|
d.query("UPDATE agents SET active_project_id = ?, last_seen_at = ? WHERE id = ?").run(resolvedProjectId, new Date().toISOString(), agent.id);
|
|
5055
5055
|
return getAgent(agent.id, d);
|
|
5056
5056
|
}
|
|
5057
|
+
// package.json
|
|
5058
|
+
var package_default = {
|
|
5059
|
+
name: "@hasna/recordings",
|
|
5060
|
+
version: "0.3.9",
|
|
5061
|
+
type: "module",
|
|
5062
|
+
description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
|
|
5063
|
+
repository: {
|
|
5064
|
+
type: "git",
|
|
5065
|
+
url: "git+https://github.com/hasna/apps"
|
|
5066
|
+
},
|
|
5067
|
+
main: "dist/index.js",
|
|
5068
|
+
types: "dist/index.d.ts",
|
|
5069
|
+
bin: {
|
|
5070
|
+
recordings: "dist/cli/index.js",
|
|
5071
|
+
"recordings-mcp": "dist/mcp/index.js",
|
|
5072
|
+
"recordings-serve": "dist/server/index.js"
|
|
5073
|
+
},
|
|
5074
|
+
exports: {
|
|
5075
|
+
".": {
|
|
5076
|
+
import: "./dist/index.js",
|
|
5077
|
+
types: "./dist/index.d.ts"
|
|
5078
|
+
},
|
|
5079
|
+
"./storage": {
|
|
5080
|
+
import: "./dist/storage.js",
|
|
5081
|
+
types: "./dist/storage.d.ts"
|
|
5082
|
+
},
|
|
5083
|
+
"./sdk": {
|
|
5084
|
+
import: "./dist/sdk/index.js",
|
|
5085
|
+
types: "./dist/sdk/index.d.ts"
|
|
5086
|
+
}
|
|
5087
|
+
},
|
|
5088
|
+
engines: {
|
|
5089
|
+
bun: ">=1.0.0"
|
|
5090
|
+
},
|
|
5091
|
+
scripts: {
|
|
5092
|
+
clean: "rm -rf dist",
|
|
5093
|
+
build: "bun run clean && bun run build:cli && bun run build:mcp && bun run build:serve && bun run build:lib && tsc --emitDeclarationOnly --outDir dist",
|
|
5094
|
+
"build:cli": "bun build src/cli/index.ts --target=bun --outfile=dist/cli/index.js --external=commander --external=chalk --external=openai",
|
|
5095
|
+
"build:mcp": "bun build src/mcp/index.ts --target=bun --outfile=dist/mcp/index.js --external=@modelcontextprotocol/sdk --external=openai",
|
|
5096
|
+
"build:serve": "bun build src/server/index.ts --target=bun --outfile=dist/server/index.js --external=@modelcontextprotocol/sdk --external=@hasna/contracts --external=openai --external=pg",
|
|
5097
|
+
"build:lib": "bun build src/index.ts src/storage.ts src/sdk/index.ts --target=bun --outdir=dist --external=openai",
|
|
5098
|
+
"build:native-fs-guard": "/bin/bash scripts/build_native_fs_guard.sh",
|
|
5099
|
+
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
5100
|
+
migrate: "bun run scripts/migrate.ts",
|
|
5101
|
+
typecheck: "tsc --noEmit",
|
|
5102
|
+
"typecheck:tcc-contract": "tsc --noEmit -p tsconfig.tcc-contract.json",
|
|
5103
|
+
test: "bun test",
|
|
5104
|
+
"verify:ci-suite": "bun scripts/ci-linux-suite.ts --check",
|
|
5105
|
+
"verify:ci-suite:run": "bun scripts/ci-linux-suite.ts --verify-run",
|
|
5106
|
+
"verify:ci-native": "bun scripts/ci-native-build.ts",
|
|
5107
|
+
"test:gated": 'bun scripts/ci-linux-suite.ts --check && RECORDINGS_TEST_TIMEOUT_MS="${RECORDINGS_TEST_TIMEOUT_MS:-120000}" bun test --timeout "$RECORDINGS_TEST_TIMEOUT_MS" $(bun scripts/ci-linux-suite.ts --gated)',
|
|
5108
|
+
"test:vacuity-battery": 'bun scripts/vacuity-manifest-gen.ts > "${TMPDIR:-/tmp}/vacuity-corrupt-sites.tsv" && bun scripts/vacuity-mutation-battery.ts "${TMPDIR:-/tmp}/vacuity-corrupt-sites.tsv"',
|
|
5109
|
+
"test:vacuity-battery:source": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/source-side.tsv",
|
|
5110
|
+
"test:vacuity-battery:chain": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/install-chain.tsv",
|
|
5111
|
+
"test:vacuity-battery:vars": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/variable-operands.tsv",
|
|
5112
|
+
"test:vacuity-battery:reorder": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/reorder.tsv",
|
|
5113
|
+
"test:coverage": "bun test --coverage",
|
|
5114
|
+
"dev:cli": "bun run src/cli/index.ts",
|
|
5115
|
+
"desktop:snapshot": "bun run src/cli/index.ts app snapshot",
|
|
5116
|
+
"dev:mcp": "bun run src/mcp/index.ts",
|
|
5117
|
+
"verify:release": "bun run scripts/release-guard.ts",
|
|
5118
|
+
"scan:artifact": "bun run scripts/scan-artifact.ts",
|
|
5119
|
+
"version:set": "bun run scripts/set-version.ts",
|
|
5120
|
+
"version:check": "bun run scripts/set-version.ts --check",
|
|
5121
|
+
prepack: "bun run prepack:platform-gate && bun run version:check && bun run build && bun run verify:release && bun run scan:artifact",
|
|
5122
|
+
"prepack:platform-gate": `bash -c 'if [ "$(uname -s)" = Darwin ]; then bun run build:native-fs-guard; else echo "WARN: native fs-guard build skipped on $(uname -s) \u2014 dry-run/CI pack; publish recordings from macOS"; fi'`,
|
|
5123
|
+
prepublishOnly: "bun run typecheck && bun run release-suite-gate",
|
|
5124
|
+
"release-suite-gate": "bun run scripts/release-suite-gate.ts",
|
|
5125
|
+
"typecheck:shortcut-contract": "tsc --noEmit -p tsconfig.test.json"
|
|
5126
|
+
},
|
|
5127
|
+
files: [
|
|
5128
|
+
"dist/",
|
|
5129
|
+
"scripts/",
|
|
5130
|
+
"Dockerfile.package",
|
|
5131
|
+
"bun.lock",
|
|
5132
|
+
"src/native/Recordings/App/",
|
|
5133
|
+
"src/native/Recordings/RecordingsLib/",
|
|
5134
|
+
"src/native/Recordings/RecordingsTests/",
|
|
5135
|
+
"src/native/Recordings/Updater/",
|
|
5136
|
+
"src/native/Recordings/Package.swift",
|
|
5137
|
+
"src/native/Recordings/Package.resolved",
|
|
5138
|
+
"src/native/Recordings/build.sh",
|
|
5139
|
+
"packaging/macos/",
|
|
5140
|
+
"README.md",
|
|
5141
|
+
"LICENSE"
|
|
5142
|
+
],
|
|
5143
|
+
dependencies: {
|
|
5144
|
+
"@hasna/contracts": "0.13.4",
|
|
5145
|
+
"@hasna/events": "0.1.11",
|
|
5146
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
5147
|
+
chalk: "^5.4.1",
|
|
5148
|
+
commander: "^13.1.0",
|
|
5149
|
+
openai: "^5.1.0",
|
|
5150
|
+
pg: "^8.13.3",
|
|
5151
|
+
zod: "^3.24.2"
|
|
5152
|
+
},
|
|
5153
|
+
devDependencies: {
|
|
5154
|
+
"@types/bun": "^1.2.5",
|
|
5155
|
+
"@types/pg": "^8.11.11",
|
|
5156
|
+
"node-api-headers": "1.9.0",
|
|
5157
|
+
typescript: "^5.8.2"
|
|
5158
|
+
},
|
|
5159
|
+
publishConfig: {
|
|
5160
|
+
registry: "https://registry.npmjs.org",
|
|
5161
|
+
access: "public"
|
|
5162
|
+
},
|
|
5163
|
+
license: "Apache-2.0",
|
|
5164
|
+
author: "Hasna <andrei@hasna.com>"
|
|
5165
|
+
};
|
|
5057
5166
|
|
|
5058
5167
|
// src/version.ts
|
|
5059
|
-
var VERSION =
|
|
5168
|
+
var VERSION = package_default.version;
|
|
5060
5169
|
|
|
5061
5170
|
// src/db/feedback.ts
|
|
5062
5171
|
function saveFeedback(input) {
|
|
@@ -5064,59 +5173,207 @@ function saveFeedback(input) {
|
|
|
5064
5173
|
db.query("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(input.message, input.email ?? null, input.category ?? "general", input.version ?? VERSION);
|
|
5065
5174
|
}
|
|
5066
5175
|
|
|
5067
|
-
//
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
"
|
|
5073
|
-
"hybrid"
|
|
5074
|
-
];
|
|
5075
|
-
function normalizeModeToken(value) {
|
|
5076
|
-
return value.trim().toLowerCase().replace(/-/g, "_");
|
|
5176
|
+
// ../contracts/dist/client/transport.js
|
|
5177
|
+
import { isIP } from "net";
|
|
5178
|
+
import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
5179
|
+
import { join as join3 } from "path";
|
|
5180
|
+
function envToken(name) {
|
|
5181
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
5077
5182
|
}
|
|
5078
|
-
function
|
|
5079
|
-
const
|
|
5080
|
-
return
|
|
5183
|
+
function clientTransportEnvKeys(name) {
|
|
5184
|
+
const envSegment = envToken(name);
|
|
5185
|
+
return {
|
|
5186
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
5187
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
5188
|
+
};
|
|
5081
5189
|
}
|
|
5082
|
-
function
|
|
5083
|
-
|
|
5084
|
-
const value = mode === "local" ? replacement.onBox : replacement.offBox;
|
|
5085
|
-
return new Error(`${sourceEnvKey}=${rawValue} names a deployment mode, and deployment modes are removed: ` + `${RETIRED_DEPLOYMENT_MODES.join(" | ")} no longer select anything. ` + `Set ${replacement.envKey}=${value} instead ` + `(${replacement.envKey} takes ${replacement.onBox} or ${replacement.offBox}).`);
|
|
5190
|
+
function credentialOverrideEnvKey(name) {
|
|
5191
|
+
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
5086
5192
|
}
|
|
5193
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
|
|
5087
5194
|
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5195
|
+
class CredentialResolutionError extends Error {
|
|
5196
|
+
appName;
|
|
5197
|
+
attempted;
|
|
5198
|
+
constructor(appName, message, attempted) {
|
|
5199
|
+
super(message);
|
|
5200
|
+
this.name = "CredentialResolutionError";
|
|
5201
|
+
this.appName = appName;
|
|
5202
|
+
this.attempted = attempted;
|
|
5203
|
+
}
|
|
5204
|
+
}
|
|
5205
|
+
var HASNA_STATE_DIR = ".hasna";
|
|
5206
|
+
var FLEET_CREDENTIAL_DIR = "cloud";
|
|
5207
|
+
var CONFIG_DIR = ".config";
|
|
5208
|
+
var CONFIG_NAMESPACE = "hasna";
|
|
5209
|
+
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
5210
|
+
var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
5211
|
+
var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
5212
|
+
var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
5213
|
+
function homeDir(env) {
|
|
5214
|
+
const home = env.HOME?.trim();
|
|
5215
|
+
return home ? home : null;
|
|
5216
|
+
}
|
|
5217
|
+
function credentialDiskSources(name, env) {
|
|
5218
|
+
return profileDiskSources(name, env, null);
|
|
5219
|
+
}
|
|
5220
|
+
function profileDiskSources(name, env, profile) {
|
|
5221
|
+
const home = homeDir(env);
|
|
5222
|
+
if (!home || !SAFE_APP_SLUG.test(name))
|
|
5223
|
+
return [];
|
|
5224
|
+
const stem = profile ? `${name}.${profile}` : name;
|
|
5225
|
+
const configStem = profile ? `${name}-${profile}` : name;
|
|
5226
|
+
return [
|
|
5227
|
+
join3(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
|
|
5228
|
+
join3(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`)
|
|
5229
|
+
];
|
|
5092
5230
|
}
|
|
5093
|
-
function
|
|
5094
|
-
|
|
5231
|
+
function parseEnvFile(text) {
|
|
5232
|
+
const values = new Map;
|
|
5233
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
5234
|
+
const line = rawLine.trim();
|
|
5235
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
5236
|
+
continue;
|
|
5237
|
+
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
5238
|
+
const equals = withoutExport.indexOf("=");
|
|
5239
|
+
if (equals <= 0)
|
|
5240
|
+
continue;
|
|
5241
|
+
const key = withoutExport.slice(0, equals).trim();
|
|
5242
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
5243
|
+
continue;
|
|
5244
|
+
let value = withoutExport.slice(equals + 1).trim();
|
|
5245
|
+
const quote = value[0];
|
|
5246
|
+
if (quote === '"' || quote === "'") {
|
|
5247
|
+
if (value.length < 2 || !value.endsWith(quote))
|
|
5248
|
+
continue;
|
|
5249
|
+
value = value.slice(1, -1);
|
|
5250
|
+
}
|
|
5251
|
+
if (value.length === 0)
|
|
5252
|
+
continue;
|
|
5253
|
+
values.set(key, value);
|
|
5254
|
+
}
|
|
5255
|
+
return values;
|
|
5095
5256
|
}
|
|
5096
|
-
function
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5257
|
+
function readAppConfigFile(path) {
|
|
5258
|
+
let text;
|
|
5259
|
+
try {
|
|
5260
|
+
const stats = statSync2(path);
|
|
5261
|
+
if (!stats.isFile() || stats.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
5262
|
+
return null;
|
|
5263
|
+
text = readFileSync2(path, "utf8");
|
|
5264
|
+
} catch {
|
|
5265
|
+
return null;
|
|
5104
5266
|
}
|
|
5105
|
-
|
|
5267
|
+
return parseEnvFile(text);
|
|
5106
5268
|
}
|
|
5107
|
-
function
|
|
5108
|
-
|
|
5269
|
+
function readCredentialFile(path, apiKeyKeys) {
|
|
5270
|
+
const values = readAppConfigFile(path);
|
|
5271
|
+
if (!values)
|
|
5272
|
+
return null;
|
|
5273
|
+
for (const key of apiKeyKeys) {
|
|
5274
|
+
const value = values.get(key)?.trim();
|
|
5275
|
+
if (value)
|
|
5276
|
+
return value;
|
|
5277
|
+
}
|
|
5278
|
+
return null;
|
|
5109
5279
|
}
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5280
|
+
var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
|
|
5281
|
+
function appConfigDiskValue(name, env, keys) {
|
|
5282
|
+
const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
|
|
5283
|
+
if (wanted.length === 0)
|
|
5284
|
+
return null;
|
|
5285
|
+
for (const path of credentialDiskSources(name, env)) {
|
|
5286
|
+
const values = readAppConfigFile(path);
|
|
5287
|
+
if (!values)
|
|
5288
|
+
continue;
|
|
5289
|
+
for (const key of wanted) {
|
|
5290
|
+
const value = values.get(key)?.trim();
|
|
5291
|
+
if (value)
|
|
5292
|
+
return { key, value, path };
|
|
5293
|
+
}
|
|
5294
|
+
}
|
|
5295
|
+
return null;
|
|
5296
|
+
}
|
|
5297
|
+
function assertUsableCredential(appName, source, value) {
|
|
5298
|
+
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
5299
|
+
return;
|
|
5300
|
+
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]);
|
|
5301
|
+
}
|
|
5302
|
+
var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
5303
|
+
var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
5304
|
+
var CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE = "caller-supplied CredentialProvider";
|
|
5305
|
+
function sealCredential(fields) {
|
|
5306
|
+
const { apiKey } = fields;
|
|
5307
|
+
const visible = {
|
|
5308
|
+
tier: fields.tier,
|
|
5309
|
+
source: fields.source,
|
|
5310
|
+
deliberate: fields.deliberate,
|
|
5311
|
+
deprecated: fields.deprecated,
|
|
5312
|
+
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
5313
|
+
warning: fields.warning
|
|
5117
5314
|
};
|
|
5315
|
+
const sealed = { ...visible };
|
|
5316
|
+
Object.defineProperty(sealed, "apiKey", {
|
|
5317
|
+
value: apiKey,
|
|
5318
|
+
enumerable: false,
|
|
5319
|
+
writable: false,
|
|
5320
|
+
configurable: false
|
|
5321
|
+
});
|
|
5322
|
+
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
5323
|
+
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
5324
|
+
enumerable: false,
|
|
5325
|
+
writable: false,
|
|
5326
|
+
configurable: false
|
|
5327
|
+
});
|
|
5328
|
+
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
5329
|
+
value: true,
|
|
5330
|
+
enumerable: false,
|
|
5331
|
+
writable: false,
|
|
5332
|
+
configurable: false
|
|
5333
|
+
});
|
|
5334
|
+
return Object.freeze(sealed);
|
|
5335
|
+
}
|
|
5336
|
+
function isSealedCredential(credential) {
|
|
5337
|
+
return credential[CREDENTIAL_SEAL] === true;
|
|
5338
|
+
}
|
|
5339
|
+
function explicitCredential(appName, apiKey) {
|
|
5340
|
+
const source = "explicit apiKey option";
|
|
5341
|
+
assertUsableCredential(appName, source, apiKey);
|
|
5342
|
+
return sealCredential({
|
|
5343
|
+
apiKey,
|
|
5344
|
+
tier: "argument",
|
|
5345
|
+
source,
|
|
5346
|
+
deliberate: true,
|
|
5347
|
+
deprecated: false,
|
|
5348
|
+
diskCandidates: [],
|
|
5349
|
+
warning: null
|
|
5350
|
+
});
|
|
5118
5351
|
}
|
|
5119
|
-
function
|
|
5352
|
+
function validateAndSealResolvedCredential(appName, credential) {
|
|
5353
|
+
const apiKey = credential.apiKey;
|
|
5354
|
+
assertUsableCredential(appName, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE, apiKey);
|
|
5355
|
+
if (!isSealedCredential(credential)) {
|
|
5356
|
+
return sealCredential({
|
|
5357
|
+
apiKey,
|
|
5358
|
+
tier: "argument",
|
|
5359
|
+
source: CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE,
|
|
5360
|
+
deliberate: true,
|
|
5361
|
+
deprecated: false,
|
|
5362
|
+
diskCandidates: [],
|
|
5363
|
+
warning: null
|
|
5364
|
+
});
|
|
5365
|
+
}
|
|
5366
|
+
return sealCredential({
|
|
5367
|
+
apiKey,
|
|
5368
|
+
tier: credential.tier,
|
|
5369
|
+
source: credential.source,
|
|
5370
|
+
deliberate: credential.deliberate,
|
|
5371
|
+
deprecated: credential.deprecated,
|
|
5372
|
+
diskCandidates: credential.diskCandidates,
|
|
5373
|
+
warning: credential.warning
|
|
5374
|
+
});
|
|
5375
|
+
}
|
|
5376
|
+
function firstEnvValue(env, keys) {
|
|
5120
5377
|
for (const key of keys) {
|
|
5121
5378
|
const value = env[key]?.trim();
|
|
5122
5379
|
if (value)
|
|
@@ -5124,71 +5381,343 @@ function firstEnv(env, keys) {
|
|
|
5124
5381
|
}
|
|
5125
5382
|
return null;
|
|
5126
5383
|
}
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5384
|
+
var DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
|
|
5385
|
+
function deprecationNotified() {
|
|
5386
|
+
const host = globalThis;
|
|
5387
|
+
const existing = host[DEPRECATION_REGISTRY];
|
|
5388
|
+
if (existing instanceof Set)
|
|
5389
|
+
return existing;
|
|
5390
|
+
const created = new Set;
|
|
5391
|
+
host[DEPRECATION_REGISTRY] = created;
|
|
5392
|
+
return created;
|
|
5393
|
+
}
|
|
5394
|
+
function defaultDeprecationSink(message) {
|
|
5395
|
+
if (typeof process !== "undefined" && process.stderr) {
|
|
5396
|
+
process.stderr.write(`${message}
|
|
5397
|
+
`);
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
function resolveCredential(name, env, options = {}) {
|
|
5401
|
+
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
5402
|
+
const diskPaths = credentialDiskSources(name, env);
|
|
5403
|
+
const explicitKey = options.apiKey?.trim();
|
|
5404
|
+
if (explicitKey) {
|
|
5405
|
+
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
5406
|
+
return sealCredential({
|
|
5407
|
+
apiKey: explicitKey,
|
|
5408
|
+
tier: "argument",
|
|
5409
|
+
source: "explicit apiKey argument",
|
|
5410
|
+
deliberate: true,
|
|
5411
|
+
deprecated: false,
|
|
5412
|
+
diskCandidates: diskPaths,
|
|
5413
|
+
warning: null
|
|
5414
|
+
});
|
|
5415
|
+
}
|
|
5416
|
+
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
5417
|
+
const overrideRaw = env[overrideKeyName];
|
|
5418
|
+
if (overrideRaw !== undefined) {
|
|
5419
|
+
const override = overrideRaw.trim();
|
|
5420
|
+
if (!override) {
|
|
5421
|
+
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]);
|
|
5422
|
+
}
|
|
5423
|
+
assertUsableCredential(name, overrideKeyName, override);
|
|
5424
|
+
return sealCredential({
|
|
5425
|
+
apiKey: override,
|
|
5426
|
+
tier: "override",
|
|
5427
|
+
source: overrideKeyName,
|
|
5428
|
+
deliberate: true,
|
|
5429
|
+
deprecated: false,
|
|
5430
|
+
diskCandidates: diskPaths,
|
|
5431
|
+
warning: null
|
|
5432
|
+
});
|
|
5433
|
+
}
|
|
5434
|
+
const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
|
|
5435
|
+
if (profile) {
|
|
5436
|
+
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
5437
|
+
if (!SAFE_PROFILE.test(profile)) {
|
|
5438
|
+
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
5439
|
+
}
|
|
5440
|
+
const paths = profileDiskSources(name, env, profile);
|
|
5441
|
+
for (const path of paths) {
|
|
5442
|
+
const value = readCredentialFile(path, apiKeyKeys);
|
|
5443
|
+
if (value) {
|
|
5444
|
+
assertUsableCredential(name, path, value);
|
|
5445
|
+
return sealCredential({
|
|
5446
|
+
apiKey: value,
|
|
5447
|
+
tier: "profile",
|
|
5448
|
+
source: path,
|
|
5449
|
+
deliberate: true,
|
|
5450
|
+
deprecated: false,
|
|
5451
|
+
diskCandidates: paths,
|
|
5452
|
+
warning: null
|
|
5453
|
+
});
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
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);
|
|
5457
|
+
}
|
|
5458
|
+
const diskHits = diskPaths.map((path) => ({ path, value: readCredentialFile(path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
5459
|
+
if (diskHits.length > 0) {
|
|
5460
|
+
const winner = diskHits[0];
|
|
5461
|
+
assertUsableCredential(name, winner.path, winner.value);
|
|
5462
|
+
const divergentSources = [
|
|
5463
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
|
|
5464
|
+
...(() => {
|
|
5465
|
+
const legacyHit = firstEnvValue(env, apiKeyKeys);
|
|
5466
|
+
return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
|
|
5467
|
+
})()
|
|
5468
|
+
];
|
|
5469
|
+
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;
|
|
5470
|
+
return sealCredential({
|
|
5471
|
+
apiKey: winner.value,
|
|
5472
|
+
tier: "disk",
|
|
5473
|
+
source: winner.path,
|
|
5474
|
+
deliberate: false,
|
|
5475
|
+
deprecated: false,
|
|
5476
|
+
diskCandidates: diskPaths,
|
|
5477
|
+
warning
|
|
5478
|
+
});
|
|
5479
|
+
}
|
|
5480
|
+
const legacy = firstEnvValue(env, apiKeyKeys);
|
|
5481
|
+
if (legacy) {
|
|
5482
|
+
assertUsableCredential(name, legacy.key, legacy.value);
|
|
5483
|
+
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.`;
|
|
5484
|
+
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}`;
|
|
5485
|
+
const sink = options.onDeprecation ?? defaultDeprecationSink;
|
|
5486
|
+
const notified = deprecationNotified();
|
|
5487
|
+
if (!notified.has(name)) {
|
|
5488
|
+
notified.add(name);
|
|
5489
|
+
sink(message);
|
|
5490
|
+
}
|
|
5491
|
+
return sealCredential({
|
|
5492
|
+
apiKey: legacy.value,
|
|
5493
|
+
tier: "legacy-env",
|
|
5494
|
+
source: legacy.key,
|
|
5495
|
+
deliberate: false,
|
|
5496
|
+
deprecated: true,
|
|
5497
|
+
diskCandidates: diskPaths,
|
|
5498
|
+
warning: message
|
|
5499
|
+
});
|
|
5500
|
+
}
|
|
5501
|
+
return null;
|
|
5502
|
+
}
|
|
5503
|
+
var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
5504
|
+
var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
5505
|
+
function isValidDnsDomain(value) {
|
|
5506
|
+
if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
|
|
5507
|
+
return false;
|
|
5508
|
+
}
|
|
5509
|
+
return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
|
|
5510
|
+
}
|
|
5511
|
+
function firstEnv(env, keys, options = {}) {
|
|
5512
|
+
for (const key of keys) {
|
|
5513
|
+
const raw = env[key];
|
|
5514
|
+
const value = raw?.trim();
|
|
5515
|
+
if (value)
|
|
5516
|
+
return { key, value: options.preserveRaw ? raw : value };
|
|
5517
|
+
}
|
|
5518
|
+
return null;
|
|
5519
|
+
}
|
|
5520
|
+
function firstEnvDefinedKey(env, keys) {
|
|
5521
|
+
for (const key of keys) {
|
|
5522
|
+
if (env[key] !== undefined)
|
|
5523
|
+
return key;
|
|
5524
|
+
}
|
|
5525
|
+
return null;
|
|
5526
|
+
}
|
|
5527
|
+
function rawAuthority(value) {
|
|
5528
|
+
const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
|
|
5529
|
+
if (!match)
|
|
5530
|
+
throw new Error("API URL must be absolute.");
|
|
5531
|
+
const afterScheme = value.slice(match[0].length);
|
|
5532
|
+
const boundary = afterScheme.search(/[/?#]/);
|
|
5533
|
+
const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
|
|
5534
|
+
if (!authority)
|
|
5535
|
+
throw new Error("API URL must include a hostname.");
|
|
5536
|
+
return authority;
|
|
5537
|
+
}
|
|
5538
|
+
function assertCanonicalPort(port) {
|
|
5539
|
+
if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
|
|
5540
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
5541
|
+
}
|
|
5542
|
+
const numericPort = Number(port);
|
|
5543
|
+
if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
|
|
5544
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
5545
|
+
}
|
|
5546
|
+
}
|
|
5547
|
+
function canonicalAuthorityHostname(authority) {
|
|
5548
|
+
let rawHostname;
|
|
5549
|
+
if (authority.startsWith("[")) {
|
|
5550
|
+
const closingBracket = authority.indexOf("]");
|
|
5551
|
+
if (closingBracket === -1) {
|
|
5552
|
+
throw new Error("API URL authority must contain a canonical hostname.");
|
|
5553
|
+
}
|
|
5554
|
+
rawHostname = authority.slice(0, closingBracket + 1);
|
|
5555
|
+
const portSuffix = authority.slice(closingBracket + 1);
|
|
5556
|
+
if (portSuffix) {
|
|
5557
|
+
if (!portSuffix.startsWith(":")) {
|
|
5558
|
+
throw new Error("API URL authority must contain a canonical hostname and port.");
|
|
5559
|
+
}
|
|
5560
|
+
assertCanonicalPort(portSuffix.slice(1));
|
|
5561
|
+
}
|
|
5562
|
+
if (isIP(rawHostname.slice(1, -1)) !== 6) {
|
|
5563
|
+
throw new Error("API URL authority must contain a canonical IPv6 literal.");
|
|
5564
|
+
}
|
|
5565
|
+
} else {
|
|
5566
|
+
const firstColon = authority.indexOf(":");
|
|
5567
|
+
const lastColon = authority.lastIndexOf(":");
|
|
5568
|
+
if (firstColon !== lastColon) {
|
|
5569
|
+
throw new Error("IPv6 API URL authorities must use brackets.");
|
|
5570
|
+
}
|
|
5571
|
+
if (lastColon !== -1) {
|
|
5572
|
+
const port = authority.slice(lastColon + 1);
|
|
5573
|
+
assertCanonicalPort(port);
|
|
5574
|
+
rawHostname = authority.slice(0, lastColon);
|
|
5575
|
+
} else {
|
|
5576
|
+
rawHostname = authority;
|
|
5577
|
+
}
|
|
5578
|
+
const ipVersion = isIP(rawHostname);
|
|
5579
|
+
const numericAddressParts = rawHostname.split(".");
|
|
5580
|
+
const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
|
|
5581
|
+
if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
|
|
5582
|
+
throw new Error("API URL authority must contain a canonical ASCII hostname.");
|
|
5137
5583
|
}
|
|
5138
|
-
throw new Error(`${key}=${value} is not a client store. ${key} no longer selects one; ` + `set ${keys.storeKeys[0]}=sqlite or ${keys.storeKeys[0]}=http instead.`);
|
|
5139
5584
|
}
|
|
5585
|
+
return rawHostname.toLowerCase();
|
|
5586
|
+
}
|
|
5587
|
+
function isDeliberateLoopbackHttpAuthority(authority) {
|
|
5588
|
+
return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
|
|
5140
5589
|
}
|
|
5141
5590
|
function toV1BaseUrl(apiUrl) {
|
|
5142
|
-
|
|
5591
|
+
if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
|
|
5592
|
+
throw new Error("API URL must not contain ASCII control characters.");
|
|
5593
|
+
}
|
|
5594
|
+
const input = apiUrl.trim();
|
|
5595
|
+
const authority = rawAuthority(input);
|
|
5596
|
+
if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
|
|
5597
|
+
throw new Error("API URL authority must be canonical ASCII without credentials.");
|
|
5598
|
+
}
|
|
5599
|
+
const canonicalHostname = canonicalAuthorityHostname(authority);
|
|
5600
|
+
const url = new URL(input);
|
|
5143
5601
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
5144
5602
|
throw new Error("API URL must use http or https.");
|
|
5145
5603
|
}
|
|
5604
|
+
if (url.username || url.password) {
|
|
5605
|
+
throw new Error("API URL must not include credentials.");
|
|
5606
|
+
}
|
|
5607
|
+
if (!url.hostname || url.hostname.endsWith(".")) {
|
|
5608
|
+
throw new Error("API URL must include a canonical hostname.");
|
|
5609
|
+
}
|
|
5610
|
+
if (url.hostname.toLowerCase() !== canonicalHostname) {
|
|
5611
|
+
throw new Error("API URL authority must not rely on parser hostname normalization.");
|
|
5612
|
+
}
|
|
5613
|
+
if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
|
|
5614
|
+
throw new Error("API URL must not use IDN or punycode hostnames.");
|
|
5615
|
+
}
|
|
5616
|
+
if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
|
|
5617
|
+
throw new Error("API URL may use http only for an exact loopback authority.");
|
|
5618
|
+
}
|
|
5619
|
+
if (url.search || url.hash) {
|
|
5620
|
+
throw new Error("API URL must not include a query string or fragment.");
|
|
5621
|
+
}
|
|
5146
5622
|
let path = url.pathname.replace(/\/+$/, "");
|
|
5147
5623
|
if (path.endsWith("/v1"))
|
|
5148
5624
|
path = path.slice(0, -"/v1".length);
|
|
5149
5625
|
url.pathname = `${path}/v1`;
|
|
5150
|
-
url.search = "";
|
|
5151
|
-
url.hash = "";
|
|
5152
5626
|
return url.toString().replace(/\/+$/, "");
|
|
5153
5627
|
}
|
|
5154
|
-
function
|
|
5155
|
-
const keys =
|
|
5156
|
-
|
|
5157
|
-
const
|
|
5158
|
-
const
|
|
5628
|
+
function resolveClientTransport(name, env = process.env, options = {}) {
|
|
5629
|
+
const keys = clientTransportEnvKeys(name);
|
|
5630
|
+
const envUrlHit = firstEnv(env, keys.apiUrlKeys, { preserveRaw: true });
|
|
5631
|
+
const explicitLocalKey = envUrlHit ? null : firstEnvDefinedKey(env, keys.apiUrlKeys);
|
|
5632
|
+
const diskUrlHit = envUrlHit || explicitLocalKey ? null : appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
5633
|
+
const urlHit = envUrlHit ?? (diskUrlHit ? { key: diskUrlHit.path, value: diskUrlHit.value } : null);
|
|
5159
5634
|
const keyHit = firstEnv(env, keys.apiKeyKeys);
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5635
|
+
const warnings = [];
|
|
5636
|
+
if (!urlHit) {
|
|
5637
|
+
if (explicitLocalKey) {
|
|
5638
|
+
const overriddenPointer = appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
5639
|
+
if (overriddenPointer) {
|
|
5640
|
+
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.`);
|
|
5641
|
+
}
|
|
5642
|
+
return {
|
|
5643
|
+
transport: "sqlite",
|
|
5644
|
+
transportSource: explicitLocalKey,
|
|
5645
|
+
baseUrl: null,
|
|
5646
|
+
apiUrlSource: null,
|
|
5647
|
+
apiKeyPresent: Boolean(keyHit),
|
|
5648
|
+
apiKeySource: keyHit ? keyHit.key : null,
|
|
5649
|
+
apiKeyTier: null,
|
|
5650
|
+
misconfigured: false,
|
|
5651
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
5652
|
+
};
|
|
5653
|
+
}
|
|
5654
|
+
return {
|
|
5655
|
+
transport: "sqlite",
|
|
5656
|
+
transportSource: "default",
|
|
5657
|
+
baseUrl: null,
|
|
5658
|
+
apiUrlSource: null,
|
|
5659
|
+
apiKeyPresent: Boolean(keyHit),
|
|
5660
|
+
apiKeySource: keyHit ? keyHit.key : null,
|
|
5661
|
+
apiKeyTier: null,
|
|
5662
|
+
misconfigured: false,
|
|
5663
|
+
warning: null
|
|
5664
|
+
};
|
|
5168
5665
|
}
|
|
5169
|
-
if (
|
|
5170
|
-
|
|
5666
|
+
if (diskUrlHit) {
|
|
5667
|
+
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.`);
|
|
5171
5668
|
}
|
|
5172
|
-
|
|
5669
|
+
const credential = resolveCredential(name, env, options.credentials);
|
|
5670
|
+
if (!credential) {
|
|
5671
|
+
const diskHint = credentialDiskSourcesForMessage(name, env);
|
|
5672
|
+
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.`);
|
|
5173
5673
|
return {
|
|
5174
5674
|
transport: "sqlite",
|
|
5175
|
-
|
|
5176
|
-
modeSource,
|
|
5675
|
+
transportSource: urlHit.key,
|
|
5177
5676
|
baseUrl: null,
|
|
5677
|
+
apiUrlSource: urlHit.key,
|
|
5178
5678
|
apiKeyPresent: false,
|
|
5679
|
+
apiKeySource: null,
|
|
5680
|
+
apiKeyTier: null,
|
|
5179
5681
|
misconfigured: true,
|
|
5180
|
-
warning:
|
|
5682
|
+
warning: warnings.join(" ")
|
|
5181
5683
|
};
|
|
5182
5684
|
}
|
|
5183
|
-
|
|
5685
|
+
if (credential.warning)
|
|
5686
|
+
warnings.push(credential.warning);
|
|
5687
|
+
const apiUrlSource = urlHit.key;
|
|
5184
5688
|
let baseUrl;
|
|
5185
5689
|
try {
|
|
5186
|
-
baseUrl = toV1BaseUrl(
|
|
5690
|
+
baseUrl = toV1BaseUrl(urlHit.value);
|
|
5187
5691
|
} catch (error) {
|
|
5188
5692
|
const message = error instanceof Error ? error.message : String(error);
|
|
5189
|
-
|
|
5693
|
+
warnings.push(`Invalid API URL from ${apiUrlSource}: ${message}. Using local store.`);
|
|
5694
|
+
return {
|
|
5695
|
+
transport: "sqlite",
|
|
5696
|
+
transportSource: urlHit.key,
|
|
5697
|
+
baseUrl: null,
|
|
5698
|
+
apiUrlSource: urlHit.key,
|
|
5699
|
+
apiKeyPresent: true,
|
|
5700
|
+
apiKeySource: credential.source,
|
|
5701
|
+
apiKeyTier: credential.tier,
|
|
5702
|
+
misconfigured: true,
|
|
5703
|
+
warning: warnings.join(" ")
|
|
5704
|
+
};
|
|
5190
5705
|
}
|
|
5191
|
-
return {
|
|
5706
|
+
return {
|
|
5707
|
+
transport: "http",
|
|
5708
|
+
transportSource: urlHit.key,
|
|
5709
|
+
baseUrl,
|
|
5710
|
+
apiUrlSource,
|
|
5711
|
+
apiKeyPresent: true,
|
|
5712
|
+
apiKeySource: credential.source,
|
|
5713
|
+
apiKeyTier: credential.tier,
|
|
5714
|
+
misconfigured: false,
|
|
5715
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
5716
|
+
};
|
|
5717
|
+
}
|
|
5718
|
+
function credentialDiskSourcesForMessage(name, env) {
|
|
5719
|
+
const paths = credentialDiskSources(name, env);
|
|
5720
|
+
return paths.length > 0 ? paths.join(" or ") : "<no HOME set in this environment, so no credential file was consulted>";
|
|
5192
5721
|
}
|
|
5193
5722
|
|
|
5194
5723
|
class HasnaHttpError extends Error {
|
|
@@ -5196,49 +5725,113 @@ class HasnaHttpError extends Error {
|
|
|
5196
5725
|
method;
|
|
5197
5726
|
path;
|
|
5198
5727
|
body;
|
|
5199
|
-
|
|
5200
|
-
|
|
5728
|
+
credentialSource;
|
|
5729
|
+
credentialTier;
|
|
5730
|
+
constructor(method, path, status, body, credential) {
|
|
5731
|
+
const guidance = credential ? `. ${credential.guidance}` : "";
|
|
5732
|
+
super(`Hasna cloud request failed: ${method} ${path} -> ${status}${guidance}`);
|
|
5201
5733
|
this.name = "HasnaHttpError";
|
|
5202
5734
|
this.status = status;
|
|
5203
5735
|
this.method = method;
|
|
5204
5736
|
this.path = path;
|
|
5205
5737
|
this.body = body;
|
|
5738
|
+
this.credentialSource = credential?.source ?? null;
|
|
5739
|
+
this.credentialTier = credential?.tier ?? null;
|
|
5740
|
+
}
|
|
5741
|
+
}
|
|
5742
|
+
function currentCredential(name, apiKey) {
|
|
5743
|
+
if (typeof apiKey === "function") {
|
|
5744
|
+
return validateAndSealResolvedCredential(name, apiKey());
|
|
5745
|
+
}
|
|
5746
|
+
return explicitCredential(name, apiKey);
|
|
5747
|
+
}
|
|
5748
|
+
function authFailureGuidance(credential) {
|
|
5749
|
+
const origin = `The API key for this request came from ${credential.source}`;
|
|
5750
|
+
if (credential.deliberate) {
|
|
5751
|
+
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.`;
|
|
5752
|
+
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}`;
|
|
5753
|
+
}
|
|
5754
|
+
if (credential.deprecated) {
|
|
5755
|
+
const target = credential.diskCandidates[0];
|
|
5756
|
+
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.`;
|
|
5757
|
+
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}`;
|
|
5758
|
+
}
|
|
5759
|
+
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.`;
|
|
5760
|
+
}
|
|
5761
|
+
var DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
|
|
5762
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
5763
|
+
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
5764
|
+
"host",
|
|
5765
|
+
":authority",
|
|
5766
|
+
"forwarded",
|
|
5767
|
+
"x-forwarded-host",
|
|
5768
|
+
"x-original-host"
|
|
5769
|
+
]);
|
|
5770
|
+
function assertNoAuthorityOverrideHeaders(headers, source) {
|
|
5771
|
+
if (!headers)
|
|
5772
|
+
return;
|
|
5773
|
+
const forbidden = Object.keys(headers).find((name) => AUTHORITY_OVERRIDE_HEADERS.has(name.trim().toLowerCase()));
|
|
5774
|
+
if (forbidden) {
|
|
5775
|
+
throw new Error(`Authenticated ${source} headers must not set authority header '${forbidden}'.`);
|
|
5206
5776
|
}
|
|
5207
5777
|
}
|
|
5208
5778
|
function appendQuery(path, query) {
|
|
5209
5779
|
if (!query)
|
|
5210
5780
|
return path;
|
|
5211
|
-
const params = new URLSearchParams;
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5781
|
+
const params = query instanceof URLSearchParams ? query : new URLSearchParams;
|
|
5782
|
+
if (!(query instanceof URLSearchParams)) {
|
|
5783
|
+
for (const [key, value] of Object.entries(query)) {
|
|
5784
|
+
if (value === null || value === undefined)
|
|
5785
|
+
continue;
|
|
5786
|
+
if (Array.isArray(value)) {
|
|
5787
|
+
for (const v of value)
|
|
5788
|
+
params.append(key, String(v));
|
|
5789
|
+
} else {
|
|
5790
|
+
params.append(key, String(value));
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5220
5793
|
}
|
|
5221
5794
|
const qs = params.toString();
|
|
5222
|
-
|
|
5795
|
+
if (!qs)
|
|
5796
|
+
return path;
|
|
5797
|
+
return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
|
|
5223
5798
|
}
|
|
5224
|
-
var
|
|
5225
|
-
|
|
5226
|
-
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5227
|
-
function createHttpTransport(options) {
|
|
5799
|
+
var defaultSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5800
|
+
function createHasnaHttpTransport(options) {
|
|
5228
5801
|
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
5229
|
-
const base = options.baseUrl
|
|
5802
|
+
const base = toV1BaseUrl(options.baseUrl);
|
|
5230
5803
|
const timeoutMs = options.timeoutMs ?? 30000;
|
|
5231
5804
|
const sleep = options.sleepImpl ?? defaultSleep;
|
|
5232
|
-
|
|
5805
|
+
const defaultRetry = options.retry;
|
|
5806
|
+
function resolveRetry(callRetry) {
|
|
5807
|
+
const chosen = callRetry !== undefined ? callRetry : defaultRetry;
|
|
5808
|
+
if (chosen === false)
|
|
5809
|
+
return null;
|
|
5810
|
+
const r = chosen ?? {};
|
|
5811
|
+
return {
|
|
5812
|
+
retries: r.retries ?? 2,
|
|
5813
|
+
baseDelayMs: r.baseDelayMs ?? 200,
|
|
5814
|
+
maxDelayMs: r.maxDelayMs ?? 2000,
|
|
5815
|
+
retryStatuses: r.retryStatuses ?? [...DEFAULT_RETRY_STATUSES]
|
|
5816
|
+
};
|
|
5817
|
+
}
|
|
5818
|
+
async function once(method, rel, url, body, opts, credential) {
|
|
5819
|
+
assertNoAuthorityOverrideHeaders(options.headers, "transport");
|
|
5820
|
+
assertNoAuthorityOverrideHeaders(opts.headers, "request");
|
|
5233
5821
|
const headers = {
|
|
5234
|
-
"x-api-key":
|
|
5235
|
-
Authorization: `Bearer ${
|
|
5822
|
+
"x-api-key": credential.apiKey,
|
|
5823
|
+
Authorization: `Bearer ${credential.apiKey}`,
|
|
5236
5824
|
Accept: "application/json",
|
|
5825
|
+
...options.headers ?? {},
|
|
5237
5826
|
...opts.headers ?? {}
|
|
5238
5827
|
};
|
|
5239
5828
|
if (opts.idempotencyKey)
|
|
5240
5829
|
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
5241
|
-
const init = {
|
|
5830
|
+
const init = {
|
|
5831
|
+
method,
|
|
5832
|
+
headers,
|
|
5833
|
+
redirect: "manual"
|
|
5834
|
+
};
|
|
5242
5835
|
if (body !== undefined) {
|
|
5243
5836
|
headers["Content-Type"] = "application/json";
|
|
5244
5837
|
init.body = JSON.stringify(body);
|
|
@@ -5276,7 +5869,27 @@ function createHttpTransport(options) {
|
|
|
5276
5869
|
}
|
|
5277
5870
|
}
|
|
5278
5871
|
if (!response.ok) {
|
|
5279
|
-
|
|
5872
|
+
if (response.status >= 300 && response.status < 400) {
|
|
5873
|
+
return {
|
|
5874
|
+
ok: false,
|
|
5875
|
+
retryable: false,
|
|
5876
|
+
error: new HasnaHttpError(method, rel, response.status, parsed)
|
|
5877
|
+
};
|
|
5878
|
+
}
|
|
5879
|
+
if (response.status === 401 || response.status === 403) {
|
|
5880
|
+
return {
|
|
5881
|
+
ok: false,
|
|
5882
|
+
retryable: false,
|
|
5883
|
+
error: new HasnaHttpError(method, rel, response.status, parsed, {
|
|
5884
|
+
source: credential.source,
|
|
5885
|
+
tier: credential.tier,
|
|
5886
|
+
guidance: authFailureGuidance(credential)
|
|
5887
|
+
})
|
|
5888
|
+
};
|
|
5889
|
+
}
|
|
5890
|
+
const retry = resolveRetry(opts.retry);
|
|
5891
|
+
const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
|
|
5892
|
+
return { ok: false, retryable, error: new HasnaHttpError(method, rel, response.status, parsed) };
|
|
5280
5893
|
}
|
|
5281
5894
|
return { ok: true, value: parsed };
|
|
5282
5895
|
}
|
|
@@ -5284,24 +5897,23 @@ function createHttpTransport(options) {
|
|
|
5284
5897
|
const upper = method.toUpperCase();
|
|
5285
5898
|
const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
|
|
5286
5899
|
const url = `${base}${rel}`;
|
|
5287
|
-
const
|
|
5288
|
-
const
|
|
5289
|
-
const maxAttempts = methodRetryable ?
|
|
5900
|
+
const retry = resolveRetry(opts.retry);
|
|
5901
|
+
const methodRetryable = IDEMPOTENT_METHODS.has(upper) || Boolean(opts.idempotencyKey);
|
|
5902
|
+
const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
|
|
5903
|
+
const credential = currentCredential(options.name, options.apiKey);
|
|
5290
5904
|
let last = null;
|
|
5291
5905
|
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
5292
|
-
const result = await once(upper, rel, url, body, opts);
|
|
5906
|
+
const result = await once(upper, rel, url, body, opts, credential);
|
|
5293
5907
|
if (result.ok)
|
|
5294
5908
|
return result.value;
|
|
5295
5909
|
last = result;
|
|
5296
|
-
const canRetry = methodRetryable && result.retryable && attempt < maxAttempts;
|
|
5910
|
+
const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
|
|
5297
5911
|
if (!canRetry)
|
|
5298
5912
|
break;
|
|
5299
|
-
const backoff = Math.min(
|
|
5913
|
+
const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
|
|
5300
5914
|
const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
|
|
5301
5915
|
await sleep(backoff + jitter);
|
|
5302
5916
|
}
|
|
5303
|
-
if (last === null)
|
|
5304
|
-
throw new Error(`Request to ${rel} completed without a result`);
|
|
5305
5917
|
throw last.error;
|
|
5306
5918
|
}
|
|
5307
5919
|
return {
|
|
@@ -5309,81 +5921,289 @@ function createHttpTransport(options) {
|
|
|
5309
5921
|
request,
|
|
5310
5922
|
get: (path, opts) => request("GET", path, undefined, opts),
|
|
5311
5923
|
post: (path, body, opts) => request("POST", path, body, opts),
|
|
5312
|
-
patch: (path, body, opts) => request("PATCH", path, body, opts),
|
|
5313
5924
|
put: (path, body, opts) => request("PUT", path, body, opts),
|
|
5925
|
+
patch: (path, body, opts) => request("PATCH", path, body, opts),
|
|
5314
5926
|
del: (path, body, opts) => request("DELETE", path, body, opts)
|
|
5315
5927
|
};
|
|
5316
5928
|
}
|
|
5929
|
+
function createClientTransport(name, env = process.env, overrides) {
|
|
5930
|
+
const credentialOptions = overrides?.credentials;
|
|
5931
|
+
const resolution = resolveClientTransport(name, env, { ...credentialOptions ? { credentials: credentialOptions } : {} });
|
|
5932
|
+
if (resolution.misconfigured) {
|
|
5933
|
+
throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for the API client.`);
|
|
5934
|
+
}
|
|
5935
|
+
if (resolution.transport === "sqlite" || !resolution.baseUrl) {
|
|
5936
|
+
return { transport: "sqlite", client: null, resolution };
|
|
5937
|
+
}
|
|
5938
|
+
const credentialProvider = () => {
|
|
5939
|
+
const resolved = resolveCredential(name, env, credentialOptions);
|
|
5940
|
+
if (!resolved) {
|
|
5941
|
+
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.`);
|
|
5942
|
+
}
|
|
5943
|
+
return resolved;
|
|
5944
|
+
};
|
|
5945
|
+
return {
|
|
5946
|
+
transport: "http",
|
|
5947
|
+
client: createHasnaHttpTransport({
|
|
5948
|
+
name,
|
|
5949
|
+
baseUrl: resolution.baseUrl,
|
|
5950
|
+
apiKey: credentialProvider,
|
|
5951
|
+
...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
|
|
5952
|
+
...overrides?.headers ? { headers: overrides.headers } : {},
|
|
5953
|
+
...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
|
|
5954
|
+
...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
|
|
5955
|
+
...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
|
|
5956
|
+
}),
|
|
5957
|
+
resolution
|
|
5958
|
+
};
|
|
5959
|
+
}
|
|
5960
|
+
|
|
5961
|
+
// ../contracts/dist/client/storage.js
|
|
5962
|
+
var MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
|
|
5963
|
+
var INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
|
|
5964
|
+
var CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
|
|
5965
|
+
var DEPRECATION_REGISTRY2 = Symbol.for("hasna:contracts:credentialDeprecationNotices");
|
|
5966
|
+
var IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
5967
|
+
var AUTHORITY_OVERRIDE_HEADERS2 = new Set([
|
|
5968
|
+
"host",
|
|
5969
|
+
":authority",
|
|
5970
|
+
"forwarded",
|
|
5971
|
+
"x-forwarded-host",
|
|
5972
|
+
"x-original-host"
|
|
5973
|
+
]);
|
|
5974
|
+
function resourcePath(resource) {
|
|
5975
|
+
const trimmed = resource.replace(/^\/+|\/+$/g, "");
|
|
5976
|
+
if (!trimmed)
|
|
5977
|
+
throw new Error("resource must be a non-empty path segment");
|
|
5978
|
+
return `/${trimmed}`;
|
|
5979
|
+
}
|
|
5980
|
+
function entityPath(resource, id) {
|
|
5981
|
+
if (id === undefined || id === null || `${id}`.length === 0) {
|
|
5982
|
+
throw new Error("id must be a non-empty string");
|
|
5983
|
+
}
|
|
5984
|
+
return `${resourcePath(resource)}/${encodeURIComponent(String(id))}`;
|
|
5985
|
+
}
|
|
5317
5986
|
function newIdempotencyKey() {
|
|
5318
5987
|
const g = globalThis;
|
|
5319
5988
|
if (g.crypto?.randomUUID)
|
|
5320
5989
|
return g.crypto.randomUUID();
|
|
5321
5990
|
return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
5322
5991
|
}
|
|
5323
|
-
function extractItems(raw
|
|
5992
|
+
function extractItems(raw) {
|
|
5324
5993
|
if (Array.isArray(raw))
|
|
5325
5994
|
return raw;
|
|
5326
5995
|
if (raw && typeof raw === "object") {
|
|
5327
5996
|
const obj = raw;
|
|
5328
|
-
for (const key of [
|
|
5997
|
+
for (const key of ["items", "data", "results", "rows", "records"]) {
|
|
5329
5998
|
if (Array.isArray(obj[key]))
|
|
5330
5999
|
return obj[key];
|
|
5331
6000
|
}
|
|
5332
6001
|
}
|
|
5333
6002
|
return [];
|
|
5334
6003
|
}
|
|
5335
|
-
function
|
|
5336
|
-
|
|
5337
|
-
|
|
6004
|
+
function extractTotal(raw) {
|
|
6005
|
+
if (raw && typeof raw === "object") {
|
|
6006
|
+
const obj = raw;
|
|
6007
|
+
for (const key of ["total", "count", "totalCount", "total_count"]) {
|
|
6008
|
+
if (typeof obj[key] === "number")
|
|
6009
|
+
return obj[key];
|
|
6010
|
+
}
|
|
6011
|
+
}
|
|
6012
|
+
return null;
|
|
6013
|
+
}
|
|
6014
|
+
function extractCursor(raw) {
|
|
6015
|
+
if (raw && typeof raw === "object") {
|
|
6016
|
+
const obj = raw;
|
|
6017
|
+
for (const key of ["cursor", "nextCursor", "next_cursor", "next"]) {
|
|
6018
|
+
if (typeof obj[key] === "string")
|
|
6019
|
+
return obj[key];
|
|
6020
|
+
}
|
|
6021
|
+
}
|
|
6022
|
+
return null;
|
|
6023
|
+
}
|
|
6024
|
+
function isNotFoundHttpError(error) {
|
|
6025
|
+
return typeof error === "object" && error !== null && error.name === "HasnaHttpError" && error.status === 404;
|
|
6026
|
+
}
|
|
6027
|
+
function createHasnaStorageClient(name, transport) {
|
|
5338
6028
|
return {
|
|
5339
6029
|
name,
|
|
5340
6030
|
baseUrl: transport.baseUrl,
|
|
5341
6031
|
transport,
|
|
5342
|
-
async list(resource,
|
|
5343
|
-
const raw = await transport.get(
|
|
5344
|
-
return {
|
|
6032
|
+
async list(resource, options = {}) {
|
|
6033
|
+
const raw = await transport.get(resourcePath(resource), options);
|
|
6034
|
+
return {
|
|
6035
|
+
items: extractItems(raw),
|
|
6036
|
+
total: extractTotal(raw),
|
|
6037
|
+
cursor: extractCursor(raw),
|
|
6038
|
+
raw
|
|
6039
|
+
};
|
|
5345
6040
|
},
|
|
5346
|
-
async get(resource, id) {
|
|
6041
|
+
async get(resource, id, options = {}) {
|
|
5347
6042
|
try {
|
|
5348
|
-
return await transport.get(
|
|
6043
|
+
return await transport.get(entityPath(resource, id), options);
|
|
5349
6044
|
} catch (error) {
|
|
5350
|
-
if (error
|
|
6045
|
+
if (isNotFoundHttpError(error))
|
|
5351
6046
|
return null;
|
|
5352
6047
|
throw error;
|
|
5353
6048
|
}
|
|
5354
6049
|
},
|
|
5355
|
-
async create(resource, body,
|
|
5356
|
-
|
|
6050
|
+
async create(resource, body, options = {}) {
|
|
6051
|
+
const { idempotencyKey, ...rest } = options;
|
|
6052
|
+
return transport.post(resourcePath(resource), body, {
|
|
6053
|
+
...rest,
|
|
6054
|
+
idempotencyKey: idempotencyKey ?? newIdempotencyKey()
|
|
6055
|
+
});
|
|
5357
6056
|
},
|
|
5358
|
-
async update(resource, id, patch,
|
|
6057
|
+
async update(resource, id, patch, options = {}) {
|
|
6058
|
+
const { method = "PATCH", idempotencyKey, ...rest } = options;
|
|
5359
6059
|
const call = method === "PUT" ? transport.put : transport.patch;
|
|
5360
|
-
return call(
|
|
6060
|
+
return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
|
|
5361
6061
|
},
|
|
5362
|
-
async delete(resource, id) {
|
|
6062
|
+
async delete(resource, id, options = {}) {
|
|
5363
6063
|
try {
|
|
5364
|
-
await transport.del(
|
|
6064
|
+
await transport.del(entityPath(resource, id), undefined, options);
|
|
5365
6065
|
} catch (error) {
|
|
5366
|
-
if (error
|
|
6066
|
+
if (isNotFoundHttpError(error))
|
|
5367
6067
|
return;
|
|
5368
6068
|
throw error;
|
|
5369
6069
|
}
|
|
5370
6070
|
}
|
|
5371
6071
|
};
|
|
5372
6072
|
}
|
|
5373
|
-
|
|
6073
|
+
|
|
6074
|
+
// src/http/client.ts
|
|
6075
|
+
function envToken2(name) {
|
|
6076
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
6077
|
+
}
|
|
6078
|
+
function envKeys(name) {
|
|
6079
|
+
const token = envToken2(name);
|
|
6080
|
+
return {
|
|
6081
|
+
storeKeys: [`HASNA_${token}_CLIENT_STORE`, `${token}_CLIENT_STORE`],
|
|
6082
|
+
apiUrlKeys: [`HASNA_${token}_API_URL`],
|
|
6083
|
+
apiKeyKeys: [`HASNA_${token}_API_KEY`]
|
|
6084
|
+
};
|
|
6085
|
+
}
|
|
6086
|
+
function normalizeClientStore(value) {
|
|
6087
|
+
const normalized = value.trim().toLowerCase();
|
|
6088
|
+
if (normalized === "sqlite")
|
|
6089
|
+
return "sqlite";
|
|
6090
|
+
if (normalized === "http" || normalized === "https")
|
|
6091
|
+
return "http";
|
|
6092
|
+
throw new Error(`Unknown client store: ${value}. Use sqlite or http.`);
|
|
6093
|
+
}
|
|
6094
|
+
function firstEnv2(env, keys) {
|
|
6095
|
+
for (const key of keys) {
|
|
6096
|
+
const value = env[key]?.trim();
|
|
6097
|
+
if (value)
|
|
6098
|
+
return { key, value };
|
|
6099
|
+
}
|
|
6100
|
+
return null;
|
|
6101
|
+
}
|
|
6102
|
+
function toV1BaseUrl2(apiUrl) {
|
|
6103
|
+
const url = new URL(apiUrl);
|
|
6104
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
6105
|
+
throw new Error("API URL must use http or https.");
|
|
6106
|
+
}
|
|
6107
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
6108
|
+
if (path.endsWith("/v1"))
|
|
6109
|
+
path = path.slice(0, -"/v1".length);
|
|
6110
|
+
url.pathname = `${path}/v1`;
|
|
6111
|
+
url.search = "";
|
|
6112
|
+
url.hash = "";
|
|
6113
|
+
return url.toString().replace(/\/+$/, "");
|
|
6114
|
+
}
|
|
6115
|
+
function resolveTransport(name, env = process.env) {
|
|
6116
|
+
const keys = envKeys(name);
|
|
6117
|
+
const storeHit = firstEnv2(env, keys.storeKeys);
|
|
6118
|
+
const urlHit = firstEnv2(env, keys.apiUrlKeys);
|
|
6119
|
+
const keyHit = firstEnv2(env, keys.apiKeyKeys);
|
|
6120
|
+
let requested = "sqlite";
|
|
6121
|
+
let modeSource = "default";
|
|
6122
|
+
if (storeHit) {
|
|
6123
|
+
requested = normalizeClientStore(storeHit.value);
|
|
6124
|
+
modeSource = storeHit.key;
|
|
6125
|
+
} else if (urlHit && keyHit) {
|
|
6126
|
+
requested = "http";
|
|
6127
|
+
modeSource = "auto:api-url+api-key";
|
|
6128
|
+
} else if (urlHit || keyHit) {
|
|
6129
|
+
const missing = urlHit ? keys.apiKeyKeys[0] : keys.apiUrlKeys[0];
|
|
6130
|
+
const present = urlHit ? keys.apiUrlKeys[0] : keys.apiKeyKeys[0];
|
|
6131
|
+
return {
|
|
6132
|
+
transport: "sqlite",
|
|
6133
|
+
requested,
|
|
6134
|
+
modeSource,
|
|
6135
|
+
baseUrl: null,
|
|
6136
|
+
apiKeyPresent: Boolean(keyHit),
|
|
6137
|
+
misconfigured: true,
|
|
6138
|
+
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.`
|
|
6139
|
+
};
|
|
6140
|
+
}
|
|
6141
|
+
if (requested === "sqlite") {
|
|
6142
|
+
return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: Boolean(keyHit), misconfigured: false, warning: null };
|
|
6143
|
+
}
|
|
6144
|
+
if (!urlHit) {
|
|
6145
|
+
return {
|
|
6146
|
+
transport: "sqlite",
|
|
6147
|
+
requested,
|
|
6148
|
+
modeSource,
|
|
6149
|
+
baseUrl: null,
|
|
6150
|
+
apiKeyPresent: Boolean(keyHit),
|
|
6151
|
+
misconfigured: true,
|
|
6152
|
+
warning: `${modeSource}=http but no API URL is set (${keys.apiUrlKeys[0]}). Refusing to route to the API.`
|
|
6153
|
+
};
|
|
6154
|
+
}
|
|
6155
|
+
if (!keyHit) {
|
|
6156
|
+
return {
|
|
6157
|
+
transport: "sqlite",
|
|
6158
|
+
requested,
|
|
6159
|
+
modeSource,
|
|
6160
|
+
baseUrl: null,
|
|
6161
|
+
apiKeyPresent: false,
|
|
6162
|
+
misconfigured: true,
|
|
6163
|
+
warning: `${modeSource}=http but no API key is set (${keys.apiKeyKeys[0]}). Refusing to route to the API.`
|
|
6164
|
+
};
|
|
6165
|
+
}
|
|
6166
|
+
const rawUrl = urlHit.value;
|
|
6167
|
+
let baseUrl;
|
|
6168
|
+
try {
|
|
6169
|
+
baseUrl = toV1BaseUrl2(rawUrl);
|
|
6170
|
+
} catch (error) {
|
|
6171
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6172
|
+
return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: true, misconfigured: true, warning: `Invalid API URL: ${message}.` };
|
|
6173
|
+
}
|
|
6174
|
+
return { transport: "http", requested, modeSource, baseUrl, apiKeyPresent: true, misconfigured: false, warning: null };
|
|
6175
|
+
}
|
|
6176
|
+
var RETRY_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
6177
|
+
var IDEMPOTENT = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
6178
|
+
function resolveStoreClient(name, env = process.env) {
|
|
5374
6179
|
const resolution = resolveTransport(name, env);
|
|
5375
6180
|
if (resolution.misconfigured) {
|
|
6181
|
+
const wired2 = createClientTransport(name, env);
|
|
6182
|
+
if (wired2.transport === "http") {
|
|
6183
|
+
return {
|
|
6184
|
+
transport: "http",
|
|
6185
|
+
client: createHasnaStorageClient(name, wired2.client),
|
|
6186
|
+
resolution: {
|
|
6187
|
+
transport: "http",
|
|
6188
|
+
requested: "http",
|
|
6189
|
+
modeSource: resolution.modeSource === "default" ? "auto:api-url+seam-credential" : resolution.modeSource,
|
|
6190
|
+
baseUrl: wired2.resolution.baseUrl,
|
|
6191
|
+
apiKeyPresent: true,
|
|
6192
|
+
misconfigured: false,
|
|
6193
|
+
warning: null
|
|
6194
|
+
}
|
|
6195
|
+
};
|
|
6196
|
+
}
|
|
5376
6197
|
throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for the /v1 API.`);
|
|
5377
6198
|
}
|
|
5378
6199
|
if (resolution.transport === "sqlite" || !resolution.baseUrl) {
|
|
5379
6200
|
return { transport: "sqlite", client: null, resolution };
|
|
5380
6201
|
}
|
|
5381
|
-
const
|
|
5382
|
-
|
|
5383
|
-
if (!apiKey)
|
|
6202
|
+
const wired = createClientTransport(name, env);
|
|
6203
|
+
if (wired.transport !== "http") {
|
|
5384
6204
|
throw new Error(`Client for '${name}' resolved to the /v1 API without an API key.`);
|
|
5385
|
-
|
|
5386
|
-
return { transport: "http", client:
|
|
6205
|
+
}
|
|
6206
|
+
return { transport: "http", client: createHasnaStorageClient(name, wired.client), resolution };
|
|
5387
6207
|
}
|
|
5388
6208
|
|
|
5389
6209
|
// src/store.ts
|
|
@@ -5466,6 +6286,22 @@ var localStore = {
|
|
|
5466
6286
|
await withLocalStoreReaderLease(() => saveFeedback(input));
|
|
5467
6287
|
}
|
|
5468
6288
|
};
|
|
6289
|
+
async function listResource(client, resource, query) {
|
|
6290
|
+
const raw = await client.transport.get(`/${resource}`, query ? { query } : undefined);
|
|
6291
|
+
return { items: extractEnvelopeItems(raw, resource), raw };
|
|
6292
|
+
}
|
|
6293
|
+
function extractEnvelopeItems(raw, resource) {
|
|
6294
|
+
if (Array.isArray(raw))
|
|
6295
|
+
return raw;
|
|
6296
|
+
if (raw && typeof raw === "object") {
|
|
6297
|
+
const obj = raw;
|
|
6298
|
+
for (const key of [resource, "items", "data", "results", "rows", "records"]) {
|
|
6299
|
+
if (Array.isArray(obj[key]))
|
|
6300
|
+
return obj[key];
|
|
6301
|
+
}
|
|
6302
|
+
}
|
|
6303
|
+
return [];
|
|
6304
|
+
}
|
|
5469
6305
|
function apiStore(client) {
|
|
5470
6306
|
return {
|
|
5471
6307
|
mode: "http",
|
|
@@ -5473,7 +6309,7 @@ function apiStore(client) {
|
|
|
5473
6309
|
async createRecording(input, idempotencyKey) {
|
|
5474
6310
|
const keyCandidate = idempotencyKey === undefined && (input.id === undefined || input.id === null) ? randomUUID2() : idempotencyKey;
|
|
5475
6311
|
const identity = recordingCreateIdentity(input, keyCandidate, { bindIdempotencyKeyToId: false });
|
|
5476
|
-
const res = await client.create("recordings", identity.input, identity.idempotencyKey);
|
|
6312
|
+
const res = await client.create("recordings", identity.input, { idempotencyKey: identity.idempotencyKey });
|
|
5477
6313
|
return unwrap(res, "recording");
|
|
5478
6314
|
},
|
|
5479
6315
|
async getRecording(id) {
|
|
@@ -5481,7 +6317,7 @@ function apiStore(client) {
|
|
|
5481
6317
|
return res ? unwrap(res, "recording") : null;
|
|
5482
6318
|
},
|
|
5483
6319
|
async listRecordings(filter) {
|
|
5484
|
-
const { items } = await client
|
|
6320
|
+
const { items } = await listResource(client, "recordings", listQuery(filter));
|
|
5485
6321
|
return items;
|
|
5486
6322
|
},
|
|
5487
6323
|
async countRecordings(filter) {
|
|
@@ -5492,7 +6328,7 @@ function apiStore(client) {
|
|
|
5492
6328
|
const seenPageKeys = new Set;
|
|
5493
6329
|
while (pageRequests < maxPageRequests) {
|
|
5494
6330
|
pageRequests += 1;
|
|
5495
|
-
const { items, raw } = await client
|
|
6331
|
+
const { items, raw } = await listResource(client, "recordings", {
|
|
5496
6332
|
...listQuery(filter),
|
|
5497
6333
|
limit: pageLimit,
|
|
5498
6334
|
offset
|
|
@@ -5515,7 +6351,7 @@ function apiStore(client) {
|
|
|
5515
6351
|
throw new Error(`Recordings API exceeded ${maxPageRequests} pages while counting legacy results`);
|
|
5516
6352
|
},
|
|
5517
6353
|
async searchRecordings(query, filter) {
|
|
5518
|
-
const { items } = await client
|
|
6354
|
+
const { items } = await listResource(client, "recordings", listQuery({ ...filter ?? {}, search: query }));
|
|
5519
6355
|
return items;
|
|
5520
6356
|
},
|
|
5521
6357
|
async deleteRecording(id) {
|
|
@@ -5547,7 +6383,7 @@ function apiStore(client) {
|
|
|
5547
6383
|
return res ? unwrap(res, "agent") : null;
|
|
5548
6384
|
},
|
|
5549
6385
|
async listAgents() {
|
|
5550
|
-
const { items } = await client
|
|
6386
|
+
const { items } = await listResource(client, "agents");
|
|
5551
6387
|
return items;
|
|
5552
6388
|
},
|
|
5553
6389
|
async heartbeatAgent(idOrName) {
|
|
@@ -5587,7 +6423,7 @@ function apiStore(client) {
|
|
|
5587
6423
|
return res ? unwrap(res, "project") : null;
|
|
5588
6424
|
},
|
|
5589
6425
|
async listProjects() {
|
|
5590
|
-
const { items } = await client
|
|
6426
|
+
const { items } = await listResource(client, "projects");
|
|
5591
6427
|
return items;
|
|
5592
6428
|
},
|
|
5593
6429
|
async saveFeedback(input) {
|
|
@@ -5635,7 +6471,7 @@ var cached = null;
|
|
|
5635
6471
|
function getStore(env = process.env) {
|
|
5636
6472
|
if (env === process.env && cached)
|
|
5637
6473
|
return cached;
|
|
5638
|
-
const resolved =
|
|
6474
|
+
const resolved = resolveStoreClient(APP, env);
|
|
5639
6475
|
const store = resolved.transport === "http" ? apiStore(resolved.client) : localStore;
|
|
5640
6476
|
if (env === process.env)
|
|
5641
6477
|
cached = store;
|