@hasna/instructions 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -10
- package/dist/chunks/{apply-ynfn1av7.js → apply-c3sd4agz.js} +5 -3
- package/dist/chunks/{apply-sfzja1f3.js → apply-pnn7a3y7.js} +6 -3
- package/dist/chunks/database-wstmr56z.js +23 -0
- package/dist/chunks/{index-pkscjnj5.js → index-3djthcm6.js} +0 -1
- package/dist/chunks/index-5d7zdftc.js +186 -0
- package/dist/chunks/{index-1hpwskwd.js → index-6jncnnw7.js} +318 -329
- package/dist/chunks/index-8rb7ng2y.js +148 -0
- package/dist/chunks/{index-v7dkb384.js → index-aj66f6xr.js} +243 -100
- package/dist/chunks/index-fpeyzn3n.js +186 -0
- package/dist/chunks/{index-9p9gn7gh.js → index-jg8ptcwh.js} +5 -7
- package/dist/chunks/index-mvckkf15.js +363 -0
- package/dist/chunks/index-n3fjspr0.js +119 -0
- package/dist/chunks/{index-85n5wnd8.js → index-pdpg8a44.js} +5 -7
- package/dist/chunks/{index-e9epbsvs.js → index-r7nfef2g.js} +15 -4
- package/dist/chunks/{index-axhwg61p.js → index-ypcef3a2.js} +15 -4
- package/dist/chunks/legacy-store-migration-z2m5yzyk.js +322 -0
- package/dist/chunks/{local-a9nen65z.js → local-0a9gja58.js} +142 -2
- package/dist/chunks/{local-0d8qrc92.js → local-969ye538.js} +153 -188
- package/dist/chunks/s3-backup-hxe3zpfz.js +240 -0
- package/dist/chunks/s3-config-hhght91k.js +14 -0
- package/dist/chunks/s3-object-store-n0s82swt.js +14 -0
- package/dist/chunks/{sync-vy1yjvs6.js → sync-q5hvwq6e.js} +4 -4
- package/dist/chunks/{sync-m46fc257.js → sync-xtkbm5br.js} +5 -4
- package/dist/chunks/{template-02wjvv05.js → template-fsfxpe4p.js} +1 -1
- package/dist/chunks/{template-ke972qqe.js → template-wgnzdn7h.js} +1 -1
- package/dist/cli/index.js +1236 -256
- package/dist/data/config-store.d.ts +10 -1
- package/dist/data/config-store.d.ts.map +1 -1
- package/dist/db/configs.d.ts +4 -1
- package/dist/db/configs.d.ts.map +1 -1
- package/dist/db/local.d.ts +1 -1
- package/dist/db/local.d.ts.map +1 -1
- package/dist/index.js +1086 -215
- package/dist/lib/apply.d.ts +7 -0
- package/dist/lib/apply.d.ts.map +1 -1
- package/dist/lib/compact-output.d.ts +3 -18
- package/dist/lib/compact-output.d.ts.map +1 -1
- package/dist/lib/export.d.ts +22 -3
- package/dist/lib/export.d.ts.map +1 -1
- package/dist/lib/import.d.ts +18 -0
- package/dist/lib/import.d.ts.map +1 -1
- package/dist/lib/legacy-store-migration.d.ts +39 -0
- package/dist/lib/legacy-store-migration.d.ts.map +1 -0
- package/dist/lib/managed-skill-runtimes.d.ts +1 -0
- package/dist/lib/managed-skill-runtimes.d.ts.map +1 -1
- package/dist/lib/sync-dir.d.ts.map +1 -1
- package/dist/mcp/index.js +41 -30
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +229 -10
- package/dist/sdk/resolve.d.ts +1 -1
- package/dist/sdk/resolve.d.ts.map +1 -1
- package/dist/sdk/v1-client.d.ts +58 -0
- package/dist/sdk/v1-client.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +242 -21
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.d.ts +13 -5
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1261 -427
- package/dist/server/openapi.d.ts +1379 -93
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts +25 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/cloud-store.d.ts +45 -2
- package/dist/storage/cloud-store.d.ts.map +1 -1
- package/dist/storage/index.d.ts +7 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +744 -0
- package/dist/storage/s3-backup.d.ts +76 -0
- package/dist/storage/s3-backup.d.ts.map +1 -0
- package/dist/storage/s3-config.d.ts +46 -0
- package/dist/storage/s3-config.d.ts.map +1 -0
- package/dist/storage/s3-object-store.d.ts +70 -0
- package/dist/storage/s3-object-store.d.ts.map +1 -0
- package/dist/types/index.d.ts +98 -3
- package/dist/types/index.d.ts.map +1 -1
- package/hasna.contract.json +110 -0
- package/migrations/0001_instructions.sql +63 -0
- package/migrations/0002_api_keys.sql +20 -0
- package/migrations/0003_idempotency_receipts.sql +24 -0
- package/package.json +24 -13
- package/assets/skills/inbox/SKILL.md +0 -86
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/lib/raw-store-root.ts
|
|
3
|
+
import { resolve as resolve2 } from "path";
|
|
4
|
+
|
|
5
|
+
// src/lib/app-home.ts
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { join, resolve } from "path";
|
|
9
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
10
|
+
import { join as pathsResolverJoin } from "path";
|
|
11
|
+
var PATHS_RESOLVER_KIND_ENV = {
|
|
12
|
+
config: "HASNA_CONFIG_HOME",
|
|
13
|
+
data: "HASNA_DATA_HOME",
|
|
14
|
+
state: "HASNA_STATE_HOME",
|
|
15
|
+
cache: "HASNA_CACHE_HOME"
|
|
16
|
+
};
|
|
17
|
+
var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
18
|
+
function pathsResolverAssertApp(app) {
|
|
19
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
20
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
21
|
+
}
|
|
22
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
23
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function pathsResolverAssertKind(kind) {
|
|
27
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
28
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function pathsResolverBaseDir(kind, options) {
|
|
32
|
+
pathsResolverAssertKind(kind);
|
|
33
|
+
const env = options.env ?? process.env;
|
|
34
|
+
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
35
|
+
if (typeof override === "string" && override.length > 0)
|
|
36
|
+
return override;
|
|
37
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
38
|
+
const platform = options.platform ?? process.platform;
|
|
39
|
+
if (platform === "darwin") {
|
|
40
|
+
switch (kind) {
|
|
41
|
+
case "config":
|
|
42
|
+
case "data":
|
|
43
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
44
|
+
case "cache":
|
|
45
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
46
|
+
case "state":
|
|
47
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
switch (kind) {
|
|
51
|
+
case "config":
|
|
52
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
53
|
+
case "data":
|
|
54
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
55
|
+
case "state":
|
|
56
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
57
|
+
case "cache":
|
|
58
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function pathsResolverResolve(kind, options) {
|
|
62
|
+
pathsResolverAssertApp(options.app);
|
|
63
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
64
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
65
|
+
}
|
|
66
|
+
function configDir(options) {
|
|
67
|
+
return pathsResolverResolve("config", options);
|
|
68
|
+
}
|
|
69
|
+
var HASNA_CONFIGS_HOME_ENV = "HASNA_CONFIGS_HOME";
|
|
70
|
+
function homeDir(env = process.env) {
|
|
71
|
+
return env["HOME"] || env["USERPROFILE"] || homedir();
|
|
72
|
+
}
|
|
73
|
+
function legacyStoreHome(env = process.env) {
|
|
74
|
+
return resolve(join(homeDir(env), ".hasna", "instructions"));
|
|
75
|
+
}
|
|
76
|
+
function resolverStoreHome(env = process.env) {
|
|
77
|
+
return configDir({ app: "configs", env, home: env.HOME || env.USERPROFILE || homedir() });
|
|
78
|
+
}
|
|
79
|
+
function adoptResolverStoreHome(resolved, env = process.env) {
|
|
80
|
+
const override = env.HASNA_CONFIG_HOME;
|
|
81
|
+
if (typeof override === "string" && override.trim().length > 0)
|
|
82
|
+
return true;
|
|
83
|
+
return existsSync(join(resolved, "instructions.db"));
|
|
84
|
+
}
|
|
85
|
+
function exactStoreHome(env = process.env) {
|
|
86
|
+
const v = env[HASNA_CONFIGS_HOME_ENV];
|
|
87
|
+
return v && v.trim() ? v.trim() : undefined;
|
|
88
|
+
}
|
|
89
|
+
function getConfigsStoreHome(env = process.env) {
|
|
90
|
+
const exact = exactStoreHome(env);
|
|
91
|
+
if (exact)
|
|
92
|
+
return resolve(exact);
|
|
93
|
+
const resolved = resolverStoreHome(env);
|
|
94
|
+
return adoptResolverStoreHome(resolved, env) ? resolve(resolved) : legacyStoreHome(env);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/lib/raw-store-root.ts
|
|
98
|
+
function getRawStoreRoot(env = process.env) {
|
|
99
|
+
return resolve2(getConfigsStoreHome(env));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ../../node_modules/.bun/@hasna+contracts@1.0.2+e8014c875821e0be/node_modules/@hasna/contracts/dist/client/transport.js
|
|
103
|
+
import { createRequire } from "module";
|
|
104
|
+
function envToken(name) {
|
|
105
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
106
|
+
}
|
|
107
|
+
function clientTransportEnvKeys(name) {
|
|
108
|
+
const envSegment = envToken(name);
|
|
109
|
+
return {
|
|
110
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
111
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function credentialOverrideEnvKey(name) {
|
|
115
|
+
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
116
|
+
}
|
|
117
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
|
|
118
|
+
function credentialPointerEnvKey(name) {
|
|
119
|
+
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
120
|
+
}
|
|
121
|
+
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
122
|
+
var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
123
|
+
var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
124
|
+
var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
125
|
+
var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
126
|
+
var requireSecretsSdk = createRequire(import.meta.url);
|
|
127
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
128
|
+
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
129
|
+
"host",
|
|
130
|
+
":authority",
|
|
131
|
+
"forwarded",
|
|
132
|
+
"x-forwarded-host",
|
|
133
|
+
"x-original-host"
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
// src/lib/local-opt-in.ts
|
|
137
|
+
var INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_INSTRUCTIONS_LOCAL"];
|
|
138
|
+
function instructionsLocalModeNotice() {
|
|
139
|
+
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.`;
|
|
140
|
+
}
|
|
141
|
+
function isInstructionsLocalOptIn(env = process.env) {
|
|
142
|
+
return INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() === "1");
|
|
143
|
+
}
|
|
144
|
+
function instructionsAuthorityEnvKeys() {
|
|
145
|
+
const keys = clientTransportEnvKeys("instructions");
|
|
146
|
+
return [
|
|
147
|
+
...keys.apiUrlKeys,
|
|
148
|
+
...keys.apiKeyKeys,
|
|
149
|
+
credentialOverrideEnvKey("instructions"),
|
|
150
|
+
credentialPointerEnvKey("instructions"),
|
|
151
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
function hasInstructionsEnvAuthorityIntent(env = process.env) {
|
|
155
|
+
return instructionsAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
|
|
156
|
+
}
|
|
157
|
+
function selectsInstructionsLocalStore(env = process.env) {
|
|
158
|
+
return !hasInstructionsEnvAuthorityIntent(env) && isInstructionsLocalOptIn(env);
|
|
159
|
+
}
|
|
160
|
+
function instructionsResolverEnv(env) {
|
|
161
|
+
const blanks = instructionsAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
162
|
+
if (blanks.length === 0)
|
|
163
|
+
return env;
|
|
164
|
+
const next = { ...env };
|
|
165
|
+
for (const key of blanks)
|
|
166
|
+
delete next[key];
|
|
167
|
+
return next;
|
|
168
|
+
}
|
|
169
|
+
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
170
|
+
function isAmbientInstructionsEnv(env) {
|
|
171
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
172
|
+
return true;
|
|
173
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
174
|
+
}
|
|
175
|
+
function instructionsResolverInputs(env, credentials = {}) {
|
|
176
|
+
const normalised = instructionsResolverEnv(env);
|
|
177
|
+
if (normalised === env)
|
|
178
|
+
return { env: normalised, credentials };
|
|
179
|
+
const keychain = { ...credentials.keychain };
|
|
180
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
181
|
+
keychain.enabled = isAmbientInstructionsEnv(env);
|
|
182
|
+
}
|
|
183
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export { getRawStoreRoot, clientTransportEnvKeys, INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS, instructionsLocalModeNotice, hasInstructionsEnvAuthorityIntent, selectsInstructionsLocalStore, instructionsResolverInputs };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
applyConfigsWithReport,
|
|
4
|
+
compactPathForConfigHome,
|
|
4
5
|
expandPath,
|
|
5
6
|
getConfigHome,
|
|
6
7
|
normalizeTargetPath
|
|
7
|
-
} from "./index-
|
|
8
|
+
} from "./index-ypcef3a2.js";
|
|
8
9
|
import {
|
|
9
10
|
applyTransform,
|
|
10
11
|
detectMachineContext,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
resolveConfigStore,
|
|
13
14
|
retiredOrUnsupportedAgentReason,
|
|
14
15
|
templateizeMachineContent
|
|
15
|
-
} from "./index-
|
|
16
|
+
} from "./index-aj66f6xr.js";
|
|
16
17
|
import {
|
|
17
18
|
isSecretVarName,
|
|
18
19
|
redactContent,
|
|
@@ -26,7 +27,6 @@ import { basename, extname, join as join2 } from "path";
|
|
|
26
27
|
// src/lib/sync-dir.ts
|
|
27
28
|
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
28
29
|
import { join, relative } from "path";
|
|
29
|
-
import { homedir } from "os";
|
|
30
30
|
var SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
|
|
31
31
|
function shouldSkip(p) {
|
|
32
32
|
return SKIP.some((s) => p.includes(s));
|
|
@@ -38,7 +38,6 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
38
38
|
return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
|
|
39
39
|
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join(absDir, f)).filter((f) => statSync(f).isFile());
|
|
40
40
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
41
|
-
const home = homedir();
|
|
42
41
|
const allConfigs = await store.listConfigs();
|
|
43
42
|
for (const file of files) {
|
|
44
43
|
if (shouldSkip(file)) {
|
|
@@ -51,7 +50,7 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
51
50
|
result.skipped.push(file + " (too large)");
|
|
52
51
|
continue;
|
|
53
52
|
}
|
|
54
|
-
const targetPath = file
|
|
53
|
+
const targetPath = compactPathForConfigHome(file);
|
|
55
54
|
const redacted = redactContent(content, redactFormatForTarget(targetPath, detectFormat(file)));
|
|
56
55
|
const existing = allConfigs.find((c) => c.target_path === targetPath);
|
|
57
56
|
if (!existing) {
|
|
@@ -73,9 +72,8 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
73
72
|
}
|
|
74
73
|
async function syncToDir(dir, opts = {}) {
|
|
75
74
|
const store = opts.store ?? resolveConfigStore();
|
|
76
|
-
const home = homedir();
|
|
77
75
|
const absDir = expandPath(dir);
|
|
78
|
-
const normalized = dir.startsWith("~/") ? dir : absDir
|
|
76
|
+
const normalized = dir.startsWith("~/") ? dir : compactPathForConfigHome(absDir);
|
|
79
77
|
const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
|
|
80
78
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
81
79
|
for (const config of configs) {
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/storage/s3-object-store.ts
|
|
3
|
+
import { createHash, createHmac } from "crypto";
|
|
4
|
+
var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
|
|
5
|
+
var VERSION_ID_PATTERN = /^[^\u0000-\u001f\u007f]{1,1024}$/;
|
|
6
|
+
var CONDITIONAL_CREATE_ATTEMPTS = 3;
|
|
7
|
+
var DEFAULT_EXISTS_DEADLINE_MS = 5000;
|
|
8
|
+
var DEFAULT_BYTES_DEADLINE_MS = 15000;
|
|
9
|
+
var DEFAULT_STAT_DEADLINE_MS = 5000;
|
|
10
|
+
var DEFAULT_VERSIONED_READ_DEADLINE_MS = 15000;
|
|
11
|
+
var DEFAULT_CONDITIONAL_CREATE_ATTEMPT_DEADLINE_MS = 15000;
|
|
12
|
+
var DEFAULT_EXISTENCE_RECONCILIATION_DEADLINE_MS = 5000;
|
|
13
|
+
var MAX_OPERATION_DEADLINE_MS = 60000;
|
|
14
|
+
var EMPTY_SHA256 = createHash("sha256").update("").digest("hex");
|
|
15
|
+
function resolveOperationDeadlineMs(value, fallback, label) {
|
|
16
|
+
const resolved = value ?? fallback;
|
|
17
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0 || resolved > MAX_OPERATION_DEADLINE_MS) {
|
|
18
|
+
throw new Error(`${label} must be an integer between 1 and ${MAX_OPERATION_DEADLINE_MS} milliseconds`);
|
|
19
|
+
}
|
|
20
|
+
return resolved;
|
|
21
|
+
}
|
|
22
|
+
async function runWithDeadline(operation, deadlineMs, onDeadline) {
|
|
23
|
+
let timer;
|
|
24
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
25
|
+
timer = setTimeout(() => {
|
|
26
|
+
try {
|
|
27
|
+
onDeadline?.();
|
|
28
|
+
} finally {
|
|
29
|
+
reject(new Error("Instructions S3 operation deadline exceeded"));
|
|
30
|
+
}
|
|
31
|
+
}, deadlineMs);
|
|
32
|
+
});
|
|
33
|
+
try {
|
|
34
|
+
return await Promise.race([Promise.resolve().then(operation), deadline]);
|
|
35
|
+
} finally {
|
|
36
|
+
if (timer !== undefined)
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function assertSafeInstructionsObjectKey(key) {
|
|
41
|
+
if (!key || key.length > 1024 || key.startsWith("/") || key.endsWith("/") || key.includes("\\") || CONTROL_CHARACTERS.test(key)) {
|
|
42
|
+
throw new Error("Instructions S3 object key is invalid");
|
|
43
|
+
}
|
|
44
|
+
for (const segment of key.split("/")) {
|
|
45
|
+
if (!segment || segment === "." || segment === "..") {
|
|
46
|
+
throw new Error("Instructions S3 object key contains an unsafe path segment");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function assertSafeInstructionsObjectVersionId(versionId) {
|
|
51
|
+
if (!VERSION_ID_PATTERN.test(versionId) || versionId.trim() !== versionId) {
|
|
52
|
+
throw new Error("Instructions S3 object version id is invalid");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function createInstructionsS3ObjectStore(config, createClient = (options) => new Bun.S3Client(options), requestFetch = fetch, deadlines = {}) {
|
|
56
|
+
const existsDeadlineMs = resolveOperationDeadlineMs(deadlines.existsDeadlineMs, DEFAULT_EXISTS_DEADLINE_MS, "Instructions S3 exists deadline");
|
|
57
|
+
const bytesDeadlineMs = resolveOperationDeadlineMs(deadlines.bytesDeadlineMs, DEFAULT_BYTES_DEADLINE_MS, "Instructions S3 bytes deadline");
|
|
58
|
+
const statDeadlineMs = resolveOperationDeadlineMs(deadlines.statDeadlineMs, DEFAULT_STAT_DEADLINE_MS, "Instructions S3 stat deadline");
|
|
59
|
+
const versionedReadDeadlineMs = resolveOperationDeadlineMs(deadlines.versionedReadDeadlineMs, DEFAULT_VERSIONED_READ_DEADLINE_MS, "Instructions S3 versioned read deadline");
|
|
60
|
+
const conditionalCreateAttemptDeadlineMs = resolveOperationDeadlineMs(deadlines.conditionalCreateAttemptDeadlineMs, DEFAULT_CONDITIONAL_CREATE_ATTEMPT_DEADLINE_MS, "Instructions S3 conditional create attempt deadline");
|
|
61
|
+
const existenceReconciliationDeadlineMs = resolveOperationDeadlineMs(deadlines.existenceReconciliationDeadlineMs, DEFAULT_EXISTENCE_RECONCILIATION_DEADLINE_MS, "Instructions S3 existence reconciliation deadline");
|
|
62
|
+
const client = createClient({
|
|
63
|
+
bucket: config.bucket,
|
|
64
|
+
region: config.region,
|
|
65
|
+
...config.endpoint ? { endpoint: config.endpoint } : {},
|
|
66
|
+
...config.credentials ? {
|
|
67
|
+
accessKeyId: config.credentials.accessKeyId,
|
|
68
|
+
secretAccessKey: config.credentials.secretAccessKey,
|
|
69
|
+
...config.credentials.sessionToken ? { sessionToken: config.credentials.sessionToken } : {}
|
|
70
|
+
} : {},
|
|
71
|
+
...config.forcePathStyle ? { virtualHostedStyle: false } : {}
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
async putIfAbsent(key, bytes, options) {
|
|
75
|
+
assertSafeInstructionsObjectKey(key);
|
|
76
|
+
let credentials;
|
|
77
|
+
try {
|
|
78
|
+
credentials = resolveRequestCredentials(config);
|
|
79
|
+
} catch {
|
|
80
|
+
throw new Error("Instructions S3 conditional object creation failed");
|
|
81
|
+
}
|
|
82
|
+
for (let attempt = 1;attempt <= CONDITIONAL_CREATE_ATTEMPTS; attempt += 1) {
|
|
83
|
+
let response;
|
|
84
|
+
const controller = new AbortController;
|
|
85
|
+
try {
|
|
86
|
+
const signed = signConditionalCreateRequest(config, key, bytes, options.contentType, credentials, new Date);
|
|
87
|
+
response = await runWithDeadline(() => requestFetch(signed.url, {
|
|
88
|
+
method: "PUT",
|
|
89
|
+
headers: signed.headers,
|
|
90
|
+
body: Uint8Array.from(bytes),
|
|
91
|
+
redirect: "error",
|
|
92
|
+
signal: controller.signal
|
|
93
|
+
}), conditionalCreateAttemptDeadlineMs, () => controller.abort());
|
|
94
|
+
} catch {
|
|
95
|
+
let exists = false;
|
|
96
|
+
try {
|
|
97
|
+
exists = await runWithDeadline(() => client.file(key).exists(), existenceReconciliationDeadlineMs);
|
|
98
|
+
} catch {}
|
|
99
|
+
if (exists)
|
|
100
|
+
return { status: "existing" };
|
|
101
|
+
if (attempt < CONDITIONAL_CREATE_ATTEMPTS)
|
|
102
|
+
continue;
|
|
103
|
+
throw new Error("Instructions S3 conditional object creation failed");
|
|
104
|
+
}
|
|
105
|
+
if (response.ok) {
|
|
106
|
+
const versionId = cleanResponseVersionId(response.headers.get("x-amz-version-id"));
|
|
107
|
+
response.body?.cancel().catch(() => {
|
|
108
|
+
return;
|
|
109
|
+
});
|
|
110
|
+
return { status: "created", ...versionId ? { versionId } : {} };
|
|
111
|
+
}
|
|
112
|
+
response.body?.cancel().catch(() => {
|
|
113
|
+
return;
|
|
114
|
+
});
|
|
115
|
+
if (response.status === 412)
|
|
116
|
+
return { status: "existing" };
|
|
117
|
+
if (response.status === 409 && attempt < CONDITIONAL_CREATE_ATTEMPTS)
|
|
118
|
+
continue;
|
|
119
|
+
if (response.status === 409) {
|
|
120
|
+
throw new Error("Instructions S3 conditional object creation remained conflicted");
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`Instructions S3 conditional object creation failed with status ${response.status}`);
|
|
123
|
+
}
|
|
124
|
+
throw new Error("Instructions S3 conditional object creation failed");
|
|
125
|
+
},
|
|
126
|
+
async get(key, options = {}) {
|
|
127
|
+
assertSafeInstructionsObjectKey(key);
|
|
128
|
+
if (options.versionId) {
|
|
129
|
+
return readExactVersion(config, key, options.versionId, "GET", requestFetch, versionedReadDeadlineMs).then((result) => result?.bytes);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const file = client.file(key);
|
|
133
|
+
const exists = await runWithDeadline(() => file.exists(), existsDeadlineMs);
|
|
134
|
+
if (!exists)
|
|
135
|
+
return;
|
|
136
|
+
return Uint8Array.from(await runWithDeadline(() => file.bytes(), bytesDeadlineMs));
|
|
137
|
+
} catch {
|
|
138
|
+
throw new Error("Instructions S3 get operation failed");
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
async head(key, options = {}) {
|
|
142
|
+
assertSafeInstructionsObjectKey(key);
|
|
143
|
+
if (options.versionId) {
|
|
144
|
+
return readExactVersion(config, key, options.versionId, "HEAD", requestFetch, versionedReadDeadlineMs).then((result) => result?.metadata);
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const file = client.file(key);
|
|
148
|
+
const exists = await runWithDeadline(() => file.exists(), existsDeadlineMs);
|
|
149
|
+
if (!exists)
|
|
150
|
+
return;
|
|
151
|
+
const stat = await runWithDeadline(() => file.stat(), statDeadlineMs);
|
|
152
|
+
return {
|
|
153
|
+
size: stat.size,
|
|
154
|
+
...stat.etag ? { etag: stat.etag } : {},
|
|
155
|
+
...stat.type ? { contentType: stat.type } : {},
|
|
156
|
+
...stat.lastModified ? { lastModified: stat.lastModified } : {}
|
|
157
|
+
};
|
|
158
|
+
} catch {
|
|
159
|
+
throw new Error("Instructions S3 head operation failed");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function readExactVersion(config, key, versionId, method, requestFetch, deadlineMs) {
|
|
165
|
+
assertSafeInstructionsObjectVersionId(versionId);
|
|
166
|
+
const credentials = resolveRequestCredentials(config);
|
|
167
|
+
const controller = new AbortController;
|
|
168
|
+
try {
|
|
169
|
+
const signed = signVersionedRequest(config, key, versionId, method, credentials, new Date);
|
|
170
|
+
const response = await runWithDeadline(() => requestFetch(signed.url, {
|
|
171
|
+
method,
|
|
172
|
+
headers: signed.headers,
|
|
173
|
+
redirect: "error",
|
|
174
|
+
signal: controller.signal
|
|
175
|
+
}), deadlineMs, () => controller.abort());
|
|
176
|
+
if (response.status === 404) {
|
|
177
|
+
response.body?.cancel().catch(() => {
|
|
178
|
+
return;
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
response.body?.cancel().catch(() => {
|
|
184
|
+
return;
|
|
185
|
+
});
|
|
186
|
+
throw new Error("versioned S3 request failed");
|
|
187
|
+
}
|
|
188
|
+
const observedVersionId = cleanResponseVersionId(response.headers.get("x-amz-version-id"));
|
|
189
|
+
if (observedVersionId !== versionId) {
|
|
190
|
+
response.body?.cancel().catch(() => {
|
|
191
|
+
return;
|
|
192
|
+
});
|
|
193
|
+
throw new Error("versioned S3 response authority mismatch");
|
|
194
|
+
}
|
|
195
|
+
const bytes = method === "GET" ? new Uint8Array(await response.arrayBuffer()) : new Uint8Array;
|
|
196
|
+
const contentLength = response.headers.get("content-length");
|
|
197
|
+
const parsedSize = contentLength === null ? bytes.byteLength : Number(contentLength);
|
|
198
|
+
if (!Number.isSafeInteger(parsedSize) || parsedSize < 0)
|
|
199
|
+
throw new Error("invalid S3 content length");
|
|
200
|
+
const lastModifiedValue = response.headers.get("last-modified");
|
|
201
|
+
const lastModified = lastModifiedValue ? new Date(lastModifiedValue) : undefined;
|
|
202
|
+
if (lastModified && !Number.isFinite(lastModified.getTime()))
|
|
203
|
+
throw new Error("invalid S3 last modified timestamp");
|
|
204
|
+
return {
|
|
205
|
+
bytes,
|
|
206
|
+
metadata: {
|
|
207
|
+
size: parsedSize,
|
|
208
|
+
versionId: observedVersionId,
|
|
209
|
+
...response.headers.get("etag") ? { etag: response.headers.get("etag") } : {},
|
|
210
|
+
...response.headers.get("content-type") ? { contentType: response.headers.get("content-type") } : {},
|
|
211
|
+
...lastModified ? { lastModified } : {}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
} catch {
|
|
215
|
+
throw new Error(`Instructions S3 ${method === "GET" ? "get" : "head"} operation failed`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function resolveRequestCredentials(config) {
|
|
219
|
+
if (config.credentials)
|
|
220
|
+
return config.credentials;
|
|
221
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID?.trim();
|
|
222
|
+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY?.trim();
|
|
223
|
+
const sessionToken = process.env.AWS_SESSION_TOKEN?.trim();
|
|
224
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
225
|
+
throw new Error("Instructions S3 signed requests require explicit or AWS environment credentials");
|
|
226
|
+
}
|
|
227
|
+
return { accessKeyId, secretAccessKey, ...sessionToken ? { sessionToken } : {} };
|
|
228
|
+
}
|
|
229
|
+
function signVersionedRequest(config, key, versionId, method, credentials, now) {
|
|
230
|
+
const url = buildObjectUrl(config, key);
|
|
231
|
+
url.searchParams.set("versionId", versionId);
|
|
232
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
233
|
+
const shortDate = amzDate.slice(0, 8);
|
|
234
|
+
const headers = new Headers({
|
|
235
|
+
host: url.host,
|
|
236
|
+
"x-amz-content-sha256": EMPTY_SHA256,
|
|
237
|
+
"x-amz-date": amzDate,
|
|
238
|
+
...credentials.sessionToken ? { "x-amz-security-token": credentials.sessionToken } : {}
|
|
239
|
+
});
|
|
240
|
+
const canonicalQuery = `versionId=${awsEncode(versionId)}`;
|
|
241
|
+
authorizeRequest(method, url, canonicalQuery, headers, EMPTY_SHA256, credentials, config.region, shortDate, amzDate);
|
|
242
|
+
return { url: url.toString(), headers };
|
|
243
|
+
}
|
|
244
|
+
function signConditionalCreateRequest(config, key, bytes, contentType, credentials, now) {
|
|
245
|
+
const url = buildObjectUrl(config, key);
|
|
246
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
247
|
+
const shortDate = amzDate.slice(0, 8);
|
|
248
|
+
const payloadHash = createHash("sha256").update(bytes).digest("hex");
|
|
249
|
+
const headers = new Headers({
|
|
250
|
+
"content-md5": createHash("md5").update(bytes).digest("base64"),
|
|
251
|
+
"content-type": contentType,
|
|
252
|
+
host: url.host,
|
|
253
|
+
"if-none-match": "*",
|
|
254
|
+
"x-amz-content-sha256": payloadHash,
|
|
255
|
+
"x-amz-date": amzDate,
|
|
256
|
+
...credentials.sessionToken ? { "x-amz-security-token": credentials.sessionToken } : {}
|
|
257
|
+
});
|
|
258
|
+
authorizeRequest("PUT", url, "", headers, payloadHash, credentials, config.region, shortDate, amzDate);
|
|
259
|
+
return { url: url.toString(), headers };
|
|
260
|
+
}
|
|
261
|
+
function authorizeRequest(method, url, canonicalQuery, headers, payloadHash, credentials, region, shortDate, amzDate) {
|
|
262
|
+
const signedHeaderNames = [];
|
|
263
|
+
headers.forEach((_value, name) => signedHeaderNames.push(name));
|
|
264
|
+
signedHeaderNames.sort();
|
|
265
|
+
const canonicalHeaders = signedHeaderNames.map((name) => `${name}:${headers.get(name).trim().replace(/\s+/g, " ")}
|
|
266
|
+
`).join("");
|
|
267
|
+
const canonicalRequest = [
|
|
268
|
+
method,
|
|
269
|
+
url.pathname,
|
|
270
|
+
canonicalQuery,
|
|
271
|
+
canonicalHeaders,
|
|
272
|
+
signedHeaderNames.join(";"),
|
|
273
|
+
payloadHash
|
|
274
|
+
].join(`
|
|
275
|
+
`);
|
|
276
|
+
const scope = `${shortDate}/${region}/s3/aws4_request`;
|
|
277
|
+
const stringToSign = [
|
|
278
|
+
"AWS4-HMAC-SHA256",
|
|
279
|
+
amzDate,
|
|
280
|
+
scope,
|
|
281
|
+
createHash("sha256").update(canonicalRequest).digest("hex")
|
|
282
|
+
].join(`
|
|
283
|
+
`);
|
|
284
|
+
const dateKey = hmac(`AWS4${credentials.secretAccessKey}`, shortDate);
|
|
285
|
+
const regionKey = hmac(dateKey, region);
|
|
286
|
+
const serviceKey = hmac(regionKey, "s3");
|
|
287
|
+
const signingKey = hmac(serviceKey, "aws4_request");
|
|
288
|
+
const signature = createHmac("sha256", signingKey).update(stringToSign).digest("hex");
|
|
289
|
+
headers.set("authorization", `AWS4-HMAC-SHA256 Credential=${credentials.accessKeyId}/${scope}, SignedHeaders=${signedHeaderNames.join(";")}, Signature=${signature}`);
|
|
290
|
+
}
|
|
291
|
+
function buildObjectUrl(config, key) {
|
|
292
|
+
const encodedKey = key.split("/").map(awsEncode).join("/");
|
|
293
|
+
if (config.endpoint) {
|
|
294
|
+
const endpoint = new URL(config.endpoint);
|
|
295
|
+
if (config.forcePathStyle)
|
|
296
|
+
endpoint.pathname = `/${awsEncode(config.bucket)}/${encodedKey}`;
|
|
297
|
+
else {
|
|
298
|
+
endpoint.hostname = `${config.bucket}.${endpoint.hostname}`;
|
|
299
|
+
endpoint.pathname = `/${encodedKey}`;
|
|
300
|
+
}
|
|
301
|
+
return endpoint;
|
|
302
|
+
}
|
|
303
|
+
return config.forcePathStyle ? new URL(`https://s3.${config.region}.amazonaws.com/${awsEncode(config.bucket)}/${encodedKey}`) : new URL(`https://${config.bucket}.s3.${config.region}.amazonaws.com/${encodedKey}`);
|
|
304
|
+
}
|
|
305
|
+
function awsEncode(value) {
|
|
306
|
+
return encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
307
|
+
}
|
|
308
|
+
function hmac(key, value) {
|
|
309
|
+
return createHmac("sha256", key).update(value).digest();
|
|
310
|
+
}
|
|
311
|
+
function cleanResponseVersionId(value) {
|
|
312
|
+
const versionId = value?.trim();
|
|
313
|
+
if (!versionId || versionId === "null")
|
|
314
|
+
return;
|
|
315
|
+
assertSafeInstructionsObjectVersionId(versionId);
|
|
316
|
+
return versionId;
|
|
317
|
+
}
|
|
318
|
+
function memoryInstructionsObjectStore() {
|
|
319
|
+
const objects = new Map;
|
|
320
|
+
let nextVersion = 1;
|
|
321
|
+
return {
|
|
322
|
+
async putIfAbsent(key, bytes, options) {
|
|
323
|
+
assertSafeInstructionsObjectKey(key);
|
|
324
|
+
const existing = objects.get(key);
|
|
325
|
+
if (existing)
|
|
326
|
+
return { status: "existing", versionId: existing.versionId };
|
|
327
|
+
const versionId = `memory-v${nextVersion++}`;
|
|
328
|
+
objects.set(key, {
|
|
329
|
+
bytes: Uint8Array.from(bytes),
|
|
330
|
+
contentType: options.contentType,
|
|
331
|
+
lastModified: new Date,
|
|
332
|
+
versionId
|
|
333
|
+
});
|
|
334
|
+
return { status: "created", versionId };
|
|
335
|
+
},
|
|
336
|
+
async get(key, options = {}) {
|
|
337
|
+
assertSafeInstructionsObjectKey(key);
|
|
338
|
+
if (options.versionId)
|
|
339
|
+
assertSafeInstructionsObjectVersionId(options.versionId);
|
|
340
|
+
const object = objects.get(key);
|
|
341
|
+
if (!object || options.versionId && options.versionId !== object.versionId)
|
|
342
|
+
return;
|
|
343
|
+
return Uint8Array.from(object.bytes);
|
|
344
|
+
},
|
|
345
|
+
async head(key, options = {}) {
|
|
346
|
+
assertSafeInstructionsObjectKey(key);
|
|
347
|
+
if (options.versionId)
|
|
348
|
+
assertSafeInstructionsObjectVersionId(options.versionId);
|
|
349
|
+
const object = objects.get(key);
|
|
350
|
+
return object && (!options.versionId || options.versionId === object.versionId) ? {
|
|
351
|
+
size: object.bytes.byteLength,
|
|
352
|
+
contentType: object.contentType,
|
|
353
|
+
lastModified: new Date(object.lastModified),
|
|
354
|
+
versionId: object.versionId
|
|
355
|
+
} : undefined;
|
|
356
|
+
},
|
|
357
|
+
keys() {
|
|
358
|
+
return [...objects.keys()].sort();
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export { assertSafeInstructionsObjectKey, assertSafeInstructionsObjectVersionId, createInstructionsS3ObjectStore, memoryInstructionsObjectStore };
|