@hasna/instructions 0.5.5 → 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 +1910 -561
- 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 +2702 -547
- 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-apply.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 +57 -0
- package/dist/lib/session-render-state.d.ts.map +1 -0
- package/dist/lib/session-render-state.test.d.ts +2 -0
- package/dist/lib/session-render-state.test.d.ts.map +1 -0
- 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 +1442 -192
- 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) {
|
|
@@ -5250,8 +5306,11 @@ var init_session_render_contract = __esm(() => {
|
|
|
5250
5306
|
SESSION_RENDER_SNAPSHOT_RELATIVE_DIR = `${SESSION_RENDER_MANAGED_NAMESPACE}/session-render-snapshots`;
|
|
5251
5307
|
});
|
|
5252
5308
|
|
|
5309
|
+
// src/lib/session-render-state.ts
|
|
5310
|
+
var init_session_render_state = () => {};
|
|
5311
|
+
|
|
5253
5312
|
// src/lib/project-context.ts
|
|
5254
|
-
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";
|
|
5255
5314
|
function revisionKey(value) {
|
|
5256
5315
|
const sequence = value.match(/^(?:rev-)?([0-9]+)$/);
|
|
5257
5316
|
if (sequence)
|
|
@@ -5289,6 +5348,7 @@ var init_project_context = __esm(() => {
|
|
|
5289
5348
|
init_zod();
|
|
5290
5349
|
init_redact();
|
|
5291
5350
|
init_session_render_contract();
|
|
5351
|
+
init_session_render_state();
|
|
5292
5352
|
PROJECT_CONTEXT_SUPPORTED_SCHEMAS = [PROJECT_CONTEXT_SCHEMA, PROJECT_CONTEXT_SCHEMA_V2];
|
|
5293
5353
|
projectContextSchema = exports_external.enum(PROJECT_CONTEXT_SUPPORTED_SCHEMAS);
|
|
5294
5354
|
PROJECT_CONTEXT_MAX_INPUT_BYTES = 8 * 1024;
|
|
@@ -5813,13 +5873,13 @@ var init_asset_plan = __esm(() => {
|
|
|
5813
5873
|
|
|
5814
5874
|
// src/lib/cursor-authority.ts
|
|
5815
5875
|
import { createHash } from "crypto";
|
|
5816
|
-
import { homedir as
|
|
5817
|
-
import { join as
|
|
5876
|
+
import { homedir as homedir3 } from "os";
|
|
5877
|
+
import { join as join6, resolve as resolve4 } from "path";
|
|
5818
5878
|
function sha256(content) {
|
|
5819
5879
|
return createHash("sha256").update(content).digest("hex");
|
|
5820
5880
|
}
|
|
5821
5881
|
function homeDir2() {
|
|
5822
|
-
return process.env["HOME"] ||
|
|
5882
|
+
return process.env["HOME"] || homedir3();
|
|
5823
5883
|
}
|
|
5824
5884
|
function markerPayload(content, markerLine, markerIndex) {
|
|
5825
5885
|
const index = markerIndex ?? content.indexOf(markerLine);
|
|
@@ -5828,7 +5888,7 @@ function markerPayload(content, markerLine, markerIndex) {
|
|
|
5828
5888
|
return content.slice(0, index) + content.slice(index + markerLine.length).replace(/^\n/, "");
|
|
5829
5889
|
}
|
|
5830
5890
|
function isCursorGlobalAuthorityPath(path) {
|
|
5831
|
-
return resolve4(path) === resolve4(
|
|
5891
|
+
return resolve4(path) === resolve4(join6(homeDir2(), CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH));
|
|
5832
5892
|
}
|
|
5833
5893
|
function stampCursorGlobalAuthorityMarker(content) {
|
|
5834
5894
|
const existing = content.match(CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN);
|
|
@@ -6500,6 +6560,1222 @@ var init_machines = __esm(() => {
|
|
|
6500
6560
|
init_database();
|
|
6501
6561
|
});
|
|
6502
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
|
+
|
|
6503
7779
|
// src/data/config-store.ts
|
|
6504
7780
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6505
7781
|
function parseBoundedPagePayload(value, label) {
|
|
@@ -6507,7 +7783,7 @@ function parseBoundedPagePayload(value, label) {
|
|
|
6507
7783
|
const consumed = Number(page?.cursor) + (page?.items?.length ?? 0);
|
|
6508
7784
|
const complete = Boolean(page && Number.isSafeInteger(page.total) && consumed >= Number(page.total));
|
|
6509
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)) {
|
|
6510
|
-
throw new
|
|
7786
|
+
throw new Error(`${label} returned an invalid or truncated bounded-read envelope`);
|
|
6511
7787
|
}
|
|
6512
7788
|
return {
|
|
6513
7789
|
...page,
|
|
@@ -6522,27 +7798,20 @@ function parseBoundedOrLegacyPage(value, legacyItems, options, label) {
|
|
|
6522
7798
|
}
|
|
6523
7799
|
}
|
|
6524
7800
|
if (!Array.isArray(legacyItems)) {
|
|
6525
|
-
throw new
|
|
7801
|
+
throw new Error(`${label} returned neither a bounded envelope nor a complete legacy array`);
|
|
6526
7802
|
}
|
|
6527
7803
|
const normalized = normalizeBoundedReadOptions(options);
|
|
6528
7804
|
const page = boundedReadPage(legacyItems.slice(normalized.cursor, normalized.cursor + normalized.limit), legacyItems.length, normalized);
|
|
6529
7805
|
return { ...page, source_bounded: false };
|
|
6530
7806
|
}
|
|
6531
|
-
function
|
|
6532
|
-
|
|
6533
|
-
const apiUrl = env[API_URL_ENV]?.trim();
|
|
6534
|
-
const apiKey = env[API_KEY_ENV]?.trim();
|
|
6535
|
-
if (!apiUrl && !apiKey)
|
|
6536
|
-
return null;
|
|
6537
|
-
if (!apiUrl || !apiKey) {
|
|
6538
|
-
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.`);
|
|
6539
|
-
}
|
|
6540
|
-
return { apiUrl, apiKey };
|
|
7807
|
+
function isNotFoundHttpError2(err) {
|
|
7808
|
+
return typeof err === "object" && err !== null && err.name === "HasnaHttpError" && err.status === 404;
|
|
6541
7809
|
}
|
|
6542
7810
|
|
|
6543
7811
|
class LocalConfigStore {
|
|
6544
7812
|
db;
|
|
6545
7813
|
mode = "local";
|
|
7814
|
+
v1BaseUrl = null;
|
|
6546
7815
|
constructor(db) {
|
|
6547
7816
|
this.db = db;
|
|
6548
7817
|
}
|
|
@@ -6671,50 +7940,22 @@ class LocalConfigStore {
|
|
|
6671
7940
|
|
|
6672
7941
|
class CloudConfigStore {
|
|
6673
7942
|
mode = "api";
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
this.
|
|
6679
|
-
this.apiKey = config.apiKey;
|
|
6680
|
-
this.timeoutMs = config.timeoutMs ?? 30000;
|
|
7943
|
+
v1BaseUrl;
|
|
7944
|
+
client;
|
|
7945
|
+
constructor(client) {
|
|
7946
|
+
this.client = client;
|
|
7947
|
+
this.v1BaseUrl = client.baseUrl;
|
|
6681
7948
|
}
|
|
6682
7949
|
async request(method, path, body, opts = {}) {
|
|
6683
|
-
const controller = new AbortController;
|
|
6684
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
6685
|
-
const headers = {
|
|
6686
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
6687
|
-
Accept: "application/json"
|
|
6688
|
-
};
|
|
6689
|
-
if (body !== undefined)
|
|
6690
|
-
headers["Content-Type"] = "application/json";
|
|
6691
|
-
if (opts.idempotent)
|
|
6692
|
-
headers["Idempotency-Key"] = randomUUID2();
|
|
6693
7950
|
try {
|
|
6694
|
-
const
|
|
6695
|
-
|
|
6696
|
-
headers,
|
|
6697
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
6698
|
-
signal: controller.signal
|
|
7951
|
+
const data = await this.client.transport.request(method, path, body, {
|
|
7952
|
+
...opts.idempotent ? { idempotencyKey: randomUUID2() } : {}
|
|
6699
7953
|
});
|
|
6700
|
-
|
|
7954
|
+
return { status: 200, data };
|
|
7955
|
+
} catch (err) {
|
|
7956
|
+
if (opts.allow404 && isNotFoundHttpError2(err))
|
|
6701
7957
|
return { status: 404, data: null };
|
|
6702
|
-
|
|
6703
|
-
let parsed = null;
|
|
6704
|
-
if (text) {
|
|
6705
|
-
try {
|
|
6706
|
-
parsed = JSON.parse(text);
|
|
6707
|
-
} catch {
|
|
6708
|
-
parsed = text;
|
|
6709
|
-
}
|
|
6710
|
-
}
|
|
6711
|
-
if (!res.ok) {
|
|
6712
|
-
const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : undefined) ?? `HTTP ${res.status} on ${method} ${path}`;
|
|
6713
|
-
throw new CloudHttpError(res.status, message, parsed);
|
|
6714
|
-
}
|
|
6715
|
-
return { status: res.status, data: parsed };
|
|
6716
|
-
} finally {
|
|
6717
|
-
clearTimeout(timer);
|
|
7958
|
+
throw err;
|
|
6718
7959
|
}
|
|
6719
7960
|
}
|
|
6720
7961
|
async listConfigs(filter = {}) {
|
|
@@ -6870,7 +8111,7 @@ class CloudConfigStore {
|
|
|
6870
8111
|
if (first.status !== 404) {
|
|
6871
8112
|
if (isUsable(first.data))
|
|
6872
8113
|
return first;
|
|
6873
|
-
throw new
|
|
8114
|
+
throw new Error(`Cloud /v1 returned an invalid profile response for ${pathForId(idOrSlug)}`);
|
|
6874
8115
|
}
|
|
6875
8116
|
const profiles = await this.listProfiles();
|
|
6876
8117
|
const profile = profiles.find((candidate) => candidate.id === idOrSlug || candidate.slug === idOrSlug);
|
|
@@ -6883,7 +8124,7 @@ class CloudConfigStore {
|
|
|
6883
8124
|
if (response.status !== 404) {
|
|
6884
8125
|
if (isUsable(response.data))
|
|
6885
8126
|
return response;
|
|
6886
|
-
throw new
|
|
8127
|
+
throw new Error(`Cloud /v1 returned an invalid profile response for ${pathForId(candidate)}`);
|
|
6887
8128
|
}
|
|
6888
8129
|
}
|
|
6889
8130
|
return { status: 404, data: null };
|
|
@@ -6966,7 +8207,7 @@ class CloudConfigStore {
|
|
|
6966
8207
|
}
|
|
6967
8208
|
if (data && "complete" in data) {
|
|
6968
8209
|
if (data.complete !== true || data.truncated !== false) {
|
|
6969
|
-
throw new
|
|
8210
|
+
throw new Error("Cloud /v1 profile resolve returned an incomplete or truncated read");
|
|
6970
8211
|
}
|
|
6971
8212
|
return { ...data, source_bounded: data.source_bounded ?? true };
|
|
6972
8213
|
}
|
|
@@ -6982,7 +8223,7 @@ class CloudConfigStore {
|
|
|
6982
8223
|
};
|
|
6983
8224
|
}
|
|
6984
8225
|
{
|
|
6985
|
-
throw new
|
|
8226
|
+
throw new Error("Cloud /v1 profile resolve returned an incomplete or truncated read");
|
|
6986
8227
|
}
|
|
6987
8228
|
}
|
|
6988
8229
|
async registerMachine(hostname2, os, arch2) {
|
|
@@ -7005,14 +8246,18 @@ class CloudConfigStore {
|
|
|
7005
8246
|
});
|
|
7006
8247
|
}
|
|
7007
8248
|
async reset() {
|
|
7008
|
-
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.");
|
|
7009
8250
|
}
|
|
7010
8251
|
}
|
|
7011
|
-
function resolveConfigStore(env = process.env) {
|
|
7012
|
-
|
|
7013
|
-
|
|
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);
|
|
7014
8260
|
}
|
|
7015
|
-
var CloudHttpError, API_URL_ENV = "HASNA_INSTRUCTIONS_API_URL", API_KEY_ENV = "HASNA_INSTRUCTIONS_API_KEY";
|
|
7016
8261
|
var init_config_store = __esm(() => {
|
|
7017
8262
|
init_configs();
|
|
7018
8263
|
init_profiles();
|
|
@@ -7022,21 +8267,12 @@ var init_config_store = __esm(() => {
|
|
|
7022
8267
|
init_types();
|
|
7023
8268
|
init_bounded_read();
|
|
7024
8269
|
init_instruction_graph();
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
status;
|
|
7028
|
-
body;
|
|
7029
|
-
constructor(status, message, body) {
|
|
7030
|
-
super(message);
|
|
7031
|
-
this.status = status;
|
|
7032
|
-
this.body = body;
|
|
7033
|
-
this.name = "CloudHttpError";
|
|
7034
|
-
}
|
|
7035
|
-
};
|
|
8270
|
+
init_transport_resolver();
|
|
8271
|
+
init_local_opt_in();
|
|
7036
8272
|
});
|
|
7037
8273
|
|
|
7038
8274
|
// src/lib/session-render-ownership.ts
|
|
7039
|
-
import { existsSync as existsSync5, readFileSync as
|
|
8275
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, statSync } from "fs";
|
|
7040
8276
|
import { dirname as dirname3, join as join8, parse as parse2, relative as relative2, sep } from "path";
|
|
7041
8277
|
function toSegments(absolutePath2) {
|
|
7042
8278
|
return absolutePath2.replaceAll("\\", "/").split("/").filter(Boolean);
|
|
@@ -7068,7 +8304,7 @@ function readManifestRelativePaths(manifestPath) {
|
|
|
7068
8304
|
}
|
|
7069
8305
|
let manifest;
|
|
7070
8306
|
try {
|
|
7071
|
-
manifest = JSON.parse(
|
|
8307
|
+
manifest = JSON.parse(readFileSync3(manifestPath, "utf-8"));
|
|
7072
8308
|
} catch {
|
|
7073
8309
|
return null;
|
|
7074
8310
|
}
|
|
@@ -7120,11 +8356,11 @@ __export(exports_apply, {
|
|
|
7120
8356
|
applyConfigs: () => applyConfigs,
|
|
7121
8357
|
applyConfig: () => applyConfig
|
|
7122
8358
|
});
|
|
7123
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as
|
|
8359
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync4, realpathSync, writeFileSync } from "fs";
|
|
7124
8360
|
import { basename as basename3, dirname as dirname4, join as join9, resolve as resolve5 } from "path";
|
|
7125
|
-
import { homedir as
|
|
8361
|
+
import { homedir as homedir4 } from "os";
|
|
7126
8362
|
function getConfigHome() {
|
|
7127
|
-
return process.env["CONFIGS_HOME"] || process.env["HOME"] ||
|
|
8363
|
+
return process.env["CONFIGS_HOME"] || process.env["HOME"] || homedir4();
|
|
7128
8364
|
}
|
|
7129
8365
|
function expandPath(p) {
|
|
7130
8366
|
if (p.startsWith("~/")) {
|
|
@@ -7171,7 +8407,7 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
|
|
|
7171
8407
|
}
|
|
7172
8408
|
const path = expandPath(renderedTargetPath);
|
|
7173
8409
|
const renderedForTarget = isCursorGlobalAuthorityPath(path) ? stampCursorGlobalAuthorityMarker(renderedContent) : renderedContent;
|
|
7174
|
-
const previousContent = existsSync6(path) ?
|
|
8410
|
+
const previousContent = existsSync6(path) ? readFileSync4(path, "utf-8") : null;
|
|
7175
8411
|
const changed = previousContent !== renderedForTarget;
|
|
7176
8412
|
if (!opts.dryRun) {
|
|
7177
8413
|
const dir = dirname4(path);
|
|
@@ -7211,7 +8447,7 @@ function wouldDestroyACredential(targetPath, renderedContent, format) {
|
|
|
7211
8447
|
const path = expandPath(targetPath);
|
|
7212
8448
|
if (!existsSync6(path))
|
|
7213
8449
|
return [];
|
|
7214
|
-
current =
|
|
8450
|
+
current = readFileSync4(path, "utf-8");
|
|
7215
8451
|
} catch {
|
|
7216
8452
|
return secretTokens;
|
|
7217
8453
|
}
|
|
@@ -7568,7 +8804,7 @@ __export(exports_sync, {
|
|
|
7568
8804
|
KNOWN_CONFIGS: () => KNOWN_CONFIGS,
|
|
7569
8805
|
CLAUDE_PROMPT_OUTPUTS: () => CLAUDE_PROMPT_OUTPUTS
|
|
7570
8806
|
});
|
|
7571
|
-
import { existsSync as existsSync7, readdirSync, readFileSync as
|
|
8807
|
+
import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync5 } from "fs";
|
|
7572
8808
|
import { basename as basename4, extname as extname2, join as join10 } from "path";
|
|
7573
8809
|
function claudeRuleOutputs(fileName) {
|
|
7574
8810
|
const stem = basename4(fileName, extname2(fileName));
|
|
@@ -7637,7 +8873,7 @@ async function syncProject(opts) {
|
|
|
7637
8873
|
if (!existsSync7(abs))
|
|
7638
8874
|
continue;
|
|
7639
8875
|
try {
|
|
7640
|
-
const rawContent =
|
|
8876
|
+
const rawContent = readFileSync5(abs, "utf-8");
|
|
7641
8877
|
if (rawContent.length > 500000) {
|
|
7642
8878
|
result.skipped.push(pf.file);
|
|
7643
8879
|
continue;
|
|
@@ -7679,7 +8915,7 @@ async function syncProject(opts) {
|
|
|
7679
8915
|
const mdFiles = readdirSync(ruleDir.dir).filter((f) => f.endsWith(".md") || f.endsWith(".mdc"));
|
|
7680
8916
|
for (const f of mdFiles) {
|
|
7681
8917
|
const abs = join10(ruleDir.dir, f);
|
|
7682
|
-
const raw =
|
|
8918
|
+
const raw = readFileSync5(abs, "utf-8");
|
|
7683
8919
|
const redacted = redactContent(raw, "markdown");
|
|
7684
8920
|
const machineAware = templateizeMachineContent(redacted.content, machine);
|
|
7685
8921
|
const content = machineAware.content;
|
|
@@ -7731,7 +8967,7 @@ async function syncKnown(opts = {}) {
|
|
|
7731
8967
|
result.skipped.push(`${targetPath} (generated output)`);
|
|
7732
8968
|
continue;
|
|
7733
8969
|
}
|
|
7734
|
-
const raw =
|
|
8970
|
+
const raw = readFileSync5(abs2, "utf-8");
|
|
7735
8971
|
const redacted = redactContent(raw, "markdown");
|
|
7736
8972
|
const machineAware = templateizeMachineContent(redacted.content, machine);
|
|
7737
8973
|
const content = machineAware.content;
|
|
@@ -7764,7 +9000,7 @@ async function syncKnown(opts = {}) {
|
|
|
7764
9000
|
continue;
|
|
7765
9001
|
}
|
|
7766
9002
|
try {
|
|
7767
|
-
const rawContent = normalizeKnownConfigSource(known,
|
|
9003
|
+
const rawContent = normalizeKnownConfigSource(known, readFileSync5(abs, "utf-8"));
|
|
7768
9004
|
if (rawContent.length > 500000) {
|
|
7769
9005
|
result.skipped.push(known.path + " (too large)");
|
|
7770
9006
|
continue;
|
|
@@ -7867,7 +9103,7 @@ function buildDiff(expectedContent, targetPath, storedFormat, showSecrets) {
|
|
|
7867
9103
|
const path = expandPath(targetPath);
|
|
7868
9104
|
if (!existsSync7(path))
|
|
7869
9105
|
return `(file not found on disk: ${path})`;
|
|
7870
|
-
const diskContent =
|
|
9106
|
+
const diskContent = readFileSync5(path, "utf-8");
|
|
7871
9107
|
if (diskContent === expectedContent)
|
|
7872
9108
|
return "(no diff \u2014 identical)";
|
|
7873
9109
|
const format = redactFormatForTarget(targetPath, storedFormat);
|
|
@@ -8095,9 +9331,9 @@ var init_sync = __esm(() => {
|
|
|
8095
9331
|
});
|
|
8096
9332
|
|
|
8097
9333
|
// src/lib/sync-dir.ts
|
|
8098
|
-
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";
|
|
8099
9335
|
import { join as join11, relative as relative3 } from "path";
|
|
8100
|
-
import { homedir as
|
|
9336
|
+
import { homedir as homedir5 } from "os";
|
|
8101
9337
|
function shouldSkip(p) {
|
|
8102
9338
|
return SKIP.some((s) => p.includes(s));
|
|
8103
9339
|
}
|
|
@@ -8108,7 +9344,7 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
8108
9344
|
return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
|
|
8109
9345
|
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync2(absDir).map((f) => join11(absDir, f)).filter((f) => statSync2(f).isFile());
|
|
8110
9346
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
8111
|
-
const home =
|
|
9347
|
+
const home = homedir5();
|
|
8112
9348
|
const allConfigs = await store.listConfigs();
|
|
8113
9349
|
for (const file of files) {
|
|
8114
9350
|
if (shouldSkip(file)) {
|
|
@@ -8116,7 +9352,7 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
8116
9352
|
continue;
|
|
8117
9353
|
}
|
|
8118
9354
|
try {
|
|
8119
|
-
const content =
|
|
9355
|
+
const content = readFileSync6(file, "utf-8");
|
|
8120
9356
|
if (content.length > 500000) {
|
|
8121
9357
|
result.skipped.push(file + " (too large)");
|
|
8122
9358
|
continue;
|
|
@@ -8143,7 +9379,7 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
8143
9379
|
}
|
|
8144
9380
|
async function syncToDir(dir, opts = {}) {
|
|
8145
9381
|
const store = opts.store ?? resolveConfigStore();
|
|
8146
|
-
const home =
|
|
9382
|
+
const home = homedir5();
|
|
8147
9383
|
const absDir = expandPath(dir);
|
|
8148
9384
|
const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
|
|
8149
9385
|
const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
|
|
@@ -8190,8 +9426,8 @@ var init_sync_dir = __esm(() => {
|
|
|
8190
9426
|
var require_package = __commonJS((exports, module) => {
|
|
8191
9427
|
module.exports = {
|
|
8192
9428
|
name: "@hasna/instructions",
|
|
8193
|
-
version: "0.
|
|
8194
|
-
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.",
|
|
8195
9431
|
type: "module",
|
|
8196
9432
|
main: "dist/index.js",
|
|
8197
9433
|
types: "dist/index.d.ts",
|
|
@@ -8206,25 +9442,27 @@ var require_package = __commonJS((exports, module) => {
|
|
|
8206
9442
|
".": {
|
|
8207
9443
|
types: "./dist/index.d.ts",
|
|
8208
9444
|
import: "./dist/index.js"
|
|
9445
|
+
},
|
|
9446
|
+
"./sdk": {
|
|
9447
|
+
types: "./dist/sdk/index.d.ts",
|
|
9448
|
+
import: "./dist/sdk/index.js"
|
|
8209
9449
|
}
|
|
8210
9450
|
},
|
|
8211
9451
|
files: [
|
|
8212
9452
|
"dist",
|
|
8213
9453
|
"assets/skills/inbox/SKILL.md",
|
|
8214
|
-
"dashboard/dist",
|
|
8215
9454
|
"LICENSE",
|
|
8216
9455
|
"README.md"
|
|
8217
9456
|
],
|
|
8218
9457
|
scripts: {
|
|
8219
9458
|
clean: "rm -rf dist",
|
|
8220
|
-
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",
|
|
8221
|
-
"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",
|
|
8222
|
-
"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",
|
|
8223
9461
|
migrate: "bun run src/server/index.ts migrate",
|
|
8224
9462
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
8225
9463
|
"kit:check": "bunx @hasna/contracts vendor-kit --check",
|
|
8226
9464
|
typecheck: "tsc --noEmit",
|
|
8227
|
-
test: "env -u HASNA_INSTRUCTIONS_API_URL -u HASNA_INSTRUCTIONS_API_KEY 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",
|
|
8228
9466
|
"check:package-secrets": "bun run src/cli/index.tsx package-manager-scan --fail-on-findings .",
|
|
8229
9467
|
"dev:cli": "bun run src/cli/index.tsx",
|
|
8230
9468
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
@@ -8245,8 +9483,7 @@ var require_package = __commonJS((exports, module) => {
|
|
|
8245
9483
|
"mcp",
|
|
8246
9484
|
"ai",
|
|
8247
9485
|
"coding-agent",
|
|
8248
|
-
"cli"
|
|
8249
|
-
"dashboard"
|
|
9486
|
+
"cli"
|
|
8250
9487
|
],
|
|
8251
9488
|
publishConfig: {
|
|
8252
9489
|
registry: "https://registry.npmjs.org",
|
|
@@ -8266,9 +9503,7 @@ var require_package = __commonJS((exports, module) => {
|
|
|
8266
9503
|
author: "Andrei Hasna <andrei@hasna.com>",
|
|
8267
9504
|
license: "Apache-2.0",
|
|
8268
9505
|
dependencies: {
|
|
8269
|
-
"@hasna/contracts": "0.14.1",
|
|
8270
9506
|
"@hasna/events": "^0.1.16",
|
|
8271
|
-
"@hasna/paths": "0.1.0",
|
|
8272
9507
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
8273
9508
|
chalk: "^5.4.1",
|
|
8274
9509
|
commander: "^13.1.0",
|
|
@@ -8279,7 +9514,8 @@ var require_package = __commonJS((exports, module) => {
|
|
|
8279
9514
|
zod: "^3.24.2"
|
|
8280
9515
|
},
|
|
8281
9516
|
devDependencies: {
|
|
8282
|
-
"@
|
|
9517
|
+
"@hasna/contracts": "1.0.2",
|
|
9518
|
+
"@types/bun": "1.3.14",
|
|
8283
9519
|
"@types/pg": "^8.11.11",
|
|
8284
9520
|
"@types/react": "^18.3.18",
|
|
8285
9521
|
"bun-types": "1.3.14",
|
|
@@ -8320,6 +9556,9 @@ function getPackageVersion() {
|
|
|
8320
9556
|
return cached;
|
|
8321
9557
|
}
|
|
8322
9558
|
|
|
9559
|
+
// src/mcp/index.ts
|
|
9560
|
+
init_config_store();
|
|
9561
|
+
|
|
8323
9562
|
// src/mcp/server.ts
|
|
8324
9563
|
init_config_store();
|
|
8325
9564
|
init_app_home();
|
|
@@ -8410,7 +9649,12 @@ function buildServer() {
|
|
|
8410
9649
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: LEAN_TOOLS }));
|
|
8411
9650
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
8412
9651
|
const { name, arguments: args = {} } = req.params;
|
|
8413
|
-
|
|
9652
|
+
let store;
|
|
9653
|
+
try {
|
|
9654
|
+
store = resolveConfigStore();
|
|
9655
|
+
} catch (e) {
|
|
9656
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
9657
|
+
}
|
|
8414
9658
|
try {
|
|
8415
9659
|
switch (name) {
|
|
8416
9660
|
case "list_configs": {
|
|
@@ -8795,6 +10039,12 @@ Options:
|
|
|
8795
10039
|
await proc.exited;
|
|
8796
10040
|
process.exit(0);
|
|
8797
10041
|
}
|
|
10042
|
+
try {
|
|
10043
|
+
resolveConfigStore();
|
|
10044
|
+
} catch (err2) {
|
|
10045
|
+
console.error(err2 instanceof Error ? err2.message : String(err2));
|
|
10046
|
+
process.exit(1);
|
|
10047
|
+
}
|
|
8798
10048
|
if (isStdioMode(argv)) {
|
|
8799
10049
|
const server = buildServer();
|
|
8800
10050
|
const transport = new StdioServerTransport;
|