@hasna/instructions 0.6.1 → 0.7.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 +123 -6
- package/dist/chunks/{apply-ynfn1av7.js → apply-7svah2qm.js} +5 -3
- package/dist/chunks/{apply-sfzja1f3.js → apply-jp13sa1e.js} +6 -3
- package/dist/chunks/database-wstmr56z.js +23 -0
- package/dist/chunks/{index-85n5wnd8.js → index-1cnyjnd7.js} +5 -7
- package/dist/chunks/{index-pkscjnj5.js → index-3djthcm6.js} +0 -1
- package/dist/chunks/{index-v7dkb384.js → index-4pfq42t5.js} +129 -77
- package/dist/chunks/index-5d7zdftc.js +186 -0
- package/dist/chunks/{index-1hpwskwd.js → index-7m4j8a33.js} +205 -329
- package/dist/chunks/index-8rb7ng2y.js +148 -0
- package/dist/chunks/index-arrrx0ed.js +344 -0
- package/dist/chunks/{index-9p9gn7gh.js → index-bzyxvwcd.js} +5 -7
- package/dist/chunks/index-fpeyzn3n.js +186 -0
- package/dist/chunks/{index-axhwg61p.js → index-fwbs6tma.js} +15 -4
- package/dist/chunks/index-n3fjspr0.js +119 -0
- package/dist/chunks/{index-e9epbsvs.js → index-z3ma4aqk.js} +15 -4
- package/dist/chunks/legacy-store-migration-z2m5yzyk.js +322 -0
- package/dist/chunks/{local-a9nen65z.js → local-5wvv5xv3.js} +2 -2
- package/dist/chunks/{local-0d8qrc92.js → local-zxa5dpf4.js} +13 -188
- package/dist/chunks/s3-backup-n2z4fwyh.js +240 -0
- package/dist/chunks/s3-config-hhght91k.js +14 -0
- package/dist/chunks/s3-object-store-90tgbh6k.js +14 -0
- package/dist/chunks/{sync-m46fc257.js → sync-2075deve.js} +5 -4
- package/dist/chunks/{sync-vy1yjvs6.js → sync-r8t7zsp3.js} +4 -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 +1011 -247
- package/dist/data/config-store.d.ts +1 -0
- package/dist/data/config-store.d.ts.map +1 -1
- package/dist/index.js +840 -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 +1 -0
- 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 +31 -20
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +205 -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 +57 -0
- package/dist/sdk/v1-client.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +212 -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 +1180 -432
- package/dist/server/openapi.d.ts +1264 -92
- 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 +57 -1
- 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 +725 -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 +65 -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 +22 -11
- package/assets/skills/inbox/SKILL.md +0 -86
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/storage/s3-config.ts
|
|
3
|
+
var INSTRUCTIONS_S3_ENV = {
|
|
4
|
+
bucket: "HASNA_INSTRUCTIONS_S3_BUCKET",
|
|
5
|
+
prefix: "HASNA_INSTRUCTIONS_S3_PREFIX",
|
|
6
|
+
region: "HASNA_INSTRUCTIONS_AWS_REGION",
|
|
7
|
+
endpoint: "HASNA_INSTRUCTIONS_S3_ENDPOINT",
|
|
8
|
+
forcePathStyle: "HASNA_INSTRUCTIONS_S3_FORCE_PATH_STYLE",
|
|
9
|
+
accessKeyId: "HASNA_INSTRUCTIONS_S3_ACCESS_KEY_ID",
|
|
10
|
+
secretAccessKey: "HASNA_INSTRUCTIONS_S3_SECRET_ACCESS_KEY",
|
|
11
|
+
sessionToken: "HASNA_INSTRUCTIONS_S3_SESSION_TOKEN"
|
|
12
|
+
};
|
|
13
|
+
var INSTRUCTIONS_S3_ALIAS_ENV = {
|
|
14
|
+
bucket: "INSTRUCTIONS_S3_BUCKET",
|
|
15
|
+
prefix: "INSTRUCTIONS_S3_PREFIX",
|
|
16
|
+
region: "INSTRUCTIONS_AWS_REGION",
|
|
17
|
+
endpoint: "INSTRUCTIONS_S3_ENDPOINT",
|
|
18
|
+
forcePathStyle: "INSTRUCTIONS_S3_FORCE_PATH_STYLE",
|
|
19
|
+
accessKeyId: "INSTRUCTIONS_S3_ACCESS_KEY_ID",
|
|
20
|
+
secretAccessKey: "INSTRUCTIONS_S3_SECRET_ACCESS_KEY",
|
|
21
|
+
sessionToken: "INSTRUCTIONS_S3_SESSION_TOKEN"
|
|
22
|
+
};
|
|
23
|
+
var BUCKET_PATTERN = /^(?!xn--)[a-z0-9](?:[a-z0-9.-]{1,61}[a-z0-9])?$/;
|
|
24
|
+
var REGION_PATTERN = /^[a-z]{2}(?:-gov)?-[a-z0-9-]+-\d$/;
|
|
25
|
+
var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
|
|
26
|
+
function loadInstructionsS3Config(env = process.env) {
|
|
27
|
+
const bucket = readEnv(env, "bucket");
|
|
28
|
+
const configuredWithoutBucket = Object.keys(INSTRUCTIONS_S3_ENV).filter((key) => key !== "bucket").some((key) => readEnv(env, key) !== undefined);
|
|
29
|
+
if (!bucket) {
|
|
30
|
+
if (configuredWithoutBucket) {
|
|
31
|
+
throw new Error(`${INSTRUCTIONS_S3_ENV.bucket} is required when any Instructions S3 setting is configured`);
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
validateBucket(bucket);
|
|
36
|
+
const prefix = normalizeInstructionsS3Prefix(readEnv(env, "prefix") ?? "instructions/");
|
|
37
|
+
const region = readEnv(env, "region") ?? "us-east-1";
|
|
38
|
+
validateRegion(region);
|
|
39
|
+
const endpoint = normalizeEndpoint(readEnv(env, "endpoint"));
|
|
40
|
+
const forcePathStyle = parseBoolean(readEnv(env, "forcePathStyle"), false);
|
|
41
|
+
const accessKeyId = readEnv(env, "accessKeyId");
|
|
42
|
+
const secretAccessKey = readEnv(env, "secretAccessKey");
|
|
43
|
+
const sessionToken = readEnv(env, "sessionToken");
|
|
44
|
+
if (Boolean(accessKeyId) !== Boolean(secretAccessKey)) {
|
|
45
|
+
throw new Error(`${INSTRUCTIONS_S3_ENV.accessKeyId} and ${INSTRUCTIONS_S3_ENV.secretAccessKey} must be configured together`);
|
|
46
|
+
}
|
|
47
|
+
if (sessionToken && (!accessKeyId || !secretAccessKey)) {
|
|
48
|
+
throw new Error(`${INSTRUCTIONS_S3_ENV.sessionToken} requires a complete static credential pair`);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
provider: "s3",
|
|
52
|
+
bucket,
|
|
53
|
+
prefix,
|
|
54
|
+
region,
|
|
55
|
+
...endpoint ? { endpoint } : {},
|
|
56
|
+
forcePathStyle,
|
|
57
|
+
...accessKeyId && secretAccessKey ? {
|
|
58
|
+
credentials: {
|
|
59
|
+
accessKeyId,
|
|
60
|
+
secretAccessKey,
|
|
61
|
+
...sessionToken ? { sessionToken } : {}
|
|
62
|
+
}
|
|
63
|
+
} : {}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function normalizeInstructionsS3Prefix(value) {
|
|
67
|
+
const trimmed = value.trim();
|
|
68
|
+
if (!trimmed || CONTROL_CHARACTERS.test(trimmed) || trimmed.includes("\\")) {
|
|
69
|
+
throw new Error("Instructions S3 prefix is invalid");
|
|
70
|
+
}
|
|
71
|
+
const withoutEdges = trimmed.replace(/^\/+|\/+$/g, "");
|
|
72
|
+
if (!withoutEdges || withoutEdges.includes("//")) {
|
|
73
|
+
throw new Error("Instructions S3 prefix is invalid");
|
|
74
|
+
}
|
|
75
|
+
const segments = withoutEdges.split("/");
|
|
76
|
+
for (const segment of segments) {
|
|
77
|
+
let decoded;
|
|
78
|
+
try {
|
|
79
|
+
decoded = decodeURIComponent(segment);
|
|
80
|
+
} catch {
|
|
81
|
+
throw new Error("Instructions S3 prefix contains invalid percent encoding");
|
|
82
|
+
}
|
|
83
|
+
if (!decoded || decoded === "." || decoded === ".." || decoded.includes("/") || decoded.includes("\\") || CONTROL_CHARACTERS.test(decoded)) {
|
|
84
|
+
throw new Error("Instructions S3 prefix contains an unsafe path segment");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return `${segments.join("/")}/`;
|
|
88
|
+
}
|
|
89
|
+
function readEnv(env, key) {
|
|
90
|
+
const canonical = INSTRUCTIONS_S3_ENV[key];
|
|
91
|
+
if (Object.prototype.hasOwnProperty.call(env, canonical))
|
|
92
|
+
return clean(env[canonical]);
|
|
93
|
+
return clean(env[INSTRUCTIONS_S3_ALIAS_ENV[key]]);
|
|
94
|
+
}
|
|
95
|
+
function clean(value) {
|
|
96
|
+
const trimmed = value?.trim();
|
|
97
|
+
return trimmed ? trimmed : undefined;
|
|
98
|
+
}
|
|
99
|
+
function parseBoolean(value, fallback) {
|
|
100
|
+
if (!value)
|
|
101
|
+
return fallback;
|
|
102
|
+
switch (value.toLowerCase()) {
|
|
103
|
+
case "1":
|
|
104
|
+
case "true":
|
|
105
|
+
case "yes":
|
|
106
|
+
case "on":
|
|
107
|
+
return true;
|
|
108
|
+
case "0":
|
|
109
|
+
case "false":
|
|
110
|
+
case "no":
|
|
111
|
+
case "off":
|
|
112
|
+
return false;
|
|
113
|
+
default:
|
|
114
|
+
throw new Error("Instructions S3 boolean setting is invalid");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function validateBucket(bucket) {
|
|
118
|
+
if (bucket.length < 3 || bucket.length > 63 || !BUCKET_PATTERN.test(bucket) || bucket.includes("..") || bucket.includes(".-") || bucket.includes("-.") || /^\d{1,3}(?:\.\d{1,3}){3}$/.test(bucket)) {
|
|
119
|
+
throw new Error("Instructions S3 bucket is invalid");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function validateRegion(region) {
|
|
123
|
+
if (!REGION_PATTERN.test(region))
|
|
124
|
+
throw new Error("Instructions AWS region is invalid");
|
|
125
|
+
}
|
|
126
|
+
function normalizeEndpoint(value) {
|
|
127
|
+
if (!value)
|
|
128
|
+
return;
|
|
129
|
+
let endpoint;
|
|
130
|
+
try {
|
|
131
|
+
endpoint = new URL(value);
|
|
132
|
+
} catch {
|
|
133
|
+
throw new Error("Instructions S3 endpoint is invalid");
|
|
134
|
+
}
|
|
135
|
+
if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
|
|
136
|
+
throw new Error("Instructions S3 endpoint must not contain credentials, query parameters, or fragments");
|
|
137
|
+
}
|
|
138
|
+
if (endpoint.pathname !== "/") {
|
|
139
|
+
throw new Error("Instructions S3 endpoint must be an origin without a path");
|
|
140
|
+
}
|
|
141
|
+
const localHttp = endpoint.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname);
|
|
142
|
+
if (endpoint.protocol !== "https:" && !localHttp) {
|
|
143
|
+
throw new Error("Instructions S3 endpoint must use HTTPS (HTTP is allowed only for loopback development)");
|
|
144
|
+
}
|
|
145
|
+
return endpoint.origin;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export { INSTRUCTIONS_S3_ENV, INSTRUCTIONS_S3_ALIAS_ENV, loadInstructionsS3Config, normalizeInstructionsS3Prefix };
|
|
@@ -0,0 +1,344 @@
|
|
|
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
|
+
for (let attempt = 1;attempt <= CONDITIONAL_CREATE_ATTEMPTS; attempt += 1) {
|
|
77
|
+
let response;
|
|
78
|
+
const controller = new AbortController;
|
|
79
|
+
try {
|
|
80
|
+
const url = client.presign(key, { method: "PUT", expiresIn: 60 });
|
|
81
|
+
response = await runWithDeadline(() => requestFetch(url, {
|
|
82
|
+
method: "PUT",
|
|
83
|
+
headers: {
|
|
84
|
+
"content-type": options.contentType,
|
|
85
|
+
"content-md5": createHash("md5").update(bytes).digest("base64"),
|
|
86
|
+
"if-none-match": "*"
|
|
87
|
+
},
|
|
88
|
+
body: Uint8Array.from(bytes),
|
|
89
|
+
redirect: "error",
|
|
90
|
+
signal: controller.signal
|
|
91
|
+
}), conditionalCreateAttemptDeadlineMs, () => controller.abort());
|
|
92
|
+
} catch {
|
|
93
|
+
let exists = false;
|
|
94
|
+
try {
|
|
95
|
+
exists = await runWithDeadline(() => client.file(key).exists(), existenceReconciliationDeadlineMs);
|
|
96
|
+
} catch {}
|
|
97
|
+
if (exists)
|
|
98
|
+
return { status: "existing" };
|
|
99
|
+
if (attempt < CONDITIONAL_CREATE_ATTEMPTS)
|
|
100
|
+
continue;
|
|
101
|
+
throw new Error("Instructions S3 conditional object creation failed");
|
|
102
|
+
}
|
|
103
|
+
if (response.ok) {
|
|
104
|
+
const versionId = cleanResponseVersionId(response.headers.get("x-amz-version-id"));
|
|
105
|
+
response.body?.cancel().catch(() => {
|
|
106
|
+
return;
|
|
107
|
+
});
|
|
108
|
+
return { status: "created", ...versionId ? { versionId } : {} };
|
|
109
|
+
}
|
|
110
|
+
response.body?.cancel().catch(() => {
|
|
111
|
+
return;
|
|
112
|
+
});
|
|
113
|
+
if (response.status === 412)
|
|
114
|
+
return { status: "existing" };
|
|
115
|
+
if (response.status === 409 && attempt < CONDITIONAL_CREATE_ATTEMPTS)
|
|
116
|
+
continue;
|
|
117
|
+
if (response.status === 409) {
|
|
118
|
+
throw new Error("Instructions S3 conditional object creation remained conflicted");
|
|
119
|
+
}
|
|
120
|
+
throw new Error(`Instructions S3 conditional object creation failed with status ${response.status}`);
|
|
121
|
+
}
|
|
122
|
+
throw new Error("Instructions S3 conditional object creation failed");
|
|
123
|
+
},
|
|
124
|
+
async get(key, options = {}) {
|
|
125
|
+
assertSafeInstructionsObjectKey(key);
|
|
126
|
+
if (options.versionId) {
|
|
127
|
+
return readExactVersion(config, key, options.versionId, "GET", requestFetch, versionedReadDeadlineMs).then((result) => result?.bytes);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const file = client.file(key);
|
|
131
|
+
const exists = await runWithDeadline(() => file.exists(), existsDeadlineMs);
|
|
132
|
+
if (!exists)
|
|
133
|
+
return;
|
|
134
|
+
return Uint8Array.from(await runWithDeadline(() => file.bytes(), bytesDeadlineMs));
|
|
135
|
+
} catch {
|
|
136
|
+
throw new Error("Instructions S3 get operation failed");
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
async head(key, options = {}) {
|
|
140
|
+
assertSafeInstructionsObjectKey(key);
|
|
141
|
+
if (options.versionId) {
|
|
142
|
+
return readExactVersion(config, key, options.versionId, "HEAD", requestFetch, versionedReadDeadlineMs).then((result) => result?.metadata);
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const file = client.file(key);
|
|
146
|
+
const exists = await runWithDeadline(() => file.exists(), existsDeadlineMs);
|
|
147
|
+
if (!exists)
|
|
148
|
+
return;
|
|
149
|
+
const stat = await runWithDeadline(() => file.stat(), statDeadlineMs);
|
|
150
|
+
return {
|
|
151
|
+
size: stat.size,
|
|
152
|
+
...stat.etag ? { etag: stat.etag } : {},
|
|
153
|
+
...stat.type ? { contentType: stat.type } : {},
|
|
154
|
+
...stat.lastModified ? { lastModified: stat.lastModified } : {}
|
|
155
|
+
};
|
|
156
|
+
} catch {
|
|
157
|
+
throw new Error("Instructions S3 head operation failed");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async function readExactVersion(config, key, versionId, method, requestFetch, deadlineMs) {
|
|
163
|
+
assertSafeInstructionsObjectVersionId(versionId);
|
|
164
|
+
const credentials = resolveRequestCredentials(config);
|
|
165
|
+
const controller = new AbortController;
|
|
166
|
+
try {
|
|
167
|
+
const signed = signVersionedRequest(config, key, versionId, method, credentials, new Date);
|
|
168
|
+
const response = await runWithDeadline(() => requestFetch(signed.url, {
|
|
169
|
+
method,
|
|
170
|
+
headers: signed.headers,
|
|
171
|
+
redirect: "error",
|
|
172
|
+
signal: controller.signal
|
|
173
|
+
}), deadlineMs, () => controller.abort());
|
|
174
|
+
if (response.status === 404) {
|
|
175
|
+
response.body?.cancel().catch(() => {
|
|
176
|
+
return;
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
response.body?.cancel().catch(() => {
|
|
182
|
+
return;
|
|
183
|
+
});
|
|
184
|
+
throw new Error("versioned S3 request failed");
|
|
185
|
+
}
|
|
186
|
+
const observedVersionId = cleanResponseVersionId(response.headers.get("x-amz-version-id"));
|
|
187
|
+
if (observedVersionId !== versionId) {
|
|
188
|
+
response.body?.cancel().catch(() => {
|
|
189
|
+
return;
|
|
190
|
+
});
|
|
191
|
+
throw new Error("versioned S3 response authority mismatch");
|
|
192
|
+
}
|
|
193
|
+
const bytes = method === "GET" ? new Uint8Array(await response.arrayBuffer()) : new Uint8Array;
|
|
194
|
+
const contentLength = response.headers.get("content-length");
|
|
195
|
+
const parsedSize = contentLength === null ? bytes.byteLength : Number(contentLength);
|
|
196
|
+
if (!Number.isSafeInteger(parsedSize) || parsedSize < 0)
|
|
197
|
+
throw new Error("invalid S3 content length");
|
|
198
|
+
const lastModifiedValue = response.headers.get("last-modified");
|
|
199
|
+
const lastModified = lastModifiedValue ? new Date(lastModifiedValue) : undefined;
|
|
200
|
+
if (lastModified && !Number.isFinite(lastModified.getTime()))
|
|
201
|
+
throw new Error("invalid S3 last modified timestamp");
|
|
202
|
+
return {
|
|
203
|
+
bytes,
|
|
204
|
+
metadata: {
|
|
205
|
+
size: parsedSize,
|
|
206
|
+
versionId: observedVersionId,
|
|
207
|
+
...response.headers.get("etag") ? { etag: response.headers.get("etag") } : {},
|
|
208
|
+
...response.headers.get("content-type") ? { contentType: response.headers.get("content-type") } : {},
|
|
209
|
+
...lastModified ? { lastModified } : {}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
} catch {
|
|
213
|
+
throw new Error(`Instructions S3 ${method === "GET" ? "get" : "head"} operation failed`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function resolveRequestCredentials(config) {
|
|
217
|
+
if (config.credentials)
|
|
218
|
+
return config.credentials;
|
|
219
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID?.trim();
|
|
220
|
+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY?.trim();
|
|
221
|
+
const sessionToken = process.env.AWS_SESSION_TOKEN?.trim();
|
|
222
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
223
|
+
throw new Error("Instructions S3 exact-version reads require explicit or AWS environment credentials");
|
|
224
|
+
}
|
|
225
|
+
return { accessKeyId, secretAccessKey, ...sessionToken ? { sessionToken } : {} };
|
|
226
|
+
}
|
|
227
|
+
function signVersionedRequest(config, key, versionId, method, credentials, now) {
|
|
228
|
+
const url = buildObjectUrl(config, key);
|
|
229
|
+
url.searchParams.set("versionId", versionId);
|
|
230
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
231
|
+
const shortDate = amzDate.slice(0, 8);
|
|
232
|
+
const headers = new Headers({
|
|
233
|
+
host: url.host,
|
|
234
|
+
"x-amz-content-sha256": EMPTY_SHA256,
|
|
235
|
+
"x-amz-date": amzDate,
|
|
236
|
+
...credentials.sessionToken ? { "x-amz-security-token": credentials.sessionToken } : {}
|
|
237
|
+
});
|
|
238
|
+
const signedHeaderNames = [
|
|
239
|
+
"host",
|
|
240
|
+
"x-amz-content-sha256",
|
|
241
|
+
"x-amz-date",
|
|
242
|
+
...credentials.sessionToken ? ["x-amz-security-token"] : []
|
|
243
|
+
].sort();
|
|
244
|
+
const canonicalHeaders = signedHeaderNames.map((name) => `${name}:${headers.get(name).trim()}
|
|
245
|
+
`).join("");
|
|
246
|
+
const canonicalQuery = `versionId=${awsEncode(versionId)}`;
|
|
247
|
+
const canonicalRequest = [
|
|
248
|
+
method,
|
|
249
|
+
url.pathname,
|
|
250
|
+
canonicalQuery,
|
|
251
|
+
canonicalHeaders,
|
|
252
|
+
signedHeaderNames.join(";"),
|
|
253
|
+
EMPTY_SHA256
|
|
254
|
+
].join(`
|
|
255
|
+
`);
|
|
256
|
+
const scope = `${shortDate}/${config.region}/s3/aws4_request`;
|
|
257
|
+
const stringToSign = [
|
|
258
|
+
"AWS4-HMAC-SHA256",
|
|
259
|
+
amzDate,
|
|
260
|
+
scope,
|
|
261
|
+
createHash("sha256").update(canonicalRequest).digest("hex")
|
|
262
|
+
].join(`
|
|
263
|
+
`);
|
|
264
|
+
const dateKey = hmac(`AWS4${credentials.secretAccessKey}`, shortDate);
|
|
265
|
+
const regionKey = hmac(dateKey, config.region);
|
|
266
|
+
const serviceKey = hmac(regionKey, "s3");
|
|
267
|
+
const signingKey = hmac(serviceKey, "aws4_request");
|
|
268
|
+
const signature = createHmac("sha256", signingKey).update(stringToSign).digest("hex");
|
|
269
|
+
headers.set("authorization", `AWS4-HMAC-SHA256 Credential=${credentials.accessKeyId}/${scope}, SignedHeaders=${signedHeaderNames.join(";")}, Signature=${signature}`);
|
|
270
|
+
return { url: url.toString(), headers };
|
|
271
|
+
}
|
|
272
|
+
function buildObjectUrl(config, key) {
|
|
273
|
+
const encodedKey = key.split("/").map(awsEncode).join("/");
|
|
274
|
+
if (config.endpoint) {
|
|
275
|
+
const endpoint = new URL(config.endpoint);
|
|
276
|
+
if (config.forcePathStyle)
|
|
277
|
+
endpoint.pathname = `/${awsEncode(config.bucket)}/${encodedKey}`;
|
|
278
|
+
else {
|
|
279
|
+
endpoint.hostname = `${config.bucket}.${endpoint.hostname}`;
|
|
280
|
+
endpoint.pathname = `/${encodedKey}`;
|
|
281
|
+
}
|
|
282
|
+
return endpoint;
|
|
283
|
+
}
|
|
284
|
+
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}`);
|
|
285
|
+
}
|
|
286
|
+
function awsEncode(value) {
|
|
287
|
+
return encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
288
|
+
}
|
|
289
|
+
function hmac(key, value) {
|
|
290
|
+
return createHmac("sha256", key).update(value).digest();
|
|
291
|
+
}
|
|
292
|
+
function cleanResponseVersionId(value) {
|
|
293
|
+
const versionId = value?.trim();
|
|
294
|
+
if (!versionId || versionId === "null")
|
|
295
|
+
return;
|
|
296
|
+
assertSafeInstructionsObjectVersionId(versionId);
|
|
297
|
+
return versionId;
|
|
298
|
+
}
|
|
299
|
+
function memoryInstructionsObjectStore() {
|
|
300
|
+
const objects = new Map;
|
|
301
|
+
let nextVersion = 1;
|
|
302
|
+
return {
|
|
303
|
+
async putIfAbsent(key, bytes, options) {
|
|
304
|
+
assertSafeInstructionsObjectKey(key);
|
|
305
|
+
const existing = objects.get(key);
|
|
306
|
+
if (existing)
|
|
307
|
+
return { status: "existing", versionId: existing.versionId };
|
|
308
|
+
const versionId = `memory-v${nextVersion++}`;
|
|
309
|
+
objects.set(key, {
|
|
310
|
+
bytes: Uint8Array.from(bytes),
|
|
311
|
+
contentType: options.contentType,
|
|
312
|
+
lastModified: new Date,
|
|
313
|
+
versionId
|
|
314
|
+
});
|
|
315
|
+
return { status: "created", versionId };
|
|
316
|
+
},
|
|
317
|
+
async get(key, options = {}) {
|
|
318
|
+
assertSafeInstructionsObjectKey(key);
|
|
319
|
+
if (options.versionId)
|
|
320
|
+
assertSafeInstructionsObjectVersionId(options.versionId);
|
|
321
|
+
const object = objects.get(key);
|
|
322
|
+
if (!object || options.versionId && options.versionId !== object.versionId)
|
|
323
|
+
return;
|
|
324
|
+
return Uint8Array.from(object.bytes);
|
|
325
|
+
},
|
|
326
|
+
async head(key, options = {}) {
|
|
327
|
+
assertSafeInstructionsObjectKey(key);
|
|
328
|
+
if (options.versionId)
|
|
329
|
+
assertSafeInstructionsObjectVersionId(options.versionId);
|
|
330
|
+
const object = objects.get(key);
|
|
331
|
+
return object && (!options.versionId || options.versionId === object.versionId) ? {
|
|
332
|
+
size: object.bytes.byteLength,
|
|
333
|
+
contentType: object.contentType,
|
|
334
|
+
lastModified: new Date(object.lastModified),
|
|
335
|
+
versionId: object.versionId
|
|
336
|
+
} : undefined;
|
|
337
|
+
},
|
|
338
|
+
keys() {
|
|
339
|
+
return [...objects.keys()].sort();
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export { assertSafeInstructionsObjectKey, assertSafeInstructionsObjectVersionId, createInstructionsS3ObjectStore, memoryInstructionsObjectStore };
|
|
@@ -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-fwbs6tma.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-4pfq42t5.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) {
|