@hasna/instructions 0.5.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -20
- package/dist/cli/fail-closed-no-env.test.d.ts +2 -0
- package/dist/cli/fail-closed-no-env.test.d.ts.map +1 -0
- package/dist/cli/index.js +1711 -422
- package/dist/data/config-store.d.ts +62 -33
- package/dist/data/config-store.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/generated/storage-kit/backend.d.ts +4 -4
- package/dist/generated/storage-kit/backend.d.ts.map +1 -1
- package/dist/generated/storage-kit/index.d.ts +1 -1
- package/dist/generated/storage-kit/index.d.ts.map +1 -1
- package/dist/generated/storage-kit/migrations.d.ts +21 -0
- package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
- package/dist/generated/storage-kit/pool.d.ts +2 -5
- package/dist/generated/storage-kit/pool.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2486 -386
- package/dist/lib/app-home.d.ts +9 -0
- package/dist/lib/app-home.d.ts.map +1 -1
- package/dist/lib/client-types.d.ts +130 -0
- package/dist/lib/client-types.d.ts.map +1 -0
- package/dist/lib/client-types.test.d.ts +2 -0
- package/dist/lib/client-types.test.d.ts.map +1 -0
- package/dist/lib/local-opt-in.d.ts +82 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/project-context.d.ts +14 -14
- package/dist/lib/project-context.d.ts.map +1 -1
- package/dist/lib/project-dashboard-standard.d.ts +1 -1
- package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
- package/dist/lib/session-render-state-hermeticity.test.d.ts +2 -0
- package/dist/lib/session-render-state-hermeticity.test.d.ts.map +1 -0
- package/dist/lib/session-render-state.d.ts +9 -0
- package/dist/lib/session-render-state.d.ts.map +1 -1
- package/dist/lib/transport-resolver.d.ts +80 -0
- package/dist/lib/transport-resolver.d.ts.map +1 -0
- package/dist/lib/transport-resolver.test.d.ts +2 -0
- package/dist/lib/transport-resolver.test.d.ts.map +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1439 -195
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +22 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +1042 -0
- package/dist/sdk/resolve.d.ts +82 -0
- package/dist/sdk/resolve.d.ts.map +1 -0
- package/dist/sdk/resolve.test.d.ts +2 -0
- package/dist/sdk/resolve.test.d.ts.map +1 -0
- package/dist/sdk/sdk-bundle-self-contained.test.d.ts +2 -0
- package/dist/sdk/sdk-bundle-self-contained.test.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +288 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -0
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +55 -64
- package/dist/test-support/preload-state-home.d.ts +2 -0
- package/dist/test-support/preload-state-home.d.ts.map +1 -0
- package/package.json +12 -12
- package/dashboard/README.md +0 -37
- package/dist/lib/retired-storage-mode.d.ts +0 -8
- package/dist/lib/retired-storage-mode.d.ts.map +0 -1
package/dist/mcp/index.js
CHANGED
|
@@ -73,116 +73,167 @@ var init_types = __esm(() => {
|
|
|
73
73
|
};
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return key;
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
76
|
+
// ../contracts/dist/client/transport.js
|
|
77
|
+
import { createRequire } from "module";
|
|
78
|
+
function envToken(name) {
|
|
79
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
83
80
|
}
|
|
84
|
-
function
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
81
|
+
function clientTransportEnvKeys(name) {
|
|
82
|
+
const envSegment = envToken(name);
|
|
83
|
+
return {
|
|
84
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
85
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function credentialOverrideEnvKey(name) {
|
|
89
|
+
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
90
|
+
}
|
|
91
|
+
function credentialPointerEnvKey(name) {
|
|
92
|
+
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
93
|
+
}
|
|
94
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", MAX_CREDENTIAL_FILE_BYTES, INSPECT_CUSTOM, CREDENTIAL_SEAL, AMBIENT_ENVIRONMENT, SECRETS_PACKAGE_SPECIFIER, requireSecretsSdk, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
|
|
95
|
+
var init_transport = __esm(() => {
|
|
96
|
+
MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
97
|
+
INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
98
|
+
CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
99
|
+
AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
100
|
+
SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
101
|
+
requireSecretsSdk = createRequire(import.meta.url);
|
|
102
|
+
IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
103
|
+
AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
104
|
+
"host",
|
|
105
|
+
":authority",
|
|
106
|
+
"forwarded",
|
|
107
|
+
"x-forwarded-host",
|
|
108
|
+
"x-original-host"
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// src/lib/local-opt-in.ts
|
|
113
|
+
function instructionsLocalModeNotice() {
|
|
114
|
+
return `instructions: local mode \u2014 ${INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS[0]}=1 selects the on-box SQLite store, ` + `and no hosted authority was configured. Set HASNA_INSTRUCTIONS_API_KEY (or add the Keychain item ` + `hasna.credentials.instructions.api-key, or write ~/.hasna/instructions/config/credentials) to go hosted.`;
|
|
115
|
+
}
|
|
116
|
+
function isInstructionsLocalOptIn(env = process.env) {
|
|
117
|
+
return INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() === "1");
|
|
118
|
+
}
|
|
119
|
+
function instructionsAuthorityEnvKeys() {
|
|
120
|
+
const keys = clientTransportEnvKeys("instructions");
|
|
121
|
+
return [
|
|
122
|
+
...keys.apiUrlKeys,
|
|
123
|
+
...keys.apiKeyKeys,
|
|
124
|
+
credentialOverrideEnvKey("instructions"),
|
|
125
|
+
credentialPointerEnvKey("instructions"),
|
|
126
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
97
127
|
];
|
|
128
|
+
}
|
|
129
|
+
function hasInstructionsEnvAuthorityIntent(env = process.env) {
|
|
130
|
+
return instructionsAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
|
|
131
|
+
}
|
|
132
|
+
function selectsInstructionsLocalStore(env = process.env) {
|
|
133
|
+
return !hasInstructionsEnvAuthorityIntent(env) && isInstructionsLocalOptIn(env);
|
|
134
|
+
}
|
|
135
|
+
function instructionsResolverEnv(env) {
|
|
136
|
+
const blanks = instructionsAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
137
|
+
if (blanks.length === 0)
|
|
138
|
+
return env;
|
|
139
|
+
const next = { ...env };
|
|
140
|
+
for (const key of blanks)
|
|
141
|
+
delete next[key];
|
|
142
|
+
return next;
|
|
143
|
+
}
|
|
144
|
+
function isAmbientInstructionsEnv(env) {
|
|
145
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
146
|
+
return true;
|
|
147
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
148
|
+
}
|
|
149
|
+
function instructionsResolverInputs(env, credentials = {}) {
|
|
150
|
+
const normalised = instructionsResolverEnv(env);
|
|
151
|
+
if (normalised === env)
|
|
152
|
+
return { env: normalised, credentials };
|
|
153
|
+
const keychain = { ...credentials.keychain };
|
|
154
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
155
|
+
keychain.enabled = isAmbientInstructionsEnv(env);
|
|
156
|
+
}
|
|
157
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
158
|
+
}
|
|
159
|
+
var INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS, CONTRACTS_AMBIENT_ENVIRONMENT;
|
|
160
|
+
var init_local_opt_in = __esm(() => {
|
|
161
|
+
init_transport();
|
|
162
|
+
INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_INSTRUCTIONS_LOCAL"];
|
|
163
|
+
CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
98
164
|
});
|
|
99
165
|
|
|
100
|
-
//
|
|
166
|
+
// src/lib/app-home.ts
|
|
167
|
+
import { existsSync as existsSync2 } from "fs";
|
|
101
168
|
import { homedir } from "os";
|
|
102
|
-
import { join as join2 } from "path";
|
|
103
|
-
|
|
169
|
+
import { join as join2, resolve } from "path";
|
|
170
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
171
|
+
import { join as pathsResolverJoin } from "path";
|
|
172
|
+
function pathsResolverAssertApp(app) {
|
|
104
173
|
if (typeof app !== "string" || app.length === 0) {
|
|
105
174
|
throw new TypeError("paths: app must be a non-empty string");
|
|
106
175
|
}
|
|
107
|
-
if (!
|
|
176
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
108
177
|
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
109
178
|
}
|
|
110
179
|
}
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const value = envOf(options)[KIND_ENV[kind]];
|
|
116
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
117
|
-
}
|
|
118
|
-
function isMacOS(platform) {
|
|
119
|
-
return platform === "darwin";
|
|
180
|
+
function pathsResolverAssertKind(kind) {
|
|
181
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
182
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
183
|
+
}
|
|
120
184
|
}
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
185
|
+
function pathsResolverBaseDir(kind, options) {
|
|
186
|
+
pathsResolverAssertKind(kind);
|
|
187
|
+
const env = options.env ?? process.env;
|
|
188
|
+
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
189
|
+
if (typeof override === "string" && override.length > 0)
|
|
124
190
|
return override;
|
|
125
|
-
const home = options.home ??
|
|
191
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
126
192
|
const platform = options.platform ?? process.platform;
|
|
127
|
-
if (
|
|
193
|
+
if (platform === "darwin") {
|
|
128
194
|
switch (kind) {
|
|
129
195
|
case "config":
|
|
130
196
|
case "data":
|
|
131
|
-
return
|
|
197
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
132
198
|
case "cache":
|
|
133
|
-
return
|
|
199
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
134
200
|
case "state":
|
|
135
|
-
return
|
|
201
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
136
202
|
}
|
|
137
203
|
}
|
|
138
204
|
switch (kind) {
|
|
139
205
|
case "config":
|
|
140
|
-
return
|
|
206
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
141
207
|
case "data":
|
|
142
|
-
return
|
|
208
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
143
209
|
case "state":
|
|
144
|
-
return
|
|
210
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
145
211
|
case "cache":
|
|
146
|
-
return
|
|
212
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
147
213
|
}
|
|
148
214
|
}
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
const appSegment = options.internal === true ?
|
|
152
|
-
return
|
|
215
|
+
function pathsResolverResolve(kind, options) {
|
|
216
|
+
pathsResolverAssertApp(options.app);
|
|
217
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
218
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
153
219
|
}
|
|
154
220
|
function configDir(options) {
|
|
155
|
-
return
|
|
221
|
+
return pathsResolverResolve("config", options);
|
|
156
222
|
}
|
|
157
|
-
var KIND_ENV, APP_SLUG_RE;
|
|
158
|
-
var init_dist = __esm(() => {
|
|
159
|
-
KIND_ENV = {
|
|
160
|
-
config: "HASNA_CONFIG_HOME",
|
|
161
|
-
data: "HASNA_DATA_HOME",
|
|
162
|
-
state: "HASNA_STATE_HOME",
|
|
163
|
-
cache: "HASNA_CACHE_HOME"
|
|
164
|
-
};
|
|
165
|
-
APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// src/lib/app-home.ts
|
|
169
|
-
import { existsSync as existsSync2 } from "fs";
|
|
170
|
-
import { homedir as homedir2 } from "os";
|
|
171
|
-
import { join as join3, resolve } from "path";
|
|
172
223
|
function homeDir(env = process.env) {
|
|
173
|
-
return env["HOME"] || env["USERPROFILE"] ||
|
|
224
|
+
return env["HOME"] || env["USERPROFILE"] || homedir();
|
|
174
225
|
}
|
|
175
226
|
function legacyStoreHome(env = process.env) {
|
|
176
|
-
return resolve(
|
|
227
|
+
return resolve(join2(homeDir(env), ".hasna", "instructions"));
|
|
177
228
|
}
|
|
178
229
|
function resolverStoreHome(env = process.env) {
|
|
179
|
-
return configDir({ app: "configs", env, home: env.HOME || env.USERPROFILE ||
|
|
230
|
+
return configDir({ app: "configs", env, home: env.HOME || env.USERPROFILE || homedir() });
|
|
180
231
|
}
|
|
181
232
|
function adoptResolverStoreHome(resolved, env = process.env) {
|
|
182
233
|
const override = env.HASNA_CONFIG_HOME;
|
|
183
234
|
if (typeof override === "string" && override.trim().length > 0)
|
|
184
235
|
return true;
|
|
185
|
-
return existsSync2(
|
|
236
|
+
return existsSync2(join2(resolved, "instructions.db"));
|
|
186
237
|
}
|
|
187
238
|
function exactStoreHome(env = process.env) {
|
|
188
239
|
const v = env[HASNA_CONFIGS_HOME_ENV];
|
|
@@ -196,7 +247,7 @@ function getConfigsStoreHome(env = process.env) {
|
|
|
196
247
|
return adoptResolverStoreHome(resolved, env) ? resolve(resolved) : legacyStoreHome(env);
|
|
197
248
|
}
|
|
198
249
|
function getConfigsStoreDbPath(env = process.env) {
|
|
199
|
-
return
|
|
250
|
+
return join2(getConfigsStoreHome(env), "instructions.db");
|
|
200
251
|
}
|
|
201
252
|
function getReportedDbPath(env = process.env) {
|
|
202
253
|
const override = env["HASNA_INSTRUCTIONS_DB_PATH"];
|
|
@@ -204,9 +255,15 @@ function getReportedDbPath(env = process.env) {
|
|
|
204
255
|
return override;
|
|
205
256
|
return getConfigsStoreDbPath(env);
|
|
206
257
|
}
|
|
207
|
-
var HASNA_CONFIGS_HOME_ENV = "HASNA_CONFIGS_HOME";
|
|
258
|
+
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE, HASNA_CONFIGS_HOME_ENV = "HASNA_CONFIGS_HOME";
|
|
208
259
|
var init_app_home = __esm(() => {
|
|
209
|
-
|
|
260
|
+
PATHS_RESOLVER_KIND_ENV = {
|
|
261
|
+
config: "HASNA_CONFIG_HOME",
|
|
262
|
+
data: "HASNA_DATA_HOME",
|
|
263
|
+
state: "HASNA_STATE_HOME",
|
|
264
|
+
cache: "HASNA_CACHE_HOME"
|
|
265
|
+
};
|
|
266
|
+
PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
210
267
|
});
|
|
211
268
|
|
|
212
269
|
// src/lib/raw-store-root.ts
|
|
@@ -221,7 +278,7 @@ var init_raw_store_root = __esm(() => {
|
|
|
221
278
|
// src/db/database.ts
|
|
222
279
|
import { Database } from "bun:sqlite";
|
|
223
280
|
import { existsSync as existsSync3, mkdirSync, rmSync } from "fs";
|
|
224
|
-
import { join as
|
|
281
|
+
import { join as join3 } from "path";
|
|
225
282
|
import { randomUUID } from "crypto";
|
|
226
283
|
function getDbPath() {
|
|
227
284
|
if (process.env["HASNA_INSTRUCTIONS_DB_PATH"]) {
|
|
@@ -229,7 +286,7 @@ function getDbPath() {
|
|
|
229
286
|
}
|
|
230
287
|
const dir = getRawStoreRoot();
|
|
231
288
|
mkdirSync(dir, { recursive: true });
|
|
232
|
-
return
|
|
289
|
+
return join3(dir, "instructions.db");
|
|
233
290
|
}
|
|
234
291
|
function uuid() {
|
|
235
292
|
return randomUUID();
|
|
@@ -243,9 +300,8 @@ function slugify(name) {
|
|
|
243
300
|
function getDatabase(path) {
|
|
244
301
|
if (_db)
|
|
245
302
|
return _db;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
throw new Error("instructions is using the HTTP API transport (HASNA_INSTRUCTIONS_API_URL set): this command is not wired to the API yet. " + "Unset HASNA_INSTRUCTIONS_API_URL / HASNA_INSTRUCTIONS_API_KEY to use it against the local store.");
|
|
303
|
+
if (!path && hasInstructionsEnvAuthorityIntent(process.env)) {
|
|
304
|
+
throw new Error("instructions is using the hosted API transport (a HASNA_INSTRUCTIONS_* credential is configured): this command is not wired to the API yet. " + "Point this run at the local store (HASNA_INSTRUCTIONS_LOCAL=1 with no hosted credential) to use it against the local SQLite store.");
|
|
249
305
|
}
|
|
250
306
|
const dbPath = path || getDbPath();
|
|
251
307
|
const db = new Database(dbPath);
|
|
@@ -326,7 +382,7 @@ function insertFeedback(input, db) {
|
|
|
326
382
|
}
|
|
327
383
|
var MIGRATIONS, _db = null;
|
|
328
384
|
var init_database = __esm(() => {
|
|
329
|
-
|
|
385
|
+
init_local_opt_in();
|
|
330
386
|
init_raw_store_root();
|
|
331
387
|
MIGRATIONS = [
|
|
332
388
|
`
|
|
@@ -707,9 +763,9 @@ var init_template = __esm(() => {
|
|
|
707
763
|
});
|
|
708
764
|
|
|
709
765
|
// src/lib/machine.ts
|
|
710
|
-
import { arch as currentArch, homedir as
|
|
766
|
+
import { arch as currentArch, homedir as homedir2, hostname as currentHostname, type as currentOsType } from "os";
|
|
711
767
|
import { existsSync as existsSync4 } from "fs";
|
|
712
|
-
import { join as
|
|
768
|
+
import { join as join4 } from "path";
|
|
713
769
|
function normalizeOsFamily(os) {
|
|
714
770
|
const value = (os ?? "").trim().toLowerCase();
|
|
715
771
|
if (value === "darwin" || value === "macos" || value === "mac" || value === "osx")
|
|
@@ -721,11 +777,11 @@ function normalizeOsFamily(os) {
|
|
|
721
777
|
return value || "unknown";
|
|
722
778
|
}
|
|
723
779
|
function detectMachineContext(overrides = {}) {
|
|
724
|
-
const homeDir2 = overrides.home_dir ?? process.env["CONFIGS_HOME"] ?? process.env["HOME"] ??
|
|
780
|
+
const homeDir2 = overrides.home_dir ?? process.env["CONFIGS_HOME"] ?? process.env["HOME"] ?? homedir2();
|
|
725
781
|
const os = overrides.os ?? currentOsType();
|
|
726
782
|
const osFamily = normalizeOsFamily(os);
|
|
727
|
-
const bunBinDir = overrides.bun_bin_dir ??
|
|
728
|
-
const defaultBunPath = osFamily === "macos" && existsSync4(BREW_BUN_PATH) ? BREW_BUN_PATH :
|
|
783
|
+
const bunBinDir = overrides.bun_bin_dir ?? join4(homeDir2, ".bun", "bin");
|
|
784
|
+
const defaultBunPath = osFamily === "macos" && existsSync4(BREW_BUN_PATH) ? BREW_BUN_PATH : join4(bunBinDir, "bun");
|
|
729
785
|
return {
|
|
730
786
|
id: "current-machine",
|
|
731
787
|
hostname: overrides.hostname ?? currentHostname(),
|
|
@@ -735,10 +791,10 @@ function detectMachineContext(overrides = {}) {
|
|
|
735
791
|
created_at: "",
|
|
736
792
|
os_family: osFamily,
|
|
737
793
|
home_dir: homeDir2,
|
|
738
|
-
workspace_root: overrides.workspace_root ??
|
|
794
|
+
workspace_root: overrides.workspace_root ?? join4(homeDir2, osFamily === "macos" ? "Workspace" : "workspace"),
|
|
739
795
|
bun_bin_dir: bunBinDir,
|
|
740
796
|
bun_path: overrides.bun_path ?? defaultBunPath,
|
|
741
|
-
path_prefix: overrides.path_prefix ?? (osFamily === "macos" ? `${
|
|
797
|
+
path_prefix: overrides.path_prefix ?? (osFamily === "macos" ? `${join4("/opt", "homebrew", "bin")}:${bunBinDir}` : bunBinDir)
|
|
742
798
|
};
|
|
743
799
|
}
|
|
744
800
|
function machineContextToVariables(machine) {
|
|
@@ -5251,12 +5307,10 @@ var init_session_render_contract = __esm(() => {
|
|
|
5251
5307
|
});
|
|
5252
5308
|
|
|
5253
5309
|
// src/lib/session-render-state.ts
|
|
5254
|
-
var init_session_render_state =
|
|
5255
|
-
init_dist();
|
|
5256
|
-
});
|
|
5310
|
+
var init_session_render_state = () => {};
|
|
5257
5311
|
|
|
5258
5312
|
// src/lib/project-context.ts
|
|
5259
|
-
import { basename, dirname as dirname2, isAbsolute, join as
|
|
5313
|
+
import { basename, dirname as dirname2, isAbsolute, join as join5, parse, relative, resolve as resolve3 } from "path";
|
|
5260
5314
|
function revisionKey(value) {
|
|
5261
5315
|
const sequence = value.match(/^(?:rev-)?([0-9]+)$/);
|
|
5262
5316
|
if (sequence)
|
|
@@ -5819,13 +5873,13 @@ var init_asset_plan = __esm(() => {
|
|
|
5819
5873
|
|
|
5820
5874
|
// src/lib/cursor-authority.ts
|
|
5821
5875
|
import { createHash } from "crypto";
|
|
5822
|
-
import { homedir as
|
|
5823
|
-
import { join as
|
|
5876
|
+
import { homedir as homedir3 } from "os";
|
|
5877
|
+
import { join as join6, resolve as resolve4 } from "path";
|
|
5824
5878
|
function sha256(content) {
|
|
5825
5879
|
return createHash("sha256").update(content).digest("hex");
|
|
5826
5880
|
}
|
|
5827
5881
|
function homeDir2() {
|
|
5828
|
-
return process.env["HOME"] ||
|
|
5882
|
+
return process.env["HOME"] || homedir3();
|
|
5829
5883
|
}
|
|
5830
5884
|
function markerPayload(content, markerLine, markerIndex) {
|
|
5831
5885
|
const index = markerIndex ?? content.indexOf(markerLine);
|
|
@@ -5834,7 +5888,7 @@ function markerPayload(content, markerLine, markerIndex) {
|
|
|
5834
5888
|
return content.slice(0, index) + content.slice(index + markerLine.length).replace(/^\n/, "");
|
|
5835
5889
|
}
|
|
5836
5890
|
function isCursorGlobalAuthorityPath(path) {
|
|
5837
|
-
return resolve4(path) === resolve4(
|
|
5891
|
+
return resolve4(path) === resolve4(join6(homeDir2(), CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH));
|
|
5838
5892
|
}
|
|
5839
5893
|
function stampCursorGlobalAuthorityMarker(content) {
|
|
5840
5894
|
const existing = content.match(CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN);
|
|
@@ -6506,6 +6560,1222 @@ var init_machines = __esm(() => {
|
|
|
6506
6560
|
init_database();
|
|
6507
6561
|
});
|
|
6508
6562
|
|
|
6563
|
+
// ../contracts/dist/client/storage.js
|
|
6564
|
+
import { isIP } from "net";
|
|
6565
|
+
import { spawnSync } from "child_process";
|
|
6566
|
+
import { closeSync, fstatSync, openSync, readFileSync as readFileSync2 } from "fs";
|
|
6567
|
+
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
6568
|
+
import { createRequire as createRequire2 } from "module";
|
|
6569
|
+
import { hostname as osHostname } from "os";
|
|
6570
|
+
import { isAbsolute as isAbsolute2, join as join7 } from "path";
|
|
6571
|
+
function envToken2(name) {
|
|
6572
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
6573
|
+
}
|
|
6574
|
+
function clientTransportEnvKeys2(name) {
|
|
6575
|
+
const envSegment = envToken2(name);
|
|
6576
|
+
return {
|
|
6577
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
6578
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
6579
|
+
};
|
|
6580
|
+
}
|
|
6581
|
+
function credentialOverrideEnvKey2(name) {
|
|
6582
|
+
return `HASNA_${envToken2(name)}_API_KEY_OVERRIDE`;
|
|
6583
|
+
}
|
|
6584
|
+
function credentialPointerEnvKey2(name) {
|
|
6585
|
+
return `HASNA_${envToken2(name)}_API_KEY_REF`;
|
|
6586
|
+
}
|
|
6587
|
+
function homeDir3(env) {
|
|
6588
|
+
const home = env.HOME?.trim();
|
|
6589
|
+
return home ? home : null;
|
|
6590
|
+
}
|
|
6591
|
+
function absoluteOverride(env, key) {
|
|
6592
|
+
const value = env[key]?.trim();
|
|
6593
|
+
return value && isAbsolute2(value) ? value : null;
|
|
6594
|
+
}
|
|
6595
|
+
function hasnaHomeDir(env) {
|
|
6596
|
+
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
6597
|
+
if (override)
|
|
6598
|
+
return override;
|
|
6599
|
+
const home = homeDir3(env);
|
|
6600
|
+
return home ? join7(home, HASNA_HOME_DIR) : null;
|
|
6601
|
+
}
|
|
6602
|
+
function appConfigDir(name, env) {
|
|
6603
|
+
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
6604
|
+
if (configRoot)
|
|
6605
|
+
return join7(configRoot, name);
|
|
6606
|
+
const root = hasnaHomeDir(env);
|
|
6607
|
+
return root ? join7(root, name, CONFIG_SUBDIR) : null;
|
|
6608
|
+
}
|
|
6609
|
+
function credentialDiskSourceList(name, env, profile = null) {
|
|
6610
|
+
if (!SAFE_APP_SLUG.test(name))
|
|
6611
|
+
return [];
|
|
6612
|
+
const directory = appConfigDir(name, env);
|
|
6613
|
+
if (!directory)
|
|
6614
|
+
return [];
|
|
6615
|
+
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
6616
|
+
return [{ path: join7(directory, file), tier: "disk" }];
|
|
6617
|
+
}
|
|
6618
|
+
function credentialDiskSources(name, env) {
|
|
6619
|
+
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
6620
|
+
}
|
|
6621
|
+
function profileDiskSources(name, env, profile) {
|
|
6622
|
+
return credentialDiskSourceList(name, env, profile).map((s) => s.path);
|
|
6623
|
+
}
|
|
6624
|
+
function parseEnvFile(text) {
|
|
6625
|
+
const values = new Map;
|
|
6626
|
+
const unusable = new Set;
|
|
6627
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
6628
|
+
const line = rawLine.trim();
|
|
6629
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
6630
|
+
continue;
|
|
6631
|
+
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
6632
|
+
const equals = withoutExport.indexOf("=");
|
|
6633
|
+
if (equals <= 0)
|
|
6634
|
+
continue;
|
|
6635
|
+
const key = withoutExport.slice(0, equals).trim();
|
|
6636
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
6637
|
+
continue;
|
|
6638
|
+
let value = withoutExport.slice(equals + 1).trim();
|
|
6639
|
+
const quote = value[0];
|
|
6640
|
+
if (quote === '"' || quote === "'") {
|
|
6641
|
+
if (value.length < 2 || !value.endsWith(quote)) {
|
|
6642
|
+
unusable.add(key);
|
|
6643
|
+
continue;
|
|
6644
|
+
}
|
|
6645
|
+
value = value.slice(1, -1);
|
|
6646
|
+
}
|
|
6647
|
+
if (value.trim().length === 0) {
|
|
6648
|
+
unusable.add(key);
|
|
6649
|
+
continue;
|
|
6650
|
+
}
|
|
6651
|
+
if (values.has(key) && values.get(key) !== value)
|
|
6652
|
+
unusable.add(key);
|
|
6653
|
+
values.set(key, value);
|
|
6654
|
+
}
|
|
6655
|
+
return { values, unusable };
|
|
6656
|
+
}
|
|
6657
|
+
function configFileModeAllowed(mode) {
|
|
6658
|
+
const permissions = mode & 4095;
|
|
6659
|
+
return permissions === 256 || permissions === 384;
|
|
6660
|
+
}
|
|
6661
|
+
function configFileReadsCoherent(before, after) {
|
|
6662
|
+
return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
|
|
6663
|
+
}
|
|
6664
|
+
function readAppConfigFile(path) {
|
|
6665
|
+
const unsafe = (reason) => {
|
|
6666
|
+
throw new CredentialFileUnsafeError(path, reason);
|
|
6667
|
+
};
|
|
6668
|
+
let fd = -1;
|
|
6669
|
+
try {
|
|
6670
|
+
fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
6671
|
+
} catch (error) {
|
|
6672
|
+
const code = error.code;
|
|
6673
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
6674
|
+
return null;
|
|
6675
|
+
if (code === "ELOOP")
|
|
6676
|
+
unsafe("the path is a symlink");
|
|
6677
|
+
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
6678
|
+
}
|
|
6679
|
+
try {
|
|
6680
|
+
const before = fstatSync(fd);
|
|
6681
|
+
if (!before.isFile())
|
|
6682
|
+
unsafe("the path is not a regular file");
|
|
6683
|
+
if (!configFileModeAllowed(before.mode)) {
|
|
6684
|
+
unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
|
|
6685
|
+
}
|
|
6686
|
+
const uid = process.getuid?.() ?? process.geteuid?.();
|
|
6687
|
+
if (uid !== undefined && before.uid !== uid)
|
|
6688
|
+
unsafe("the file is not owned by the current user");
|
|
6689
|
+
if (before.size > MAX_CREDENTIAL_FILE_BYTES2)
|
|
6690
|
+
unsafe("the file exceeds the size limit");
|
|
6691
|
+
const bytes = readFileSync2(fd);
|
|
6692
|
+
const after = fstatSync(fd);
|
|
6693
|
+
if (!configFileReadsCoherent(before, after)) {
|
|
6694
|
+
unsafe("the file changed while being read");
|
|
6695
|
+
}
|
|
6696
|
+
return parseEnvFile(bytes.toString("utf8"));
|
|
6697
|
+
} finally {
|
|
6698
|
+
if (fd !== -1)
|
|
6699
|
+
closeSync(fd);
|
|
6700
|
+
}
|
|
6701
|
+
}
|
|
6702
|
+
function readCredentialFile(path, apiKeyKeys) {
|
|
6703
|
+
const parsed = readAppConfigFile(path);
|
|
6704
|
+
if (!parsed)
|
|
6705
|
+
return null;
|
|
6706
|
+
for (const key of apiKeyKeys) {
|
|
6707
|
+
if (parsed.unusable.has(key)) {
|
|
6708
|
+
throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
|
|
6709
|
+
}
|
|
6710
|
+
}
|
|
6711
|
+
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
6712
|
+
if (new Set(values).size > 1) {
|
|
6713
|
+
throw new CredentialFileUnsafeError(path, "credential aliases disagree");
|
|
6714
|
+
}
|
|
6715
|
+
return values[0] ?? null;
|
|
6716
|
+
}
|
|
6717
|
+
function appConfigDiskValue(name, env, keys) {
|
|
6718
|
+
const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
|
|
6719
|
+
if (wanted.length === 0)
|
|
6720
|
+
return null;
|
|
6721
|
+
for (const path of credentialDiskSources(name, env)) {
|
|
6722
|
+
const parsed = readAppConfigFile(path);
|
|
6723
|
+
if (!parsed)
|
|
6724
|
+
continue;
|
|
6725
|
+
if (wanted.some((key) => parsed.unusable.has(key))) {
|
|
6726
|
+
return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
|
|
6727
|
+
}
|
|
6728
|
+
const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
6729
|
+
if (new Set(values).size > 1)
|
|
6730
|
+
throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
|
|
6731
|
+
for (const key of wanted) {
|
|
6732
|
+
if (parsed.unusable.has(key))
|
|
6733
|
+
return { key, value: "", path, unusable: true };
|
|
6734
|
+
const value = parsed.values.get(key)?.trim();
|
|
6735
|
+
if (value)
|
|
6736
|
+
return { key, value, path };
|
|
6737
|
+
}
|
|
6738
|
+
}
|
|
6739
|
+
return null;
|
|
6740
|
+
}
|
|
6741
|
+
function assertUsableCredential(appName, source, value) {
|
|
6742
|
+
if (VAULT_POINTER_SHAPE.test(value)) {
|
|
6743
|
+
throw new CredentialResolutionError(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey2(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
|
|
6744
|
+
}
|
|
6745
|
+
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
6746
|
+
return;
|
|
6747
|
+
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]);
|
|
6748
|
+
}
|
|
6749
|
+
function sealCredential(fields) {
|
|
6750
|
+
const { apiKey } = fields;
|
|
6751
|
+
const visible = {
|
|
6752
|
+
tier: fields.tier,
|
|
6753
|
+
source: fields.source,
|
|
6754
|
+
deliberate: fields.deliberate,
|
|
6755
|
+
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
6756
|
+
warning: fields.warning
|
|
6757
|
+
};
|
|
6758
|
+
const sealed = { ...visible };
|
|
6759
|
+
Object.defineProperty(sealed, "apiKey", {
|
|
6760
|
+
value: apiKey,
|
|
6761
|
+
enumerable: false,
|
|
6762
|
+
writable: false,
|
|
6763
|
+
configurable: false
|
|
6764
|
+
});
|
|
6765
|
+
if (fields.pointerVaultKey !== undefined) {
|
|
6766
|
+
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
6767
|
+
value: fields.pointerVaultKey,
|
|
6768
|
+
enumerable: false,
|
|
6769
|
+
writable: false,
|
|
6770
|
+
configurable: false
|
|
6771
|
+
});
|
|
6772
|
+
}
|
|
6773
|
+
Object.defineProperty(sealed, INSPECT_CUSTOM2, {
|
|
6774
|
+
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
6775
|
+
enumerable: false,
|
|
6776
|
+
writable: false,
|
|
6777
|
+
configurable: false
|
|
6778
|
+
});
|
|
6779
|
+
Object.defineProperty(sealed, CREDENTIAL_SEAL2, {
|
|
6780
|
+
value: true,
|
|
6781
|
+
enumerable: false,
|
|
6782
|
+
writable: false,
|
|
6783
|
+
configurable: false
|
|
6784
|
+
});
|
|
6785
|
+
return Object.freeze(sealed);
|
|
6786
|
+
}
|
|
6787
|
+
function isSealedCredential(credential) {
|
|
6788
|
+
return credential[CREDENTIAL_SEAL2] === true;
|
|
6789
|
+
}
|
|
6790
|
+
function explicitCredential(appName, apiKey) {
|
|
6791
|
+
const source = "explicit apiKey option";
|
|
6792
|
+
assertUsableCredential(appName, source, apiKey);
|
|
6793
|
+
return sealCredential({
|
|
6794
|
+
apiKey,
|
|
6795
|
+
tier: "argument",
|
|
6796
|
+
source,
|
|
6797
|
+
deliberate: true,
|
|
6798
|
+
diskCandidates: [],
|
|
6799
|
+
warning: null
|
|
6800
|
+
});
|
|
6801
|
+
}
|
|
6802
|
+
function validateAndSealResolvedCredential(appName, credential) {
|
|
6803
|
+
const apiKey = credential.apiKey;
|
|
6804
|
+
assertUsableCredential(appName, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE, apiKey);
|
|
6805
|
+
if (!isSealedCredential(credential)) {
|
|
6806
|
+
return sealCredential({
|
|
6807
|
+
apiKey,
|
|
6808
|
+
tier: "argument",
|
|
6809
|
+
source: CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE,
|
|
6810
|
+
deliberate: true,
|
|
6811
|
+
diskCandidates: [],
|
|
6812
|
+
warning: null
|
|
6813
|
+
});
|
|
6814
|
+
}
|
|
6815
|
+
return sealCredential({
|
|
6816
|
+
apiKey,
|
|
6817
|
+
tier: credential.tier,
|
|
6818
|
+
source: credential.source,
|
|
6819
|
+
deliberate: credential.deliberate,
|
|
6820
|
+
diskCandidates: credential.diskCandidates,
|
|
6821
|
+
warning: credential.warning,
|
|
6822
|
+
...credential.pointerVaultKey !== undefined ? { pointerVaultKey: credential.pointerVaultKey } : {}
|
|
6823
|
+
});
|
|
6824
|
+
}
|
|
6825
|
+
function firstEnvValue(env, keys) {
|
|
6826
|
+
for (const key of keys) {
|
|
6827
|
+
if (!Object.prototype.hasOwnProperty.call(env, key))
|
|
6828
|
+
continue;
|
|
6829
|
+
const value = env[key]?.trim();
|
|
6830
|
+
if (value)
|
|
6831
|
+
return { key, value };
|
|
6832
|
+
}
|
|
6833
|
+
return null;
|
|
6834
|
+
}
|
|
6835
|
+
function isAmbientEnvironment(env) {
|
|
6836
|
+
return env === process.env || env[AMBIENT_ENVIRONMENT2] === true;
|
|
6837
|
+
}
|
|
6838
|
+
function defaultKeychainRunner(argv) {
|
|
6839
|
+
const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
|
|
6840
|
+
encoding: "utf8",
|
|
6841
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
6842
|
+
timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
|
|
6843
|
+
});
|
|
6844
|
+
return {
|
|
6845
|
+
status: result.status,
|
|
6846
|
+
stdout: result.stdout ?? "",
|
|
6847
|
+
stderr: result.error ? result.error.message : result.stderr ?? ""
|
|
6848
|
+
};
|
|
6849
|
+
}
|
|
6850
|
+
function keychainTierEnabled(env, options) {
|
|
6851
|
+
if ((options.platform ?? process.platform) !== "darwin")
|
|
6852
|
+
return false;
|
|
6853
|
+
if (options.enabled !== undefined)
|
|
6854
|
+
return options.enabled;
|
|
6855
|
+
return options.run !== undefined || isAmbientEnvironment(env);
|
|
6856
|
+
}
|
|
6857
|
+
function keychainAccount(env, options) {
|
|
6858
|
+
const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
|
|
6859
|
+
if (station)
|
|
6860
|
+
return station;
|
|
6861
|
+
const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
|
|
6862
|
+
if (host)
|
|
6863
|
+
return host;
|
|
6864
|
+
const user = env.USER?.trim();
|
|
6865
|
+
return user || null;
|
|
6866
|
+
}
|
|
6867
|
+
function keychainFailureHint(text) {
|
|
6868
|
+
const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
|
|
6869
|
+
const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
|
|
6870
|
+
return clean ? `: ${clean}` : "";
|
|
6871
|
+
}
|
|
6872
|
+
function readKeychainItem(name, env, kind, options) {
|
|
6873
|
+
if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
|
|
6874
|
+
return null;
|
|
6875
|
+
const account = keychainAccount(env, options);
|
|
6876
|
+
if (!account)
|
|
6877
|
+
return null;
|
|
6878
|
+
const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
|
|
6879
|
+
const source = `keychain:${service}@${account}`;
|
|
6880
|
+
const run = options.run ?? defaultKeychainRunner;
|
|
6881
|
+
let result;
|
|
6882
|
+
try {
|
|
6883
|
+
result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
|
|
6884
|
+
} catch (error) {
|
|
6885
|
+
const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
|
|
6886
|
+
throw new CredentialResolutionError(name, `The Keychain lookup for ${source} could not run${reason}. A Keychain failure is never resolved ` + `around: fix the keychain, or delete the item to fall through to the credential on disk.`, [source]);
|
|
6887
|
+
}
|
|
6888
|
+
if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
|
|
6889
|
+
return null;
|
|
6890
|
+
if (result.status !== 0) {
|
|
6891
|
+
throw new CredentialResolutionError(name, `The Keychain lookup for ${source} failed (security exited ` + `${result.status ?? "without a status"}${keychainFailureHint(result.stderr)}). A Keychain item that ` + `exists but cannot be read is never resolved around: unlock the keychain, run from a session that ` + `may use it, or delete the item to fall through to the credential on disk.`, [source]);
|
|
6892
|
+
}
|
|
6893
|
+
const value = result.stdout.trim();
|
|
6894
|
+
if (!value) {
|
|
6895
|
+
throw new CredentialResolutionError(name, `${source} exists but holds an empty value; a declared item never falls through to another ` + `identity. Store a value in it or delete the item.`, [source]);
|
|
6896
|
+
}
|
|
6897
|
+
return { value, source };
|
|
6898
|
+
}
|
|
6899
|
+
function keychainConfigValue(name, env, options = {}) {
|
|
6900
|
+
return readKeychainItem(name, env, "api-url", options);
|
|
6901
|
+
}
|
|
6902
|
+
function snapshotClientEnvironment(name, env) {
|
|
6903
|
+
const keys = clientTransportEnvKeys2(name);
|
|
6904
|
+
const ambient = isAmbientEnvironment(env);
|
|
6905
|
+
const snapshot = Object.create(null);
|
|
6906
|
+
for (const key of [
|
|
6907
|
+
...keys.apiUrlKeys,
|
|
6908
|
+
...keys.apiKeyKeys,
|
|
6909
|
+
credentialOverrideEnvKey2(name),
|
|
6910
|
+
credentialPointerEnvKey2(name),
|
|
6911
|
+
CREDENTIAL_PROFILE_ENV_KEY2,
|
|
6912
|
+
"HOME",
|
|
6913
|
+
HASNA_HOME_ENV_KEY,
|
|
6914
|
+
HASNA_CONFIG_HOME_ENV_KEY,
|
|
6915
|
+
KEYCHAIN_STATION_ENV_KEY,
|
|
6916
|
+
"USER"
|
|
6917
|
+
]) {
|
|
6918
|
+
const descriptor = Object.getOwnPropertyDescriptor(env, key);
|
|
6919
|
+
if (!descriptor)
|
|
6920
|
+
continue;
|
|
6921
|
+
if (!("value" in descriptor)) {
|
|
6922
|
+
throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
|
|
6923
|
+
}
|
|
6924
|
+
if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
|
|
6925
|
+
throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
|
|
6926
|
+
}
|
|
6927
|
+
snapshot[key] = descriptor.value;
|
|
6928
|
+
}
|
|
6929
|
+
if (ambient) {
|
|
6930
|
+
Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT2, {
|
|
6931
|
+
value: true,
|
|
6932
|
+
enumerable: false,
|
|
6933
|
+
writable: false,
|
|
6934
|
+
configurable: false
|
|
6935
|
+
});
|
|
6936
|
+
}
|
|
6937
|
+
return Object.freeze(snapshot);
|
|
6938
|
+
}
|
|
6939
|
+
function resolveCredential(name, env, options = {}) {
|
|
6940
|
+
env = snapshotClientEnvironment(name, env);
|
|
6941
|
+
const { apiKeyKeys } = clientTransportEnvKeys2(name);
|
|
6942
|
+
const diskPaths = credentialDiskSources(name, env);
|
|
6943
|
+
if (options.apiKey !== undefined) {
|
|
6944
|
+
const explicitKey = options.apiKey.trim();
|
|
6945
|
+
if (!explicitKey) {
|
|
6946
|
+
throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
|
|
6947
|
+
}
|
|
6948
|
+
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
6949
|
+
return sealCredential({
|
|
6950
|
+
apiKey: explicitKey,
|
|
6951
|
+
tier: "argument",
|
|
6952
|
+
source: "explicit apiKey argument",
|
|
6953
|
+
deliberate: true,
|
|
6954
|
+
diskCandidates: diskPaths,
|
|
6955
|
+
warning: null
|
|
6956
|
+
});
|
|
6957
|
+
}
|
|
6958
|
+
const overrideKeyName = credentialOverrideEnvKey2(name);
|
|
6959
|
+
const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
|
|
6960
|
+
if (overrideRaw !== undefined) {
|
|
6961
|
+
const override = overrideRaw.trim();
|
|
6962
|
+
if (!override) {
|
|
6963
|
+
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]);
|
|
6964
|
+
}
|
|
6965
|
+
assertUsableCredential(name, overrideKeyName, override);
|
|
6966
|
+
return sealCredential({
|
|
6967
|
+
apiKey: override,
|
|
6968
|
+
tier: "override",
|
|
6969
|
+
source: overrideKeyName,
|
|
6970
|
+
deliberate: true,
|
|
6971
|
+
diskCandidates: diskPaths,
|
|
6972
|
+
warning: null
|
|
6973
|
+
});
|
|
6974
|
+
}
|
|
6975
|
+
const pointerKeyName = credentialPointerEnvKey2(name);
|
|
6976
|
+
const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
|
|
6977
|
+
if (pointerRaw !== undefined) {
|
|
6978
|
+
const pointer = pointerRaw.trim();
|
|
6979
|
+
if (!pointer) {
|
|
6980
|
+
throw new CredentialResolutionError(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
|
|
6981
|
+
}
|
|
6982
|
+
if (!VAULT_POINTER_SHAPE.test(pointer)) {
|
|
6983
|
+
throw new CredentialResolutionError(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
|
|
6984
|
+
}
|
|
6985
|
+
return sealCredential({
|
|
6986
|
+
apiKey: "",
|
|
6987
|
+
pointerVaultKey: pointer,
|
|
6988
|
+
tier: "pointer",
|
|
6989
|
+
source: pointerKeyName,
|
|
6990
|
+
deliberate: true,
|
|
6991
|
+
diskCandidates: diskPaths,
|
|
6992
|
+
warning: null
|
|
6993
|
+
});
|
|
6994
|
+
}
|
|
6995
|
+
if (options.profile !== undefined && !options.profile.trim()) {
|
|
6996
|
+
throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
|
|
6997
|
+
}
|
|
6998
|
+
const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY2) ? env[CREDENTIAL_PROFILE_ENV_KEY2] : undefined;
|
|
6999
|
+
if (profileRaw !== undefined && !profileRaw.trim()) {
|
|
7000
|
+
throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY2} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY2]);
|
|
7001
|
+
}
|
|
7002
|
+
const profile = options.profile?.trim() || profileRaw?.trim();
|
|
7003
|
+
if (profile) {
|
|
7004
|
+
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY2;
|
|
7005
|
+
if (!SAFE_PROFILE.test(profile)) {
|
|
7006
|
+
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
7007
|
+
}
|
|
7008
|
+
const paths = profileDiskSources(name, env, profile);
|
|
7009
|
+
for (const path of paths) {
|
|
7010
|
+
const value = readCredentialFile(path, apiKeyKeys);
|
|
7011
|
+
if (value) {
|
|
7012
|
+
assertUsableCredential(name, path, value);
|
|
7013
|
+
return sealCredential({
|
|
7014
|
+
apiKey: value,
|
|
7015
|
+
tier: "profile",
|
|
7016
|
+
source: path,
|
|
7017
|
+
deliberate: true,
|
|
7018
|
+
diskCandidates: paths,
|
|
7019
|
+
warning: null
|
|
7020
|
+
});
|
|
7021
|
+
}
|
|
7022
|
+
}
|
|
7023
|
+
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_KEY2}.`, paths);
|
|
7024
|
+
}
|
|
7025
|
+
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
7026
|
+
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
7027
|
+
if (blankEnv) {
|
|
7028
|
+
throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
|
|
7029
|
+
}
|
|
7030
|
+
if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
|
|
7031
|
+
throw new CredentialResolutionError(name, `${definedEnvEntries.map((entry) => entry.key).join(" and ")} disagree; credential aliases must be identical or only one may be set.`, definedEnvEntries.map((entry) => entry.key));
|
|
7032
|
+
}
|
|
7033
|
+
const envHit = firstEnvValue(env, apiKeyKeys);
|
|
7034
|
+
const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
|
|
7035
|
+
if (keychainHit) {
|
|
7036
|
+
assertUsableCredential(name, keychainHit.source, keychainHit.value);
|
|
7037
|
+
const warning = envHit && envHit.value !== keychainHit.value ? `Credential sources disagree for '${name}': ${keychainHit.source} and ${envHit.key} hold ` + `different keys. ${keychainHit.source} wins, because the Keychain 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;
|
|
7038
|
+
return sealCredential({
|
|
7039
|
+
apiKey: keychainHit.value,
|
|
7040
|
+
tier: "keychain",
|
|
7041
|
+
source: keychainHit.source,
|
|
7042
|
+
deliberate: false,
|
|
7043
|
+
diskCandidates: diskPaths,
|
|
7044
|
+
warning
|
|
7045
|
+
});
|
|
7046
|
+
}
|
|
7047
|
+
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
7048
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
7049
|
+
if (diskHits.length > 0) {
|
|
7050
|
+
const winner = diskHits[0];
|
|
7051
|
+
assertUsableCredential(name, winner.src.path, winner.value);
|
|
7052
|
+
const divergentSources = [
|
|
7053
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
7054
|
+
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
7055
|
+
];
|
|
7056
|
+
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.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;
|
|
7057
|
+
return sealCredential({
|
|
7058
|
+
apiKey: winner.value,
|
|
7059
|
+
tier: winner.src.tier,
|
|
7060
|
+
source: winner.src.path,
|
|
7061
|
+
deliberate: false,
|
|
7062
|
+
diskCandidates: diskPaths,
|
|
7063
|
+
warning
|
|
7064
|
+
});
|
|
7065
|
+
}
|
|
7066
|
+
if (envHit) {
|
|
7067
|
+
assertUsableCredential(name, envHit.key, envHit.value);
|
|
7068
|
+
return sealCredential({
|
|
7069
|
+
apiKey: envHit.value,
|
|
7070
|
+
tier: "env",
|
|
7071
|
+
source: envHit.key,
|
|
7072
|
+
deliberate: false,
|
|
7073
|
+
diskCandidates: diskPaths,
|
|
7074
|
+
warning: null
|
|
7075
|
+
});
|
|
7076
|
+
}
|
|
7077
|
+
return null;
|
|
7078
|
+
}
|
|
7079
|
+
async function completePointerCredential(name, pointerResolution, env = process.env) {
|
|
7080
|
+
const vaultKey = pointerResolution.pointerVaultKey;
|
|
7081
|
+
const pointerEnvKey = pointerResolution.source;
|
|
7082
|
+
if (!vaultKey) {
|
|
7083
|
+
throw new CredentialResolutionError(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
|
|
7084
|
+
}
|
|
7085
|
+
let secretsSdk;
|
|
7086
|
+
try {
|
|
7087
|
+
secretsSdk = requireSecretsSdk2(SECRETS_PACKAGE_SPECIFIER2);
|
|
7088
|
+
} catch {
|
|
7089
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets SDK (@hasna/secrets) is not installed ` + `in this process. A vault pointer is TERMINAL: install @hasna/secrets to resolve it, or unset ${pointerEnvKey}.`, [pointerEnvKey]);
|
|
7090
|
+
}
|
|
7091
|
+
let client;
|
|
7092
|
+
try {
|
|
7093
|
+
client = secretsSdk.createSecretsClientFromEnv(env);
|
|
7094
|
+
} catch {
|
|
7095
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets client could not be configured from this ` + `environment (the secrets service URL and key env are missing or invalid). A vault pointer is TERMINAL and ` + `never falls through to a literal or disk credential.`, [pointerEnvKey]);
|
|
7096
|
+
}
|
|
7097
|
+
let secret;
|
|
7098
|
+
try {
|
|
7099
|
+
secret = await client.getSecret({ key: vaultKey });
|
|
7100
|
+
} catch {
|
|
7101
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the vault could not be reached or the item is ` + `unavailable. A vault pointer is TERMINAL and never falls through to a literal or disk credential.`, [pointerEnvKey]);
|
|
7102
|
+
}
|
|
7103
|
+
const value = secret.value;
|
|
7104
|
+
if (!value) {
|
|
7105
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
|
|
7106
|
+
}
|
|
7107
|
+
assertUsableCredential(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
|
|
7108
|
+
return sealCredential({
|
|
7109
|
+
apiKey: value,
|
|
7110
|
+
tier: "pointer",
|
|
7111
|
+
source: `${pointerEnvKey} -> vault:${vaultKey}`,
|
|
7112
|
+
deliberate: true,
|
|
7113
|
+
diskCandidates: pointerResolution.diskCandidates,
|
|
7114
|
+
warning: null
|
|
7115
|
+
});
|
|
7116
|
+
}
|
|
7117
|
+
function defaultFleetGatewayBaseUrl(name) {
|
|
7118
|
+
return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
|
|
7119
|
+
}
|
|
7120
|
+
function isValidDnsDomain(value) {
|
|
7121
|
+
if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
|
|
7122
|
+
return false;
|
|
7123
|
+
}
|
|
7124
|
+
return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
|
|
7125
|
+
}
|
|
7126
|
+
function validateAppSlug(name) {
|
|
7127
|
+
if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
|
|
7128
|
+
throw new Error("App name must be one lowercase DNS label.");
|
|
7129
|
+
}
|
|
7130
|
+
return name;
|
|
7131
|
+
}
|
|
7132
|
+
function rawAuthority(value) {
|
|
7133
|
+
const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
|
|
7134
|
+
if (!match)
|
|
7135
|
+
throw new Error("API URL must be absolute.");
|
|
7136
|
+
const afterScheme = value.slice(match[0].length);
|
|
7137
|
+
const boundary = afterScheme.search(/[/?#]/);
|
|
7138
|
+
const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
|
|
7139
|
+
if (!authority)
|
|
7140
|
+
throw new Error("API URL must include a hostname.");
|
|
7141
|
+
return authority;
|
|
7142
|
+
}
|
|
7143
|
+
function assertCanonicalPort(port) {
|
|
7144
|
+
if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
|
|
7145
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
7146
|
+
}
|
|
7147
|
+
const numericPort = Number(port);
|
|
7148
|
+
if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
|
|
7149
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
7150
|
+
}
|
|
7151
|
+
}
|
|
7152
|
+
function canonicalAuthorityHostname(authority) {
|
|
7153
|
+
let rawHostname;
|
|
7154
|
+
if (authority.startsWith("[")) {
|
|
7155
|
+
const closingBracket = authority.indexOf("]");
|
|
7156
|
+
if (closingBracket === -1) {
|
|
7157
|
+
throw new Error("API URL authority must contain a canonical hostname.");
|
|
7158
|
+
}
|
|
7159
|
+
rawHostname = authority.slice(0, closingBracket + 1);
|
|
7160
|
+
const portSuffix = authority.slice(closingBracket + 1);
|
|
7161
|
+
if (portSuffix) {
|
|
7162
|
+
if (!portSuffix.startsWith(":")) {
|
|
7163
|
+
throw new Error("API URL authority must contain a canonical hostname and port.");
|
|
7164
|
+
}
|
|
7165
|
+
assertCanonicalPort(portSuffix.slice(1));
|
|
7166
|
+
}
|
|
7167
|
+
if (isIP(rawHostname.slice(1, -1)) !== 6) {
|
|
7168
|
+
throw new Error("API URL authority must contain a canonical IPv6 literal.");
|
|
7169
|
+
}
|
|
7170
|
+
} else {
|
|
7171
|
+
const firstColon = authority.indexOf(":");
|
|
7172
|
+
const lastColon = authority.lastIndexOf(":");
|
|
7173
|
+
if (firstColon !== lastColon) {
|
|
7174
|
+
throw new Error("IPv6 API URL authorities must use brackets.");
|
|
7175
|
+
}
|
|
7176
|
+
if (lastColon !== -1) {
|
|
7177
|
+
const port = authority.slice(lastColon + 1);
|
|
7178
|
+
assertCanonicalPort(port);
|
|
7179
|
+
rawHostname = authority.slice(0, lastColon);
|
|
7180
|
+
} else {
|
|
7181
|
+
rawHostname = authority;
|
|
7182
|
+
}
|
|
7183
|
+
const ipVersion = isIP(rawHostname);
|
|
7184
|
+
const numericAddressParts = rawHostname.split(".");
|
|
7185
|
+
const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
|
|
7186
|
+
if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
|
|
7187
|
+
throw new Error("API URL authority must contain a canonical ASCII hostname.");
|
|
7188
|
+
}
|
|
7189
|
+
}
|
|
7190
|
+
return rawHostname.toLowerCase();
|
|
7191
|
+
}
|
|
7192
|
+
function isDeliberateLoopbackHttpAuthority(authority) {
|
|
7193
|
+
return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
|
|
7194
|
+
}
|
|
7195
|
+
function toV1BaseUrl(apiUrl) {
|
|
7196
|
+
if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
|
|
7197
|
+
throw new Error("API URL must not contain ASCII control characters.");
|
|
7198
|
+
}
|
|
7199
|
+
const input = apiUrl.trim();
|
|
7200
|
+
const authority = rawAuthority(input);
|
|
7201
|
+
if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
|
|
7202
|
+
throw new Error("API URL authority must be canonical ASCII without credentials.");
|
|
7203
|
+
}
|
|
7204
|
+
const canonicalHostname = canonicalAuthorityHostname(authority);
|
|
7205
|
+
const url = new URL(input);
|
|
7206
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
7207
|
+
throw new Error("API URL must use http or https.");
|
|
7208
|
+
}
|
|
7209
|
+
if (url.username || url.password) {
|
|
7210
|
+
throw new Error("API URL must not include credentials.");
|
|
7211
|
+
}
|
|
7212
|
+
if (!url.hostname || url.hostname.endsWith(".")) {
|
|
7213
|
+
throw new Error("API URL must include a canonical hostname.");
|
|
7214
|
+
}
|
|
7215
|
+
if (url.hostname.toLowerCase() !== canonicalHostname) {
|
|
7216
|
+
throw new Error("API URL authority must not rely on parser hostname normalization.");
|
|
7217
|
+
}
|
|
7218
|
+
if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
|
|
7219
|
+
throw new Error("API URL must not use IDN or punycode hostnames.");
|
|
7220
|
+
}
|
|
7221
|
+
if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
|
|
7222
|
+
throw new Error("API URL may use http only for an exact loopback authority.");
|
|
7223
|
+
}
|
|
7224
|
+
if (url.search || url.hash) {
|
|
7225
|
+
throw new Error("API URL must not include a query string or fragment.");
|
|
7226
|
+
}
|
|
7227
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
7228
|
+
if (path.endsWith("/v1"))
|
|
7229
|
+
path = path.slice(0, -"/v1".length);
|
|
7230
|
+
url.pathname = `${path}/v1`;
|
|
7231
|
+
return url.toString().replace(/\/+$/, "");
|
|
7232
|
+
}
|
|
7233
|
+
function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
|
|
7234
|
+
env = snapshotClientEnvironment(name, env);
|
|
7235
|
+
const keys = clientTransportEnvKeys2(name);
|
|
7236
|
+
const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
|
|
7237
|
+
const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
|
|
7238
|
+
if (blankUrl) {
|
|
7239
|
+
throw new ClientTransportConfigurationError(name, `${blankUrl.key} is set but blank; public clients require an explicit HTTPS API URL and never select local storage.`, [blankUrl.key]);
|
|
7240
|
+
}
|
|
7241
|
+
const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
|
|
7242
|
+
if (controlledUrl) {
|
|
7243
|
+
throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
|
|
7244
|
+
}
|
|
7245
|
+
const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
|
|
7246
|
+
if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
|
|
7247
|
+
throw new ClientTransportConfigurationError(name, `${usableUrlEntries.map((entry) => entry.key).join(" and ")} disagree; client authority aliases must be identical or only one may be set.`, usableUrlEntries.map((entry) => entry.key));
|
|
7248
|
+
}
|
|
7249
|
+
const envUrlHit = usableUrlEntries[0] ?? null;
|
|
7250
|
+
const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
|
|
7251
|
+
const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
7252
|
+
if (diskConfigUrlHit?.unusable) {
|
|
7253
|
+
throw new ClientTransportConfigurationError(name, `${diskConfigUrlHit.key} in ${diskConfigUrlHit.path} is declared but blank or malformed; public clients require a valid HTTPS service authority.`, [diskConfigUrlHit.path]);
|
|
7254
|
+
}
|
|
7255
|
+
const urlCandidates = [
|
|
7256
|
+
...envUrlHit ? [envUrlHit] : [],
|
|
7257
|
+
...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
|
|
7258
|
+
...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
|
|
7259
|
+
];
|
|
7260
|
+
const configuredUrl = urlCandidates[0] ?? null;
|
|
7261
|
+
const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
|
|
7262
|
+
if (configuredUrl && divergentUrls.length > 0) {
|
|
7263
|
+
throw new ClientTransportConfigurationError(name, `${configuredUrl.key} and ${divergentUrls.map((candidate) => candidate.key).join(" and ")} select different service authorities; refusing to send a credential written for one authority to the other.`, urlCandidates.map((candidate) => candidate.key));
|
|
7264
|
+
}
|
|
7265
|
+
const warnings = [];
|
|
7266
|
+
if (configuredUrl && !envUrlHit) {
|
|
7267
|
+
warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${configuredUrl.key} was used, so this client connects to the server. ` + `Keep that entry aligned with the intended service authority.`);
|
|
7268
|
+
}
|
|
7269
|
+
const credential = resolveCredential(name, env, options.credentials);
|
|
7270
|
+
if (!credential) {
|
|
7271
|
+
const diskHint = credentialDiskSourcesForMessage(name, env);
|
|
7272
|
+
const lead = configuredUrl ? `${configuredUrl.key} selects the HTTP server for '${name}', but no API key could be resolved` : `${keys.apiUrlKeys[0]} is not set and no API key could be resolved for '${name}'; a credential is required before the default fleet gateway authority applies`;
|
|
7273
|
+
warnings.push(`${lead}; refusing to create an unauthenticated client \u2014 public clients never fall back to SQLite or another local store. ` + `Looked in the Keychain (macOS only), then for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
|
|
7274
|
+
throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
|
|
7275
|
+
}
|
|
7276
|
+
if (credential.warning)
|
|
7277
|
+
warnings.push(credential.warning);
|
|
7278
|
+
let urlHit;
|
|
7279
|
+
if (configuredUrl) {
|
|
7280
|
+
urlHit = configuredUrl;
|
|
7281
|
+
} else {
|
|
7282
|
+
try {
|
|
7283
|
+
urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
|
|
7284
|
+
} catch (error) {
|
|
7285
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7286
|
+
throw new ClientTransportConfigurationError(name, `No ${keys.apiUrlKeys[0]} is configured and the default fleet gateway authority cannot be composed for '${name}': ${message}`, [keys.apiUrlKeys[0]]);
|
|
7287
|
+
}
|
|
7288
|
+
}
|
|
7289
|
+
const apiUrlSource = urlHit.key;
|
|
7290
|
+
let baseUrl;
|
|
7291
|
+
try {
|
|
7292
|
+
baseUrl = toV1BaseUrl(urlHit.value);
|
|
7293
|
+
} catch (error) {
|
|
7294
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7295
|
+
throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
|
|
7296
|
+
}
|
|
7297
|
+
return {
|
|
7298
|
+
resolution: {
|
|
7299
|
+
transport: "http",
|
|
7300
|
+
transportSource: urlHit.key,
|
|
7301
|
+
baseUrl,
|
|
7302
|
+
apiUrlSource,
|
|
7303
|
+
apiKeyPresent: true,
|
|
7304
|
+
apiKeySource: credential.source,
|
|
7305
|
+
apiKeyTier: credential.tier,
|
|
7306
|
+
misconfigured: false,
|
|
7307
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
7308
|
+
},
|
|
7309
|
+
credential
|
|
7310
|
+
};
|
|
7311
|
+
}
|
|
7312
|
+
function credentialDiskSourcesForMessage(name, env) {
|
|
7313
|
+
const paths = credentialDiskSources(name, env);
|
|
7314
|
+
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
7315
|
+
}
|
|
7316
|
+
function currentCredential(name, apiKey) {
|
|
7317
|
+
if (typeof apiKey === "function") {
|
|
7318
|
+
return validateAndSealResolvedCredential(name, apiKey());
|
|
7319
|
+
}
|
|
7320
|
+
return explicitCredential(name, apiKey);
|
|
7321
|
+
}
|
|
7322
|
+
async function resolveRequestCredential(name, apiKey, env = process.env) {
|
|
7323
|
+
const resolved = currentCredential(name, apiKey);
|
|
7324
|
+
if (resolved.tier === "pointer") {
|
|
7325
|
+
return completePointerCredential(name, resolved, env);
|
|
7326
|
+
}
|
|
7327
|
+
return resolved;
|
|
7328
|
+
}
|
|
7329
|
+
function authFailureGuidance(credential) {
|
|
7330
|
+
const origin = `The API key for this request came from ${credential.source}`;
|
|
7331
|
+
if (credential.deliberate) {
|
|
7332
|
+
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.`;
|
|
7333
|
+
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}`;
|
|
7334
|
+
}
|
|
7335
|
+
if (credential.tier === "env") {
|
|
7336
|
+
const target = credential.diskCandidates[0];
|
|
7337
|
+
const remedy = target ? `Store the CURRENT key in the Keychain or write it to ${target} \u2014 both are re-read on every call, so ` + `rotations take effect immediately and in every shell. Do not simply unset ${credential.source}: ` + `nothing was found in the Keychain or on disk, so that would leave this client with no credential at all.` : `This environment has no HOME or HASNA_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.`;
|
|
7338
|
+
return `${origin}, a variable in this process's environment. If a wrapper injected it for this one process, the ` + `wrapper re-reads its store on every invocation and the stored key itself is being rejected \u2014 rotate it. ` + `If this SHELL exported it, the export is a snapshot taken when the shell started: a STALE SHELL that ` + `exported the key before it was rotated keeps sending the old one until it exits. ${remedy}`;
|
|
7339
|
+
}
|
|
7340
|
+
if (credential.tier === "keychain") {
|
|
7341
|
+
return `${origin}, which was re-read from the Keychain on this very call \u2014 so a stale shell is NOT the cause ` + `here. The stored item is genuinely being rejected: update it with the current key, or re-run the fleet ` + `key distribution so this machine gets the current key.`;
|
|
7342
|
+
}
|
|
7343
|
+
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.`;
|
|
7344
|
+
}
|
|
7345
|
+
function assertNoAuthorityOverrideHeaders(headers, source) {
|
|
7346
|
+
if (!headers)
|
|
7347
|
+
return;
|
|
7348
|
+
const forbidden = Object.keys(headers).find((name) => AUTHORITY_OVERRIDE_HEADERS2.has(name.trim().toLowerCase()));
|
|
7349
|
+
if (forbidden) {
|
|
7350
|
+
throw new Error(`Authenticated ${source} headers must not set authority header '${forbidden}'.`);
|
|
7351
|
+
}
|
|
7352
|
+
}
|
|
7353
|
+
function appendQuery(path, query) {
|
|
7354
|
+
if (!query)
|
|
7355
|
+
return path;
|
|
7356
|
+
const params = query instanceof URLSearchParams ? query : new URLSearchParams;
|
|
7357
|
+
if (!(query instanceof URLSearchParams)) {
|
|
7358
|
+
for (const [key, value] of Object.entries(query)) {
|
|
7359
|
+
if (value === null || value === undefined)
|
|
7360
|
+
continue;
|
|
7361
|
+
if (Array.isArray(value)) {
|
|
7362
|
+
for (const v of value)
|
|
7363
|
+
params.append(key, String(v));
|
|
7364
|
+
} else {
|
|
7365
|
+
params.append(key, String(value));
|
|
7366
|
+
}
|
|
7367
|
+
}
|
|
7368
|
+
}
|
|
7369
|
+
const qs = params.toString();
|
|
7370
|
+
if (!qs)
|
|
7371
|
+
return path;
|
|
7372
|
+
return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
|
|
7373
|
+
}
|
|
7374
|
+
function createHasnaHttpTransportInternal(options, requestBindingProvider) {
|
|
7375
|
+
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
7376
|
+
const base = toV1BaseUrl(options.baseUrl);
|
|
7377
|
+
const timeoutMs = options.timeoutMs ?? 30000;
|
|
7378
|
+
const sleep = options.sleepImpl ?? defaultSleep;
|
|
7379
|
+
const defaultRetry = options.retry;
|
|
7380
|
+
function resolveRetry(callRetry) {
|
|
7381
|
+
const chosen = callRetry !== undefined ? callRetry : defaultRetry;
|
|
7382
|
+
if (chosen === false)
|
|
7383
|
+
return null;
|
|
7384
|
+
const r = chosen ?? {};
|
|
7385
|
+
return {
|
|
7386
|
+
retries: r.retries ?? 2,
|
|
7387
|
+
baseDelayMs: r.baseDelayMs ?? 200,
|
|
7388
|
+
maxDelayMs: r.maxDelayMs ?? 2000,
|
|
7389
|
+
retryStatuses: r.retryStatuses ?? [...DEFAULT_RETRY_STATUSES]
|
|
7390
|
+
};
|
|
7391
|
+
}
|
|
7392
|
+
async function once(method, rel, url, body, opts, credential) {
|
|
7393
|
+
assertNoAuthorityOverrideHeaders(options.headers, "transport");
|
|
7394
|
+
assertNoAuthorityOverrideHeaders(opts.headers, "request");
|
|
7395
|
+
const headers = {
|
|
7396
|
+
"x-api-key": credential.apiKey,
|
|
7397
|
+
Authorization: `Bearer ${credential.apiKey}`,
|
|
7398
|
+
Accept: "application/json",
|
|
7399
|
+
...options.headers ?? {},
|
|
7400
|
+
...opts.headers ?? {}
|
|
7401
|
+
};
|
|
7402
|
+
if (opts.idempotencyKey)
|
|
7403
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
7404
|
+
const init = {
|
|
7405
|
+
method,
|
|
7406
|
+
headers,
|
|
7407
|
+
redirect: "manual"
|
|
7408
|
+
};
|
|
7409
|
+
if (body !== undefined) {
|
|
7410
|
+
headers["Content-Type"] = "application/json";
|
|
7411
|
+
init.body = JSON.stringify(body);
|
|
7412
|
+
}
|
|
7413
|
+
const controller = new AbortController;
|
|
7414
|
+
const onAbort = () => controller.abort();
|
|
7415
|
+
if (opts.signal) {
|
|
7416
|
+
if (opts.signal.aborted)
|
|
7417
|
+
controller.abort();
|
|
7418
|
+
else
|
|
7419
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
7420
|
+
}
|
|
7421
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? timeoutMs);
|
|
7422
|
+
init.signal = controller.signal;
|
|
7423
|
+
let response;
|
|
7424
|
+
try {
|
|
7425
|
+
response = await fetchImpl(url, init);
|
|
7426
|
+
} catch (error) {
|
|
7427
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
7428
|
+
if (opts.signal?.aborted)
|
|
7429
|
+
return { ok: false, retryable: false, error: err };
|
|
7430
|
+
return { ok: false, retryable: true, error: err };
|
|
7431
|
+
} finally {
|
|
7432
|
+
clearTimeout(timer);
|
|
7433
|
+
if (opts.signal)
|
|
7434
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
7435
|
+
}
|
|
7436
|
+
const authenticationFailure = response.status === 401 || response.status === 403;
|
|
7437
|
+
let parsed = undefined;
|
|
7438
|
+
if (authenticationFailure) {
|
|
7439
|
+
try {
|
|
7440
|
+
await response.body?.cancel();
|
|
7441
|
+
} catch {}
|
|
7442
|
+
} else {
|
|
7443
|
+
const text = await response.text();
|
|
7444
|
+
if (text.length > 0) {
|
|
7445
|
+
try {
|
|
7446
|
+
parsed = JSON.parse(text);
|
|
7447
|
+
} catch {
|
|
7448
|
+
parsed = text;
|
|
7449
|
+
}
|
|
7450
|
+
}
|
|
7451
|
+
}
|
|
7452
|
+
if (!response.ok) {
|
|
7453
|
+
if (response.status >= 300 && response.status < 400) {
|
|
7454
|
+
return {
|
|
7455
|
+
ok: false,
|
|
7456
|
+
retryable: false,
|
|
7457
|
+
error: new HasnaHttpError(method, rel, response.status, parsed)
|
|
7458
|
+
};
|
|
7459
|
+
}
|
|
7460
|
+
if (authenticationFailure) {
|
|
7461
|
+
return {
|
|
7462
|
+
ok: false,
|
|
7463
|
+
retryable: false,
|
|
7464
|
+
error: new HasnaHttpError(method, rel, response.status, undefined, {
|
|
7465
|
+
source: credential.source,
|
|
7466
|
+
tier: credential.tier,
|
|
7467
|
+
guidance: authFailureGuidance(credential)
|
|
7468
|
+
})
|
|
7469
|
+
};
|
|
7470
|
+
}
|
|
7471
|
+
const retry = resolveRetry(opts.retry);
|
|
7472
|
+
const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
|
|
7473
|
+
return { ok: false, retryable, error: new HasnaHttpError(method, rel, response.status, parsed) };
|
|
7474
|
+
}
|
|
7475
|
+
return { ok: true, value: parsed };
|
|
7476
|
+
}
|
|
7477
|
+
async function request(method, path, body, opts = {}) {
|
|
7478
|
+
const upper = method.toUpperCase();
|
|
7479
|
+
const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
|
|
7480
|
+
const retry = resolveRetry(opts.retry);
|
|
7481
|
+
const methodRetryable = IDEMPOTENT_METHODS2.has(upper) || Boolean(opts.idempotencyKey);
|
|
7482
|
+
const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
|
|
7483
|
+
const binding = requestBindingProvider ? await requestBindingProvider() : {
|
|
7484
|
+
baseUrl: base,
|
|
7485
|
+
credential: await resolveRequestCredential(options.name, options.apiKey)
|
|
7486
|
+
};
|
|
7487
|
+
const url = `${binding.baseUrl}${rel}`;
|
|
7488
|
+
const credential = binding.credential;
|
|
7489
|
+
let last = null;
|
|
7490
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
7491
|
+
const result = await once(upper, rel, url, body, opts, credential);
|
|
7492
|
+
if (result.ok)
|
|
7493
|
+
return result.value;
|
|
7494
|
+
last = result;
|
|
7495
|
+
const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
|
|
7496
|
+
if (!canRetry)
|
|
7497
|
+
break;
|
|
7498
|
+
const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
|
|
7499
|
+
const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
|
|
7500
|
+
await sleep(backoff + jitter);
|
|
7501
|
+
}
|
|
7502
|
+
throw last.error;
|
|
7503
|
+
}
|
|
7504
|
+
return {
|
|
7505
|
+
baseUrl: base,
|
|
7506
|
+
request,
|
|
7507
|
+
get: (path, opts) => request("GET", path, undefined, opts),
|
|
7508
|
+
post: (path, body, opts) => request("POST", path, body, opts),
|
|
7509
|
+
put: (path, body, opts) => request("PUT", path, body, opts),
|
|
7510
|
+
patch: (path, body, opts) => request("PATCH", path, body, opts),
|
|
7511
|
+
del: (path, body, opts) => request("DELETE", path, body, opts)
|
|
7512
|
+
};
|
|
7513
|
+
}
|
|
7514
|
+
function createClientTransport(name, env = process.env, overrides) {
|
|
7515
|
+
const credentialOptions = overrides?.credentials;
|
|
7516
|
+
const snapshotOptions = { ...credentialOptions ? { credentials: credentialOptions } : {} };
|
|
7517
|
+
const resolution = resolveClientTransportSnapshot(name, env, snapshotOptions).resolution;
|
|
7518
|
+
const sameBinding = (left, right) => left.resolution.baseUrl === right.resolution.baseUrl && left.credential.apiKey === right.credential.apiKey && left.credential.pointerVaultKey === right.credential.pointerVaultKey && left.credential.source === right.credential.source && left.credential.tier === right.credential.tier;
|
|
7519
|
+
const unstableConfiguration = () => new ClientTransportConfigurationError(name, "The configured service authority or credential changed while a request was being prepared; no authenticated request was sent.");
|
|
7520
|
+
const requestBindingProvider = async () => {
|
|
7521
|
+
const first = resolveClientTransportSnapshot(name, env, snapshotOptions);
|
|
7522
|
+
const reviewed = resolveClientTransportSnapshot(name, env, snapshotOptions);
|
|
7523
|
+
if (!sameBinding(first, reviewed))
|
|
7524
|
+
throw unstableConfiguration();
|
|
7525
|
+
if (reviewed.resolution.baseUrl !== resolution.baseUrl) {
|
|
7526
|
+
throw new ClientTransportConfigurationError(name, "The configured service authority changed; rebuild the client before sending credentials.");
|
|
7527
|
+
}
|
|
7528
|
+
const credential = await resolveRequestCredential(name, () => reviewed.credential, env);
|
|
7529
|
+
const immediatelyBeforeDispatch = resolveClientTransportSnapshot(name, env, snapshotOptions);
|
|
7530
|
+
if (!sameBinding(reviewed, immediatelyBeforeDispatch))
|
|
7531
|
+
throw unstableConfiguration();
|
|
7532
|
+
if (immediatelyBeforeDispatch.resolution.baseUrl !== resolution.baseUrl) {
|
|
7533
|
+
throw new ClientTransportConfigurationError(name, "The configured service authority changed; rebuild the client before sending credentials.");
|
|
7534
|
+
}
|
|
7535
|
+
return { baseUrl: immediatelyBeforeDispatch.resolution.baseUrl, credential };
|
|
7536
|
+
};
|
|
7537
|
+
return {
|
|
7538
|
+
transport: "http",
|
|
7539
|
+
client: createHasnaHttpTransportInternal({
|
|
7540
|
+
name,
|
|
7541
|
+
baseUrl: resolution.baseUrl,
|
|
7542
|
+
apiKey: () => {
|
|
7543
|
+
throw new Error("The authenticated request binding provider was not invoked.");
|
|
7544
|
+
},
|
|
7545
|
+
...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
|
|
7546
|
+
...overrides?.headers ? { headers: overrides.headers } : {},
|
|
7547
|
+
...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
|
|
7548
|
+
...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
|
|
7549
|
+
...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
|
|
7550
|
+
}, requestBindingProvider),
|
|
7551
|
+
resolution
|
|
7552
|
+
};
|
|
7553
|
+
}
|
|
7554
|
+
function resourcePath(resource) {
|
|
7555
|
+
const trimmed = resource.replace(/^\/+|\/+$/g, "");
|
|
7556
|
+
if (!trimmed)
|
|
7557
|
+
throw new Error("resource must be a non-empty path segment");
|
|
7558
|
+
return `/${trimmed}`;
|
|
7559
|
+
}
|
|
7560
|
+
function entityPath(resource, id) {
|
|
7561
|
+
if (id === undefined || id === null || `${id}`.length === 0) {
|
|
7562
|
+
throw new Error("id must be a non-empty string");
|
|
7563
|
+
}
|
|
7564
|
+
return `${resourcePath(resource)}/${encodeURIComponent(String(id))}`;
|
|
7565
|
+
}
|
|
7566
|
+
function newIdempotencyKey() {
|
|
7567
|
+
const g = globalThis;
|
|
7568
|
+
if (g.crypto?.randomUUID)
|
|
7569
|
+
return g.crypto.randomUUID();
|
|
7570
|
+
return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
7571
|
+
}
|
|
7572
|
+
function extractItems(raw) {
|
|
7573
|
+
if (Array.isArray(raw))
|
|
7574
|
+
return raw;
|
|
7575
|
+
if (raw && typeof raw === "object") {
|
|
7576
|
+
const obj = raw;
|
|
7577
|
+
for (const key of ["items", "data", "results", "rows", "records"]) {
|
|
7578
|
+
if (Array.isArray(obj[key]))
|
|
7579
|
+
return obj[key];
|
|
7580
|
+
}
|
|
7581
|
+
}
|
|
7582
|
+
return [];
|
|
7583
|
+
}
|
|
7584
|
+
function extractTotal(raw) {
|
|
7585
|
+
if (raw && typeof raw === "object") {
|
|
7586
|
+
const obj = raw;
|
|
7587
|
+
for (const key of ["total", "count", "totalCount", "total_count"]) {
|
|
7588
|
+
if (typeof obj[key] === "number")
|
|
7589
|
+
return obj[key];
|
|
7590
|
+
}
|
|
7591
|
+
}
|
|
7592
|
+
return null;
|
|
7593
|
+
}
|
|
7594
|
+
function extractCursor(raw) {
|
|
7595
|
+
if (raw && typeof raw === "object") {
|
|
7596
|
+
const obj = raw;
|
|
7597
|
+
for (const key of ["cursor", "nextCursor", "next_cursor", "next"]) {
|
|
7598
|
+
if (typeof obj[key] === "string")
|
|
7599
|
+
return obj[key];
|
|
7600
|
+
}
|
|
7601
|
+
}
|
|
7602
|
+
return null;
|
|
7603
|
+
}
|
|
7604
|
+
function isNotFoundHttpError(error) {
|
|
7605
|
+
return typeof error === "object" && error !== null && error.name === "HasnaHttpError" && error.status === 404;
|
|
7606
|
+
}
|
|
7607
|
+
function createHasnaStorageClient(name, transport) {
|
|
7608
|
+
return {
|
|
7609
|
+
name,
|
|
7610
|
+
baseUrl: transport.baseUrl,
|
|
7611
|
+
transport,
|
|
7612
|
+
async list(resource, options = {}) {
|
|
7613
|
+
const raw = await transport.get(resourcePath(resource), options);
|
|
7614
|
+
return {
|
|
7615
|
+
items: extractItems(raw),
|
|
7616
|
+
total: extractTotal(raw),
|
|
7617
|
+
cursor: extractCursor(raw),
|
|
7618
|
+
raw
|
|
7619
|
+
};
|
|
7620
|
+
},
|
|
7621
|
+
async get(resource, id, options = {}) {
|
|
7622
|
+
try {
|
|
7623
|
+
return await transport.get(entityPath(resource, id), options);
|
|
7624
|
+
} catch (error) {
|
|
7625
|
+
if (isNotFoundHttpError(error))
|
|
7626
|
+
return null;
|
|
7627
|
+
throw error;
|
|
7628
|
+
}
|
|
7629
|
+
},
|
|
7630
|
+
async create(resource, body, options = {}) {
|
|
7631
|
+
const { idempotencyKey, ...rest } = options;
|
|
7632
|
+
return transport.post(resourcePath(resource), body, {
|
|
7633
|
+
...rest,
|
|
7634
|
+
idempotencyKey: idempotencyKey ?? newIdempotencyKey()
|
|
7635
|
+
});
|
|
7636
|
+
},
|
|
7637
|
+
async update(resource, id, patch, options = {}) {
|
|
7638
|
+
const { method = "PATCH", idempotencyKey, ...rest } = options;
|
|
7639
|
+
const call = method === "PUT" ? transport.put : transport.patch;
|
|
7640
|
+
return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
|
|
7641
|
+
},
|
|
7642
|
+
async delete(resource, id, options = {}) {
|
|
7643
|
+
try {
|
|
7644
|
+
await transport.del(entityPath(resource, id), undefined, options);
|
|
7645
|
+
} catch (error) {
|
|
7646
|
+
if (isNotFoundHttpError(error))
|
|
7647
|
+
return;
|
|
7648
|
+
throw error;
|
|
7649
|
+
}
|
|
7650
|
+
}
|
|
7651
|
+
};
|
|
7652
|
+
}
|
|
7653
|
+
function resolveStorageClient(name, env = process.env, overrides) {
|
|
7654
|
+
const wired = createClientTransport(name, env, overrides);
|
|
7655
|
+
return { transport: "http", client: createHasnaStorageClient(name, wired.client) };
|
|
7656
|
+
}
|
|
7657
|
+
var CREDENTIAL_PROFILE_ENV_KEY2 = "HASNA_PROFILE", CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES2, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE = "caller-supplied CredentialProvider", AMBIENT_ENVIRONMENT2, SECRETS_PACKAGE_SPECIFIER2, requireSecretsSdk2, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, HasnaHttpError, DEFAULT_RETRY_STATUSES, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2, defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
7658
|
+
var init_storage = __esm(() => {
|
|
7659
|
+
CredentialResolutionError = class CredentialResolutionError extends Error {
|
|
7660
|
+
appName;
|
|
7661
|
+
attempted;
|
|
7662
|
+
constructor(appName, message, attempted) {
|
|
7663
|
+
super(message);
|
|
7664
|
+
this.name = "CredentialResolutionError";
|
|
7665
|
+
this.appName = appName;
|
|
7666
|
+
this.attempted = attempted;
|
|
7667
|
+
}
|
|
7668
|
+
};
|
|
7669
|
+
CredentialFileUnsafeError = class CredentialFileUnsafeError extends Error {
|
|
7670
|
+
path;
|
|
7671
|
+
constructor(path, reason) {
|
|
7672
|
+
super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
|
|
7673
|
+
this.name = "CredentialFileUnsafeError";
|
|
7674
|
+
this.path = path;
|
|
7675
|
+
}
|
|
7676
|
+
};
|
|
7677
|
+
MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
|
|
7678
|
+
SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
7679
|
+
SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
7680
|
+
ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
7681
|
+
VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
7682
|
+
CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
|
|
7683
|
+
INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
|
|
7684
|
+
CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
|
|
7685
|
+
AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
7686
|
+
SECRETS_PACKAGE_SPECIFIER2 = "@hasna/" + "secrets";
|
|
7687
|
+
requireSecretsSdk2 = createRequire2(import.meta.url);
|
|
7688
|
+
ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
7689
|
+
DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
7690
|
+
ClientTransportConfigurationError = class ClientTransportConfigurationError extends Error {
|
|
7691
|
+
appName;
|
|
7692
|
+
sources;
|
|
7693
|
+
constructor(appName, message, sources = []) {
|
|
7694
|
+
super(message);
|
|
7695
|
+
this.name = "ClientTransportConfigurationError";
|
|
7696
|
+
this.appName = appName;
|
|
7697
|
+
this.sources = Object.freeze([...sources]);
|
|
7698
|
+
}
|
|
7699
|
+
};
|
|
7700
|
+
HasnaHttpError = class HasnaHttpError extends Error {
|
|
7701
|
+
status;
|
|
7702
|
+
method;
|
|
7703
|
+
path;
|
|
7704
|
+
credentialSource;
|
|
7705
|
+
credentialTier;
|
|
7706
|
+
constructor(method, path, status, body, credential) {
|
|
7707
|
+
const guidance = credential ? `. ${credential.guidance}` : "";
|
|
7708
|
+
super(`Hasna cloud request failed: ${method} ${path} -> ${status}${guidance}`);
|
|
7709
|
+
this.name = "HasnaHttpError";
|
|
7710
|
+
this.status = status;
|
|
7711
|
+
this.method = method;
|
|
7712
|
+
this.path = path;
|
|
7713
|
+
Object.defineProperty(this, "body", {
|
|
7714
|
+
value: body,
|
|
7715
|
+
enumerable: status !== 401 && status !== 403,
|
|
7716
|
+
writable: false,
|
|
7717
|
+
configurable: false
|
|
7718
|
+
});
|
|
7719
|
+
this.credentialSource = credential?.source ?? null;
|
|
7720
|
+
this.credentialTier = credential?.tier ?? null;
|
|
7721
|
+
}
|
|
7722
|
+
};
|
|
7723
|
+
DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
|
|
7724
|
+
IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
7725
|
+
AUTHORITY_OVERRIDE_HEADERS2 = new Set([
|
|
7726
|
+
"host",
|
|
7727
|
+
":authority",
|
|
7728
|
+
"forwarded",
|
|
7729
|
+
"x-forwarded-host",
|
|
7730
|
+
"x-original-host"
|
|
7731
|
+
]);
|
|
7732
|
+
});
|
|
7733
|
+
|
|
7734
|
+
// src/lib/transport-resolver.ts
|
|
7735
|
+
function rethrowInstructionsAuthorityFailure(error) {
|
|
7736
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7737
|
+
const name = error instanceof Error ? error.name : "";
|
|
7738
|
+
const failure = (code, lead) => {
|
|
7739
|
+
throw new Error(`${code}: ${lead} ${message} There is no local fallback: local SQLite is opt-in only ` + `(${INSTRUCTIONS_LOCAL_OPT_IN_ENV}=1) and is disabled by default \u2014 failing closed`, { cause: error });
|
|
7740
|
+
};
|
|
7741
|
+
if (name === "CredentialResolutionError" || name === "CredentialFileUnsafeError") {
|
|
7742
|
+
return failure("REMOTE_API_CREDENTIAL_INVALID", "The configured Instructions credential could not be used.");
|
|
7743
|
+
}
|
|
7744
|
+
if (/no API key could be resolved/.test(message)) {
|
|
7745
|
+
if (/is not set and no API key could be resolved/.test(message)) {
|
|
7746
|
+
return failure("REMOTE_API_CONFIG_MISSING", "no Instructions credential resolved from the Keychain item " + "hasna.credentials.instructions.api-key, ~/.hasna/instructions/config/credentials, " + `or ${clientTransportEnvKeys(INSTRUCTIONS_APP).apiKeyKeys[0]}.`);
|
|
7747
|
+
}
|
|
7748
|
+
return failure("REMOTE_API_KEY_MISSING", "an Instructions authority is configured but no API key resolved \u2014 looked in " + "hasna.credentials.instructions.api-key, ~/.hasna/instructions/config/credentials, " + `and ${clientTransportEnvKeys(INSTRUCTIONS_APP).apiKeyKeys[0]}.`);
|
|
7749
|
+
}
|
|
7750
|
+
return failure("REMOTE_API_URL_INVALID", "the configured Instructions authority is invalid.");
|
|
7751
|
+
}
|
|
7752
|
+
function resolveInstructionsStorageClient(env = process.env, options = {}) {
|
|
7753
|
+
const inputs = instructionsResolverInputs(env, options.credentials);
|
|
7754
|
+
try {
|
|
7755
|
+
return resolveStorageClient(INSTRUCTIONS_APP, inputs.env, {
|
|
7756
|
+
fetchImpl: (input, init) => fetch(input, { ...init, redirect: "manual" }),
|
|
7757
|
+
credentials: inputs.credentials
|
|
7758
|
+
});
|
|
7759
|
+
} catch (error) {
|
|
7760
|
+
rethrowInstructionsAuthorityFailure(error);
|
|
7761
|
+
}
|
|
7762
|
+
}
|
|
7763
|
+
function announceLocalInstructionsMode(write = (line) => process.stderr.write(`${line}
|
|
7764
|
+
`)) {
|
|
7765
|
+
if (localNoticePrinted)
|
|
7766
|
+
return false;
|
|
7767
|
+
localNoticePrinted = true;
|
|
7768
|
+
write(instructionsLocalModeNotice());
|
|
7769
|
+
return true;
|
|
7770
|
+
}
|
|
7771
|
+
var INSTRUCTIONS_APP = "instructions", INSTRUCTIONS_LOCAL_OPT_IN_ENV, localNoticePrinted = false;
|
|
7772
|
+
var init_transport_resolver = __esm(() => {
|
|
7773
|
+
init_transport();
|
|
7774
|
+
init_storage();
|
|
7775
|
+
init_local_opt_in();
|
|
7776
|
+
INSTRUCTIONS_LOCAL_OPT_IN_ENV = INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS[0];
|
|
7777
|
+
});
|
|
7778
|
+
|
|
6509
7779
|
// src/data/config-store.ts
|
|
6510
7780
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6511
7781
|
function parseBoundedPagePayload(value, label) {
|
|
@@ -6513,7 +7783,7 @@ function parseBoundedPagePayload(value, label) {
|
|
|
6513
7783
|
const consumed = Number(page?.cursor) + (page?.items?.length ?? 0);
|
|
6514
7784
|
const complete = Boolean(page && Number.isSafeInteger(page.total) && consumed >= Number(page.total));
|
|
6515
7785
|
if (!page || !Array.isArray(page.items) || !Number.isSafeInteger(page.total) || Number(page.total) < 0 || !Number.isSafeInteger(page.limit) || Number(page.limit) < 1 || !Number.isSafeInteger(page.cursor) || Number(page.cursor) < 0 || page.items.length > Number(page.limit) || typeof page.has_more !== "boolean" || typeof page.complete !== "boolean" || page.truncated !== false || page.next_cursor !== null && !Number.isSafeInteger(page.next_cursor) || page.complete !== complete || page.has_more !== !complete || page.next_cursor !== (complete ? null : consumed)) {
|
|
6516
|
-
throw new
|
|
7786
|
+
throw new Error(`${label} returned an invalid or truncated bounded-read envelope`);
|
|
6517
7787
|
}
|
|
6518
7788
|
return {
|
|
6519
7789
|
...page,
|
|
@@ -6528,27 +7798,20 @@ function parseBoundedOrLegacyPage(value, legacyItems, options, label) {
|
|
|
6528
7798
|
}
|
|
6529
7799
|
}
|
|
6530
7800
|
if (!Array.isArray(legacyItems)) {
|
|
6531
|
-
throw new
|
|
7801
|
+
throw new Error(`${label} returned neither a bounded envelope nor a complete legacy array`);
|
|
6532
7802
|
}
|
|
6533
7803
|
const normalized = normalizeBoundedReadOptions(options);
|
|
6534
7804
|
const page = boundedReadPage(legacyItems.slice(normalized.cursor, normalized.cursor + normalized.limit), legacyItems.length, normalized);
|
|
6535
7805
|
return { ...page, source_bounded: false };
|
|
6536
7806
|
}
|
|
6537
|
-
function
|
|
6538
|
-
|
|
6539
|
-
const apiUrl = env[API_URL_ENV]?.trim();
|
|
6540
|
-
const apiKey = env[API_KEY_ENV]?.trim();
|
|
6541
|
-
if (!apiUrl && !apiKey)
|
|
6542
|
-
return null;
|
|
6543
|
-
if (!apiUrl || !apiKey) {
|
|
6544
|
-
throw new Error(`API mode requires BOTH ${API_URL_ENV} and ${API_KEY_ENV}; only ` + `${apiUrl ? API_URL_ENV : API_KEY_ENV} is set. Set both to use the HTTP API, ` + `or unset both to use the local store.`);
|
|
6545
|
-
}
|
|
6546
|
-
return { apiUrl, apiKey };
|
|
7807
|
+
function isNotFoundHttpError2(err) {
|
|
7808
|
+
return typeof err === "object" && err !== null && err.name === "HasnaHttpError" && err.status === 404;
|
|
6547
7809
|
}
|
|
6548
7810
|
|
|
6549
7811
|
class LocalConfigStore {
|
|
6550
7812
|
db;
|
|
6551
7813
|
mode = "local";
|
|
7814
|
+
v1BaseUrl = null;
|
|
6552
7815
|
constructor(db) {
|
|
6553
7816
|
this.db = db;
|
|
6554
7817
|
}
|
|
@@ -6677,50 +7940,22 @@ class LocalConfigStore {
|
|
|
6677
7940
|
|
|
6678
7941
|
class CloudConfigStore {
|
|
6679
7942
|
mode = "api";
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
this.
|
|
6685
|
-
this.apiKey = config.apiKey;
|
|
6686
|
-
this.timeoutMs = config.timeoutMs ?? 30000;
|
|
7943
|
+
v1BaseUrl;
|
|
7944
|
+
client;
|
|
7945
|
+
constructor(client) {
|
|
7946
|
+
this.client = client;
|
|
7947
|
+
this.v1BaseUrl = client.baseUrl;
|
|
6687
7948
|
}
|
|
6688
7949
|
async request(method, path, body, opts = {}) {
|
|
6689
|
-
const controller = new AbortController;
|
|
6690
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
6691
|
-
const headers = {
|
|
6692
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
6693
|
-
Accept: "application/json"
|
|
6694
|
-
};
|
|
6695
|
-
if (body !== undefined)
|
|
6696
|
-
headers["Content-Type"] = "application/json";
|
|
6697
|
-
if (opts.idempotent)
|
|
6698
|
-
headers["Idempotency-Key"] = randomUUID2();
|
|
6699
7950
|
try {
|
|
6700
|
-
const
|
|
6701
|
-
|
|
6702
|
-
headers,
|
|
6703
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
6704
|
-
signal: controller.signal
|
|
7951
|
+
const data = await this.client.transport.request(method, path, body, {
|
|
7952
|
+
...opts.idempotent ? { idempotencyKey: randomUUID2() } : {}
|
|
6705
7953
|
});
|
|
6706
|
-
|
|
7954
|
+
return { status: 200, data };
|
|
7955
|
+
} catch (err) {
|
|
7956
|
+
if (opts.allow404 && isNotFoundHttpError2(err))
|
|
6707
7957
|
return { status: 404, data: null };
|
|
6708
|
-
|
|
6709
|
-
let parsed = null;
|
|
6710
|
-
if (text) {
|
|
6711
|
-
try {
|
|
6712
|
-
parsed = JSON.parse(text);
|
|
6713
|
-
} catch {
|
|
6714
|
-
parsed = text;
|
|
6715
|
-
}
|
|
6716
|
-
}
|
|
6717
|
-
if (!res.ok) {
|
|
6718
|
-
const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : undefined) ?? `HTTP ${res.status} on ${method} ${path}`;
|
|
6719
|
-
throw new CloudHttpError(res.status, message, parsed);
|
|
6720
|
-
}
|
|
6721
|
-
return { status: res.status, data: parsed };
|
|
6722
|
-
} finally {
|
|
6723
|
-
clearTimeout(timer);
|
|
7958
|
+
throw err;
|
|
6724
7959
|
}
|
|
6725
7960
|
}
|
|
6726
7961
|
async listConfigs(filter = {}) {
|
|
@@ -6876,7 +8111,7 @@ class CloudConfigStore {
|
|
|
6876
8111
|
if (first.status !== 404) {
|
|
6877
8112
|
if (isUsable(first.data))
|
|
6878
8113
|
return first;
|
|
6879
|
-
throw new
|
|
8114
|
+
throw new Error(`Cloud /v1 returned an invalid profile response for ${pathForId(idOrSlug)}`);
|
|
6880
8115
|
}
|
|
6881
8116
|
const profiles = await this.listProfiles();
|
|
6882
8117
|
const profile = profiles.find((candidate) => candidate.id === idOrSlug || candidate.slug === idOrSlug);
|
|
@@ -6889,7 +8124,7 @@ class CloudConfigStore {
|
|
|
6889
8124
|
if (response.status !== 404) {
|
|
6890
8125
|
if (isUsable(response.data))
|
|
6891
8126
|
return response;
|
|
6892
|
-
throw new
|
|
8127
|
+
throw new Error(`Cloud /v1 returned an invalid profile response for ${pathForId(candidate)}`);
|
|
6893
8128
|
}
|
|
6894
8129
|
}
|
|
6895
8130
|
return { status: 404, data: null };
|
|
@@ -6972,7 +8207,7 @@ class CloudConfigStore {
|
|
|
6972
8207
|
}
|
|
6973
8208
|
if (data && "complete" in data) {
|
|
6974
8209
|
if (data.complete !== true || data.truncated !== false) {
|
|
6975
|
-
throw new
|
|
8210
|
+
throw new Error("Cloud /v1 profile resolve returned an incomplete or truncated read");
|
|
6976
8211
|
}
|
|
6977
8212
|
return { ...data, source_bounded: data.source_bounded ?? true };
|
|
6978
8213
|
}
|
|
@@ -6988,7 +8223,7 @@ class CloudConfigStore {
|
|
|
6988
8223
|
};
|
|
6989
8224
|
}
|
|
6990
8225
|
{
|
|
6991
|
-
throw new
|
|
8226
|
+
throw new Error("Cloud /v1 profile resolve returned an incomplete or truncated read");
|
|
6992
8227
|
}
|
|
6993
8228
|
}
|
|
6994
8229
|
async registerMachine(hostname2, os, arch2) {
|
|
@@ -7011,14 +8246,18 @@ class CloudConfigStore {
|
|
|
7011
8246
|
});
|
|
7012
8247
|
}
|
|
7013
8248
|
async reset() {
|
|
7014
|
-
throw new Error("`init --force` cannot wipe the shared cloud store from a client. " + "
|
|
8249
|
+
throw new Error("`init --force` cannot wipe the shared cloud store from a client. " + "Point this run at the local store (HASNA_INSTRUCTIONS_LOCAL=1 with no hosted credential) to reset it instead.");
|
|
7015
8250
|
}
|
|
7016
8251
|
}
|
|
7017
|
-
function resolveConfigStore(env = process.env) {
|
|
7018
|
-
|
|
7019
|
-
|
|
8252
|
+
function resolveConfigStore(env = process.env, options = {}) {
|
|
8253
|
+
if (selectsInstructionsLocalStore(env)) {
|
|
8254
|
+
if (env === process.env)
|
|
8255
|
+
announceLocalInstructionsMode();
|
|
8256
|
+
return new LocalConfigStore;
|
|
8257
|
+
}
|
|
8258
|
+
const { client } = resolveInstructionsStorageClient(env, options);
|
|
8259
|
+
return new CloudConfigStore(client);
|
|
7020
8260
|
}
|
|
7021
|
-
var CloudHttpError, API_URL_ENV = "HASNA_INSTRUCTIONS_API_URL", API_KEY_ENV = "HASNA_INSTRUCTIONS_API_KEY";
|
|
7022
8261
|
var init_config_store = __esm(() => {
|
|
7023
8262
|
init_configs();
|
|
7024
8263
|
init_profiles();
|
|
@@ -7028,21 +8267,12 @@ var init_config_store = __esm(() => {
|
|
|
7028
8267
|
init_types();
|
|
7029
8268
|
init_bounded_read();
|
|
7030
8269
|
init_instruction_graph();
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
status;
|
|
7034
|
-
body;
|
|
7035
|
-
constructor(status, message, body) {
|
|
7036
|
-
super(message);
|
|
7037
|
-
this.status = status;
|
|
7038
|
-
this.body = body;
|
|
7039
|
-
this.name = "CloudHttpError";
|
|
7040
|
-
}
|
|
7041
|
-
};
|
|
8270
|
+
init_transport_resolver();
|
|
8271
|
+
init_local_opt_in();
|
|
7042
8272
|
});
|
|
7043
8273
|
|
|
7044
8274
|
// src/lib/session-render-ownership.ts
|
|
7045
|
-
import { existsSync as existsSync5, readFileSync as
|
|
8275
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, statSync } from "fs";
|
|
7046
8276
|
import { dirname as dirname3, join as join8, parse as parse2, relative as relative2, sep } from "path";
|
|
7047
8277
|
function toSegments(absolutePath2) {
|
|
7048
8278
|
return absolutePath2.replaceAll("\\", "/").split("/").filter(Boolean);
|
|
@@ -7074,7 +8304,7 @@ function readManifestRelativePaths(manifestPath) {
|
|
|
7074
8304
|
}
|
|
7075
8305
|
let manifest;
|
|
7076
8306
|
try {
|
|
7077
|
-
manifest = JSON.parse(
|
|
8307
|
+
manifest = JSON.parse(readFileSync3(manifestPath, "utf-8"));
|
|
7078
8308
|
} catch {
|
|
7079
8309
|
return null;
|
|
7080
8310
|
}
|
|
@@ -7126,11 +8356,11 @@ __export(exports_apply, {
|
|
|
7126
8356
|
applyConfigs: () => applyConfigs,
|
|
7127
8357
|
applyConfig: () => applyConfig
|
|
7128
8358
|
});
|
|
7129
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as
|
|
8359
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync4, realpathSync, writeFileSync } from "fs";
|
|
7130
8360
|
import { basename as basename3, dirname as dirname4, join as join9, resolve as resolve5 } from "path";
|
|
7131
|
-
import { homedir as
|
|
8361
|
+
import { homedir as homedir4 } from "os";
|
|
7132
8362
|
function getConfigHome() {
|
|
7133
|
-
return process.env["CONFIGS_HOME"] || process.env["HOME"] ||
|
|
8363
|
+
return process.env["CONFIGS_HOME"] || process.env["HOME"] || homedir4();
|
|
7134
8364
|
}
|
|
7135
8365
|
function expandPath(p) {
|
|
7136
8366
|
if (p.startsWith("~/")) {
|
|
@@ -7177,7 +8407,7 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
|
|
|
7177
8407
|
}
|
|
7178
8408
|
const path = expandPath(renderedTargetPath);
|
|
7179
8409
|
const renderedForTarget = isCursorGlobalAuthorityPath(path) ? stampCursorGlobalAuthorityMarker(renderedContent) : renderedContent;
|
|
7180
|
-
const previousContent = existsSync6(path) ?
|
|
8410
|
+
const previousContent = existsSync6(path) ? readFileSync4(path, "utf-8") : null;
|
|
7181
8411
|
const changed = previousContent !== renderedForTarget;
|
|
7182
8412
|
if (!opts.dryRun) {
|
|
7183
8413
|
const dir = dirname4(path);
|
|
@@ -7217,7 +8447,7 @@ function wouldDestroyACredential(targetPath, renderedContent, format) {
|
|
|
7217
8447
|
const path = expandPath(targetPath);
|
|
7218
8448
|
if (!existsSync6(path))
|
|
7219
8449
|
return [];
|
|
7220
|
-
current =
|
|
8450
|
+
current = readFileSync4(path, "utf-8");
|
|
7221
8451
|
} catch {
|
|
7222
8452
|
return secretTokens;
|
|
7223
8453
|
}
|
|
@@ -7574,7 +8804,7 @@ __export(exports_sync, {
|
|
|
7574
8804
|
KNOWN_CONFIGS: () => KNOWN_CONFIGS,
|
|
7575
8805
|
CLAUDE_PROMPT_OUTPUTS: () => CLAUDE_PROMPT_OUTPUTS
|
|
7576
8806
|
});
|
|
7577
|
-
import { existsSync as existsSync7, readdirSync, readFileSync as
|
|
8807
|
+
import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync5 } from "fs";
|
|
7578
8808
|
import { basename as basename4, extname as extname2, join as join10 } from "path";
|
|
7579
8809
|
function claudeRuleOutputs(fileName) {
|
|
7580
8810
|
const stem = basename4(fileName, extname2(fileName));
|
|
@@ -7643,7 +8873,7 @@ async function syncProject(opts) {
|
|
|
7643
8873
|
if (!existsSync7(abs))
|
|
7644
8874
|
continue;
|
|
7645
8875
|
try {
|
|
7646
|
-
const rawContent =
|
|
8876
|
+
const rawContent = readFileSync5(abs, "utf-8");
|
|
7647
8877
|
if (rawContent.length > 500000) {
|
|
7648
8878
|
result.skipped.push(pf.file);
|
|
7649
8879
|
continue;
|
|
@@ -7685,7 +8915,7 @@ async function syncProject(opts) {
|
|
|
7685
8915
|
const mdFiles = readdirSync(ruleDir.dir).filter((f) => f.endsWith(".md") || f.endsWith(".mdc"));
|
|
7686
8916
|
for (const f of mdFiles) {
|
|
7687
8917
|
const abs = join10(ruleDir.dir, f);
|
|
7688
|
-
const raw =
|
|
8918
|
+
const raw = readFileSync5(abs, "utf-8");
|
|
7689
8919
|
const redacted = redactContent(raw, "markdown");
|
|
7690
8920
|
const machineAware = templateizeMachineContent(redacted.content, machine);
|
|
7691
8921
|
const content = machineAware.content;
|
|
@@ -7737,7 +8967,7 @@ async function syncKnown(opts = {}) {
|
|
|
7737
8967
|
result.skipped.push(`${targetPath} (generated output)`);
|
|
7738
8968
|
continue;
|
|
7739
8969
|
}
|
|
7740
|
-
const raw =
|
|
8970
|
+
const raw = readFileSync5(abs2, "utf-8");
|
|
7741
8971
|
const redacted = redactContent(raw, "markdown");
|
|
7742
8972
|
const machineAware = templateizeMachineContent(redacted.content, machine);
|
|
7743
8973
|
const content = machineAware.content;
|
|
@@ -7770,7 +9000,7 @@ async function syncKnown(opts = {}) {
|
|
|
7770
9000
|
continue;
|
|
7771
9001
|
}
|
|
7772
9002
|
try {
|
|
7773
|
-
const rawContent = normalizeKnownConfigSource(known,
|
|
9003
|
+
const rawContent = normalizeKnownConfigSource(known, readFileSync5(abs, "utf-8"));
|
|
7774
9004
|
if (rawContent.length > 500000) {
|
|
7775
9005
|
result.skipped.push(known.path + " (too large)");
|
|
7776
9006
|
continue;
|
|
@@ -7873,7 +9103,7 @@ function buildDiff(expectedContent, targetPath, storedFormat, showSecrets) {
|
|
|
7873
9103
|
const path = expandPath(targetPath);
|
|
7874
9104
|
if (!existsSync7(path))
|
|
7875
9105
|
return `(file not found on disk: ${path})`;
|
|
7876
|
-
const diskContent =
|
|
9106
|
+
const diskContent = readFileSync5(path, "utf-8");
|
|
7877
9107
|
if (diskContent === expectedContent)
|
|
7878
9108
|
return "(no diff \u2014 identical)";
|
|
7879
9109
|
const format = redactFormatForTarget(targetPath, storedFormat);
|
|
@@ -8101,9 +9331,9 @@ var init_sync = __esm(() => {
|
|
|
8101
9331
|
});
|
|
8102
9332
|
|
|
8103
9333
|
// src/lib/sync-dir.ts
|
|
8104
|
-
import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as
|
|
9334
|
+
import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
8105
9335
|
import { join as join11, relative as relative3 } from "path";
|
|
8106
|
-
import { homedir as
|
|
9336
|
+
import { homedir as homedir5 } from "os";
|
|
8107
9337
|
function shouldSkip(p) {
|
|
8108
9338
|
return SKIP.some((s) => p.includes(s));
|
|
8109
9339
|
}
|
|
@@ -8114,7 +9344,7 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
8114
9344
|
return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
|
|
8115
9345
|
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync2(absDir).map((f) => join11(absDir, f)).filter((f) => statSync2(f).isFile());
|
|
8116
9346
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
8117
|
-
const home =
|
|
9347
|
+
const home = homedir5();
|
|
8118
9348
|
const allConfigs = await store.listConfigs();
|
|
8119
9349
|
for (const file of files) {
|
|
8120
9350
|
if (shouldSkip(file)) {
|
|
@@ -8122,7 +9352,7 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
8122
9352
|
continue;
|
|
8123
9353
|
}
|
|
8124
9354
|
try {
|
|
8125
|
-
const content =
|
|
9355
|
+
const content = readFileSync6(file, "utf-8");
|
|
8126
9356
|
if (content.length > 500000) {
|
|
8127
9357
|
result.skipped.push(file + " (too large)");
|
|
8128
9358
|
continue;
|
|
@@ -8149,7 +9379,7 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
8149
9379
|
}
|
|
8150
9380
|
async function syncToDir(dir, opts = {}) {
|
|
8151
9381
|
const store = opts.store ?? resolveConfigStore();
|
|
8152
|
-
const home =
|
|
9382
|
+
const home = homedir5();
|
|
8153
9383
|
const absDir = expandPath(dir);
|
|
8154
9384
|
const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
|
|
8155
9385
|
const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
|
|
@@ -8196,8 +9426,8 @@ var init_sync_dir = __esm(() => {
|
|
|
8196
9426
|
var require_package = __commonJS((exports, module) => {
|
|
8197
9427
|
module.exports = {
|
|
8198
9428
|
name: "@hasna/instructions",
|
|
8199
|
-
version: "0.
|
|
8200
|
-
description: "AI coding agent instruction & configuration manager \u2014 store, version, apply, and share all your AI coding configs. CLI + MCP + HTTP API (instructions-serve) + generated SDK
|
|
9429
|
+
version: "0.6.0",
|
|
9430
|
+
description: "AI coding agent instruction & configuration manager \u2014 store, version, apply, and share all your AI coding configs. CLI + MCP + HTTP API (instructions-serve) + generated SDK.",
|
|
8201
9431
|
type: "module",
|
|
8202
9432
|
main: "dist/index.js",
|
|
8203
9433
|
types: "dist/index.d.ts",
|
|
@@ -8212,25 +9442,27 @@ var require_package = __commonJS((exports, module) => {
|
|
|
8212
9442
|
".": {
|
|
8213
9443
|
types: "./dist/index.d.ts",
|
|
8214
9444
|
import: "./dist/index.js"
|
|
9445
|
+
},
|
|
9446
|
+
"./sdk": {
|
|
9447
|
+
types: "./dist/sdk/index.d.ts",
|
|
9448
|
+
import: "./dist/sdk/index.js"
|
|
8215
9449
|
}
|
|
8216
9450
|
},
|
|
8217
9451
|
files: [
|
|
8218
9452
|
"dist",
|
|
8219
9453
|
"assets/skills/inbox/SKILL.md",
|
|
8220
|
-
"dashboard/dist",
|
|
8221
9454
|
"LICENSE",
|
|
8222
9455
|
"README.md"
|
|
8223
9456
|
],
|
|
8224
9457
|
scripts: {
|
|
8225
9458
|
clean: "rm -rf dist",
|
|
8226
|
-
build: "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts --outdir dist --target bun --external pg && tsc --emitDeclarationOnly --outDir dist",
|
|
8227
|
-
"build:server": "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts --outdir dist --target bun --external pg",
|
|
8228
|
-
"build:dashboard": "cd dashboard && bun run build",
|
|
9459
|
+
build: "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts --outdir dist --target bun --external pg && bun build src/sdk/index.ts --outdir dist/sdk --target bun && tsc --emitDeclarationOnly --outDir dist",
|
|
9460
|
+
"build:server": "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts --outdir dist --target bun --external pg && bun build src/sdk/index.ts --outdir dist/sdk --target bun",
|
|
8229
9461
|
migrate: "bun run src/server/index.ts migrate",
|
|
8230
9462
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
8231
9463
|
"kit:check": "bunx @hasna/contracts vendor-kit --check",
|
|
8232
9464
|
typecheck: "tsc --noEmit",
|
|
8233
|
-
test: "env -u HASNA_INSTRUCTIONS_API_URL -u HASNA_INSTRUCTIONS_API_KEY HASNA_STATE_HOME=$(mktemp -d) bun test",
|
|
9465
|
+
test: "env -u HASNA_INSTRUCTIONS_API_URL -u HASNA_INSTRUCTIONS_API_KEY -u INSTRUCTIONS_API_URL -u INSTRUCTIONS_API_KEY -u HASNA_INSTRUCTIONS_API_KEY_OVERRIDE -u HASNA_INSTRUCTIONS_API_KEY_REF -u HASNA_PROFILE HASNA_STATE_HOME=$(mktemp -d) bun test",
|
|
8234
9466
|
"check:package-secrets": "bun run src/cli/index.tsx package-manager-scan --fail-on-findings .",
|
|
8235
9467
|
"dev:cli": "bun run src/cli/index.tsx",
|
|
8236
9468
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
@@ -8251,8 +9483,7 @@ var require_package = __commonJS((exports, module) => {
|
|
|
8251
9483
|
"mcp",
|
|
8252
9484
|
"ai",
|
|
8253
9485
|
"coding-agent",
|
|
8254
|
-
"cli"
|
|
8255
|
-
"dashboard"
|
|
9486
|
+
"cli"
|
|
8256
9487
|
],
|
|
8257
9488
|
publishConfig: {
|
|
8258
9489
|
registry: "https://registry.npmjs.org",
|
|
@@ -8272,9 +9503,7 @@ var require_package = __commonJS((exports, module) => {
|
|
|
8272
9503
|
author: "Andrei Hasna <andrei@hasna.com>",
|
|
8273
9504
|
license: "Apache-2.0",
|
|
8274
9505
|
dependencies: {
|
|
8275
|
-
"@hasna/contracts": "0.14.1",
|
|
8276
9506
|
"@hasna/events": "^0.1.16",
|
|
8277
|
-
"@hasna/paths": "0.1.0",
|
|
8278
9507
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
8279
9508
|
chalk: "^5.4.1",
|
|
8280
9509
|
commander: "^13.1.0",
|
|
@@ -8285,7 +9514,8 @@ var require_package = __commonJS((exports, module) => {
|
|
|
8285
9514
|
zod: "^3.24.2"
|
|
8286
9515
|
},
|
|
8287
9516
|
devDependencies: {
|
|
8288
|
-
"@
|
|
9517
|
+
"@hasna/contracts": "1.0.2",
|
|
9518
|
+
"@types/bun": "1.3.14",
|
|
8289
9519
|
"@types/pg": "^8.11.11",
|
|
8290
9520
|
"@types/react": "^18.3.18",
|
|
8291
9521
|
"bun-types": "1.3.14",
|
|
@@ -8326,6 +9556,9 @@ function getPackageVersion() {
|
|
|
8326
9556
|
return cached;
|
|
8327
9557
|
}
|
|
8328
9558
|
|
|
9559
|
+
// src/mcp/index.ts
|
|
9560
|
+
init_config_store();
|
|
9561
|
+
|
|
8329
9562
|
// src/mcp/server.ts
|
|
8330
9563
|
init_config_store();
|
|
8331
9564
|
init_app_home();
|
|
@@ -8416,7 +9649,12 @@ function buildServer() {
|
|
|
8416
9649
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: LEAN_TOOLS }));
|
|
8417
9650
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
8418
9651
|
const { name, arguments: args = {} } = req.params;
|
|
8419
|
-
|
|
9652
|
+
let store;
|
|
9653
|
+
try {
|
|
9654
|
+
store = resolveConfigStore();
|
|
9655
|
+
} catch (e) {
|
|
9656
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
9657
|
+
}
|
|
8420
9658
|
try {
|
|
8421
9659
|
switch (name) {
|
|
8422
9660
|
case "list_configs": {
|
|
@@ -8801,6 +10039,12 @@ Options:
|
|
|
8801
10039
|
await proc.exited;
|
|
8802
10040
|
process.exit(0);
|
|
8803
10041
|
}
|
|
10042
|
+
try {
|
|
10043
|
+
resolveConfigStore();
|
|
10044
|
+
} catch (err2) {
|
|
10045
|
+
console.error(err2 instanceof Error ? err2.message : String(err2));
|
|
10046
|
+
process.exit(1);
|
|
10047
|
+
}
|
|
8804
10048
|
if (isStdioMode(argv)) {
|
|
8805
10049
|
const server = buildServer();
|
|
8806
10050
|
const transport = new StdioServerTransport;
|