@hasna/skills 0.9.2 → 0.9.4
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/bin/index.js +144 -12
- package/bin/maintenance.js +4913 -0
- package/bin/mcp.js +3 -2
- package/bin/migrate.js +55 -2
- package/bin/server.js +173 -10
- package/bin/worker.js +173 -10
- package/dist/index.js +8 -5
- package/dist/lib/agent-codex-trust.d.ts +34 -0
- package/dist/lib/agent-integration.d.ts +1 -0
- package/dist/lib/codex-hook-rpc.d.ts +1 -1
- package/dist/sdk/index.js +178 -13
- package/dist/server/maintenance-entry.d.ts +2 -0
- package/dist/server/maintenance.d.ts +1 -0
- package/dist/server/sqlite-store.d.ts +3 -1
- package/dist/server/store.d.ts +7 -1
- package/dist/server/types.d.ts +44 -0
- package/migrations/postgres/0005_operator_scope_receipts.sql +9 -0
- package/migrations/sqlite/0005_operator_scope_receipts.sql +7 -0
- package/package.json +3 -2
package/bin/mcp.js
CHANGED
|
@@ -20019,7 +20019,7 @@ var package_default;
|
|
|
20019
20019
|
var init_package = __esm(() => {
|
|
20020
20020
|
package_default = {
|
|
20021
20021
|
name: "@hasna/skills",
|
|
20022
|
-
version: "0.9.
|
|
20022
|
+
version: "0.9.4",
|
|
20023
20023
|
description: "Skills library for AI coding agents",
|
|
20024
20024
|
type: "module",
|
|
20025
20025
|
bin: {
|
|
@@ -20028,6 +20028,7 @@ var init_package = __esm(() => {
|
|
|
20028
20028
|
"skills-serve": "bin/server.js",
|
|
20029
20029
|
"skills-server": "bin/server.js",
|
|
20030
20030
|
"skills-worker": "bin/worker.js",
|
|
20031
|
+
"skills-maintenance": "bin/maintenance.js",
|
|
20031
20032
|
"skills-migrate": "bin/migrate.js"
|
|
20032
20033
|
},
|
|
20033
20034
|
exports: {
|
|
@@ -20068,7 +20069,7 @@ var init_package = __esm(() => {
|
|
|
20068
20069
|
types: "./dist/index.d.ts",
|
|
20069
20070
|
scripts: {
|
|
20070
20071
|
clean: "rm -rf bin/ dist/",
|
|
20071
|
-
build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
20072
|
+
build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/server/maintenance.ts --outfile ./bin/maintenance.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
20072
20073
|
"build:js": "rm -rf dist && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
20073
20074
|
test: "bun test --timeout 30000",
|
|
20074
20075
|
dev: "bun run ./src/cli/index.tsx",
|
package/bin/migrate.js
CHANGED
|
@@ -7,7 +7,7 @@ import { join as join5 } from "path";
|
|
|
7
7
|
// package.json
|
|
8
8
|
var package_default = {
|
|
9
9
|
name: "@hasna/skills",
|
|
10
|
-
version: "0.9.
|
|
10
|
+
version: "0.9.4",
|
|
11
11
|
description: "Skills library for AI coding agents",
|
|
12
12
|
type: "module",
|
|
13
13
|
bin: {
|
|
@@ -16,6 +16,7 @@ var package_default = {
|
|
|
16
16
|
"skills-serve": "bin/server.js",
|
|
17
17
|
"skills-server": "bin/server.js",
|
|
18
18
|
"skills-worker": "bin/worker.js",
|
|
19
|
+
"skills-maintenance": "bin/maintenance.js",
|
|
19
20
|
"skills-migrate": "bin/migrate.js"
|
|
20
21
|
},
|
|
21
22
|
exports: {
|
|
@@ -56,7 +57,7 @@ var package_default = {
|
|
|
56
57
|
types: "./dist/index.d.ts",
|
|
57
58
|
scripts: {
|
|
58
59
|
clean: "rm -rf bin/ dist/",
|
|
59
|
-
build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
60
|
+
build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/server/maintenance.ts --outfile ./bin/maintenance.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
60
61
|
"build:js": "rm -rf dist && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
61
62
|
test: "bun test --timeout 30000",
|
|
62
63
|
dev: "bun run ./src/cli/index.tsx",
|
|
@@ -1110,6 +1111,10 @@ class SkillLifecycleConflictError extends Error {
|
|
|
1110
1111
|
this.name = "SkillLifecycleConflictError";
|
|
1111
1112
|
}
|
|
1112
1113
|
}
|
|
1114
|
+
function validOperatorScopeEnrollmentInput(input) {
|
|
1115
|
+
const bounded = (value, max = 256) => typeof value === "string" && value.length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
|
|
1116
|
+
return bounded(input.keyId) && bounded(input.stationId) && bounded(input.orgId) && bounded(input.operationId) && bounded(input.manifestDigest, 64) && /^[a-f0-9]{64}$/.test(input.manifestDigest) && bounded(input.operatorJobId) && bounded(input.operatorTaskArn, 512) && input.expectedScopes.length > 0 && input.expectedScopes.length <= 32 && input.expectedScopes.every((scope) => bounded(scope, 128) && /^[a-z][a-z0-9_-]*:[a-z][a-z0-9_-]*$/.test(scope));
|
|
1117
|
+
}
|
|
1113
1118
|
|
|
1114
1119
|
// src/lib/revision.ts
|
|
1115
1120
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -1257,6 +1262,54 @@ class SqliteSkillsStore {
|
|
|
1257
1262
|
}
|
|
1258
1263
|
return { kind: "updated", scopes };
|
|
1259
1264
|
}
|
|
1265
|
+
async enrollPublishScopeByOperator(input) {
|
|
1266
|
+
if (!validOperatorScopeEnrollmentInput(input))
|
|
1267
|
+
return { kind: "invalid" };
|
|
1268
|
+
if (input.expectedScopes.includes("skills:publish"))
|
|
1269
|
+
return { kind: "stale", scopes: input.expectedScopes };
|
|
1270
|
+
const operation = this.get("SELECT org_id, target_id FROM skills_audit_events WHERE action = ? AND operator_operation_id = ? LIMIT 1", ["api_key_scopes_added", input.operationId]);
|
|
1271
|
+
if (operation && (String(operation.org_id) !== input.orgId || String(operation.target_id) !== input.keyId))
|
|
1272
|
+
return { kind: "target_mismatch" };
|
|
1273
|
+
const row = this.get("SELECT id, org_id, name, scopes_json FROM api_keys WHERE id = ? AND revoked_at IS NULL LIMIT 1", [input.keyId]);
|
|
1274
|
+
if (!row)
|
|
1275
|
+
return { kind: "not_found" };
|
|
1276
|
+
if (String(row.org_id) !== input.orgId)
|
|
1277
|
+
return { kind: "target_mismatch" };
|
|
1278
|
+
const current = parseScopes(row.scopes_json);
|
|
1279
|
+
const prior = this.get("SELECT metadata_json FROM skills_audit_events WHERE action = ? AND target_type = ? AND target_id = ? AND json_extract(metadata_json, '$.operator_operation_id') = ? LIMIT 1", ["api_key_scopes_added", "api_key", input.keyId, input.operationId]);
|
|
1280
|
+
if (prior) {
|
|
1281
|
+
try {
|
|
1282
|
+
if (JSON.parse(String(prior.metadata_json)).target_manifest_digest !== input.manifestDigest)
|
|
1283
|
+
return { kind: "target_mismatch" };
|
|
1284
|
+
} catch {
|
|
1285
|
+
return { kind: "target_mismatch" };
|
|
1286
|
+
}
|
|
1287
|
+
return current.includes("skills:publish") ? { kind: "already_applied", scopes: current } : { kind: "stale", scopes: current };
|
|
1288
|
+
}
|
|
1289
|
+
if (current.length !== input.expectedScopes.length || current.some((scope, index) => scope !== input.expectedScopes[index]))
|
|
1290
|
+
return { kind: "stale", scopes: current };
|
|
1291
|
+
const scopes = [...current, "skills:publish"];
|
|
1292
|
+
const tx = this.db.transaction(() => {
|
|
1293
|
+
const changed = this.db.run("UPDATE api_keys SET scopes_json = ? WHERE id = ? AND org_id = ? AND revoked_at IS NULL AND scopes_json = ?", [JSON.stringify(scopes), input.keyId, input.orgId, JSON.stringify(input.expectedScopes)]);
|
|
1294
|
+
if (changed.changes !== 1)
|
|
1295
|
+
return false;
|
|
1296
|
+
this.db.run("INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, operator_operation_id, metadata_json) VALUES (?, NULL, NULL, ?, ?, ?, ?, ?)", [input.orgId, "api_key_scopes_added", "api_key", input.keyId, input.operationId, JSON.stringify({ added: ["skills:publish"], scopes, operator_operation_id: input.operationId, operator_job_id: input.operatorJobId, operator_task_arn: input.operatorTaskArn, target_manifest_digest: input.manifestDigest, station_id: input.stationId })]);
|
|
1297
|
+
return true;
|
|
1298
|
+
});
|
|
1299
|
+
if (!tx()) {
|
|
1300
|
+
const latest = this.get("SELECT scopes_json FROM api_keys WHERE id = ? AND org_id = ? AND revoked_at IS NULL LIMIT 1", [input.keyId, input.orgId]);
|
|
1301
|
+
return { kind: "stale", scopes: latest ? parseScopes(latest.scopes_json) : current };
|
|
1302
|
+
}
|
|
1303
|
+
return { kind: "updated", scopes };
|
|
1304
|
+
}
|
|
1305
|
+
async inspectOperatorScopeTarget(keyId, orgId) {
|
|
1306
|
+
const row = this.get("SELECT org_id, scopes_json FROM api_keys WHERE id = ? AND revoked_at IS NULL LIMIT 1", [keyId]);
|
|
1307
|
+
if (!row)
|
|
1308
|
+
return { kind: "not_found" };
|
|
1309
|
+
if (String(row.org_id) !== orgId)
|
|
1310
|
+
return { kind: "target_mismatch" };
|
|
1311
|
+
return { kind: "found", scopes: parseScopes(row.scopes_json) };
|
|
1312
|
+
}
|
|
1260
1313
|
async createRun(input) {
|
|
1261
1314
|
if (input.idempotencyKey) {
|
|
1262
1315
|
const existing = this.get("SELECT * FROM skills_runs WHERE org_id = ? AND idempotency_key = ? LIMIT 1", [input.principal.orgId, input.idempotencyKey]);
|
package/bin/server.js
CHANGED
|
@@ -24122,7 +24122,7 @@ var init_dist_es9 = __esm(() => {
|
|
|
24122
24122
|
// package.json
|
|
24123
24123
|
var package_default = {
|
|
24124
24124
|
name: "@hasna/skills",
|
|
24125
|
-
version: "0.9.
|
|
24125
|
+
version: "0.9.4",
|
|
24126
24126
|
description: "Skills library for AI coding agents",
|
|
24127
24127
|
type: "module",
|
|
24128
24128
|
bin: {
|
|
@@ -24131,6 +24131,7 @@ var package_default = {
|
|
|
24131
24131
|
"skills-serve": "bin/server.js",
|
|
24132
24132
|
"skills-server": "bin/server.js",
|
|
24133
24133
|
"skills-worker": "bin/worker.js",
|
|
24134
|
+
"skills-maintenance": "bin/maintenance.js",
|
|
24134
24135
|
"skills-migrate": "bin/migrate.js"
|
|
24135
24136
|
},
|
|
24136
24137
|
exports: {
|
|
@@ -24171,7 +24172,7 @@ var package_default = {
|
|
|
24171
24172
|
types: "./dist/index.d.ts",
|
|
24172
24173
|
scripts: {
|
|
24173
24174
|
clean: "rm -rf bin/ dist/",
|
|
24174
|
-
build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
24175
|
+
build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/server/maintenance.ts --outfile ./bin/maintenance.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
24175
24176
|
"build:js": "rm -rf dist && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
24176
24177
|
test: "bun test --timeout 30000",
|
|
24177
24178
|
dev: "bun run ./src/cli/index.tsx",
|
|
@@ -35319,6 +35320,10 @@ class SkillLifecycleConflictError extends Error {
|
|
|
35319
35320
|
this.name = "SkillLifecycleConflictError";
|
|
35320
35321
|
}
|
|
35321
35322
|
}
|
|
35323
|
+
function validOperatorScopeEnrollmentInput(input) {
|
|
35324
|
+
const bounded = (value, max = 256) => typeof value === "string" && value.length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
|
|
35325
|
+
return bounded(input.keyId) && bounded(input.stationId) && bounded(input.orgId) && bounded(input.operationId) && bounded(input.manifestDigest, 64) && /^[a-f0-9]{64}$/.test(input.manifestDigest) && bounded(input.operatorJobId) && bounded(input.operatorTaskArn, 512) && input.expectedScopes.length > 0 && input.expectedScopes.length <= 32 && input.expectedScopes.every((scope) => bounded(scope, 128) && /^[a-z][a-z0-9_-]*:[a-z][a-z0-9_-]*$/.test(scope));
|
|
35326
|
+
}
|
|
35322
35327
|
|
|
35323
35328
|
// src/lib/revision.ts
|
|
35324
35329
|
import { createHash as createHash5 } from "crypto";
|
|
@@ -35467,6 +35472,54 @@ class SqliteSkillsStore {
|
|
|
35467
35472
|
}
|
|
35468
35473
|
return { kind: "updated", scopes };
|
|
35469
35474
|
}
|
|
35475
|
+
async enrollPublishScopeByOperator(input) {
|
|
35476
|
+
if (!validOperatorScopeEnrollmentInput(input))
|
|
35477
|
+
return { kind: "invalid" };
|
|
35478
|
+
if (input.expectedScopes.includes("skills:publish"))
|
|
35479
|
+
return { kind: "stale", scopes: input.expectedScopes };
|
|
35480
|
+
const operation = this.get("SELECT org_id, target_id FROM skills_audit_events WHERE action = ? AND operator_operation_id = ? LIMIT 1", ["api_key_scopes_added", input.operationId]);
|
|
35481
|
+
if (operation && (String(operation.org_id) !== input.orgId || String(operation.target_id) !== input.keyId))
|
|
35482
|
+
return { kind: "target_mismatch" };
|
|
35483
|
+
const row = this.get("SELECT id, org_id, name, scopes_json FROM api_keys WHERE id = ? AND revoked_at IS NULL LIMIT 1", [input.keyId]);
|
|
35484
|
+
if (!row)
|
|
35485
|
+
return { kind: "not_found" };
|
|
35486
|
+
if (String(row.org_id) !== input.orgId)
|
|
35487
|
+
return { kind: "target_mismatch" };
|
|
35488
|
+
const current = parseScopes(row.scopes_json);
|
|
35489
|
+
const prior = this.get("SELECT metadata_json FROM skills_audit_events WHERE action = ? AND target_type = ? AND target_id = ? AND json_extract(metadata_json, '$.operator_operation_id') = ? LIMIT 1", ["api_key_scopes_added", "api_key", input.keyId, input.operationId]);
|
|
35490
|
+
if (prior) {
|
|
35491
|
+
try {
|
|
35492
|
+
if (JSON.parse(String(prior.metadata_json)).target_manifest_digest !== input.manifestDigest)
|
|
35493
|
+
return { kind: "target_mismatch" };
|
|
35494
|
+
} catch {
|
|
35495
|
+
return { kind: "target_mismatch" };
|
|
35496
|
+
}
|
|
35497
|
+
return current.includes("skills:publish") ? { kind: "already_applied", scopes: current } : { kind: "stale", scopes: current };
|
|
35498
|
+
}
|
|
35499
|
+
if (current.length !== input.expectedScopes.length || current.some((scope, index) => scope !== input.expectedScopes[index]))
|
|
35500
|
+
return { kind: "stale", scopes: current };
|
|
35501
|
+
const scopes = [...current, "skills:publish"];
|
|
35502
|
+
const tx = this.db.transaction(() => {
|
|
35503
|
+
const changed = this.db.run("UPDATE api_keys SET scopes_json = ? WHERE id = ? AND org_id = ? AND revoked_at IS NULL AND scopes_json = ?", [JSON.stringify(scopes), input.keyId, input.orgId, JSON.stringify(input.expectedScopes)]);
|
|
35504
|
+
if (changed.changes !== 1)
|
|
35505
|
+
return false;
|
|
35506
|
+
this.db.run("INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, operator_operation_id, metadata_json) VALUES (?, NULL, NULL, ?, ?, ?, ?, ?)", [input.orgId, "api_key_scopes_added", "api_key", input.keyId, input.operationId, JSON.stringify({ added: ["skills:publish"], scopes, operator_operation_id: input.operationId, operator_job_id: input.operatorJobId, operator_task_arn: input.operatorTaskArn, target_manifest_digest: input.manifestDigest, station_id: input.stationId })]);
|
|
35507
|
+
return true;
|
|
35508
|
+
});
|
|
35509
|
+
if (!tx()) {
|
|
35510
|
+
const latest = this.get("SELECT scopes_json FROM api_keys WHERE id = ? AND org_id = ? AND revoked_at IS NULL LIMIT 1", [input.keyId, input.orgId]);
|
|
35511
|
+
return { kind: "stale", scopes: latest ? parseScopes(latest.scopes_json) : current };
|
|
35512
|
+
}
|
|
35513
|
+
return { kind: "updated", scopes };
|
|
35514
|
+
}
|
|
35515
|
+
async inspectOperatorScopeTarget(keyId, orgId) {
|
|
35516
|
+
const row = this.get("SELECT org_id, scopes_json FROM api_keys WHERE id = ? AND revoked_at IS NULL LIMIT 1", [keyId]);
|
|
35517
|
+
if (!row)
|
|
35518
|
+
return { kind: "not_found" };
|
|
35519
|
+
if (String(row.org_id) !== orgId)
|
|
35520
|
+
return { kind: "target_mismatch" };
|
|
35521
|
+
return { kind: "found", scopes: parseScopes(row.scopes_json) };
|
|
35522
|
+
}
|
|
35470
35523
|
async createRun(input) {
|
|
35471
35524
|
if (input.idempotencyKey) {
|
|
35472
35525
|
const existing = this.get("SELECT * FROM skills_runs WHERE org_id = ? AND idempotency_key = ? LIMIT 1", [input.principal.orgId, input.idempotencyKey]);
|
|
@@ -42891,6 +42944,7 @@ class MemorySkillsStore {
|
|
|
42891
42944
|
bundles = new Map;
|
|
42892
42945
|
versions = new Map;
|
|
42893
42946
|
pins = new Map;
|
|
42947
|
+
operatorEnrollments = new Map;
|
|
42894
42948
|
constructor(apiKeys = []) {
|
|
42895
42949
|
for (const key of apiKeys)
|
|
42896
42950
|
this.addApiKey(key.token, key.principal);
|
|
@@ -42920,6 +42974,41 @@ class MemorySkillsStore {
|
|
|
42920
42974
|
}
|
|
42921
42975
|
return { kind: "not_found" };
|
|
42922
42976
|
}
|
|
42977
|
+
async enrollPublishScopeByOperator(input) {
|
|
42978
|
+
if (!validOperatorScopeEnrollmentInput(input))
|
|
42979
|
+
return { kind: "invalid" };
|
|
42980
|
+
const previous = this.operatorEnrollments.get(input.operationId);
|
|
42981
|
+
if (previous) {
|
|
42982
|
+
if (previous.keyId !== input.keyId || previous.manifestDigest !== input.manifestDigest)
|
|
42983
|
+
return { kind: "target_mismatch" };
|
|
42984
|
+
for (const target of this.apiKeys.values()) {
|
|
42985
|
+
if (target.apiKeyId === input.keyId && target.orgId === input.orgId && target.scopes.includes("skills:publish")) {
|
|
42986
|
+
return { kind: "already_applied", scopes: [...target.scopes] };
|
|
42987
|
+
}
|
|
42988
|
+
}
|
|
42989
|
+
return { kind: "stale", scopes: [] };
|
|
42990
|
+
}
|
|
42991
|
+
for (const [hash2, target] of this.apiKeys) {
|
|
42992
|
+
if (target.apiKeyId !== input.keyId || target.orgId !== input.orgId)
|
|
42993
|
+
continue;
|
|
42994
|
+
if (target.scopes.length !== input.expectedScopes.length || target.scopes.some((scope2, index) => scope2 !== input.expectedScopes[index])) {
|
|
42995
|
+
return { kind: "stale", scopes: [...target.scopes] };
|
|
42996
|
+
}
|
|
42997
|
+
const scopes = target.scopes.includes("skills:publish") ? [...target.scopes] : [...target.scopes, "skills:publish"];
|
|
42998
|
+
this.apiKeys.set(hash2, { ...target, scopes });
|
|
42999
|
+
this.operatorEnrollments.set(input.operationId, { keyId: input.keyId, manifestDigest: input.manifestDigest });
|
|
43000
|
+
return { kind: "updated", scopes };
|
|
43001
|
+
}
|
|
43002
|
+
return { kind: "not_found" };
|
|
43003
|
+
}
|
|
43004
|
+
async inspectOperatorScopeTarget(keyId, orgId) {
|
|
43005
|
+
for (const target of this.apiKeys.values()) {
|
|
43006
|
+
if (target.apiKeyId !== keyId)
|
|
43007
|
+
continue;
|
|
43008
|
+
return target.orgId === orgId ? { kind: "found", scopes: [...target.scopes] } : { kind: "target_mismatch" };
|
|
43009
|
+
}
|
|
43010
|
+
return { kind: "not_found" };
|
|
43011
|
+
}
|
|
42923
43012
|
async createRun(input) {
|
|
42924
43013
|
const idemKey = input.idempotencyKey?.trim();
|
|
42925
43014
|
const idemMapKey = idemKey ? `${input.principal.orgId}:${idemKey}` : undefined;
|
|
@@ -43334,24 +43423,98 @@ class PostgresSkillsStore {
|
|
|
43334
43423
|
if (current.length !== expectedScopes.length || current.some((scope2, index) => scope2 !== expectedScopes[index])) {
|
|
43335
43424
|
return { kind: "stale", scopes: current };
|
|
43336
43425
|
}
|
|
43337
|
-
const
|
|
43338
|
-
const updated = await tx`
|
|
43339
|
-
UPDATE api_keys SET scopes_json = ${JSON.stringify(scopes)}::jsonb
|
|
43340
|
-
WHERE id = ${keyId} AND org_id = ${actor.orgId} AND revoked_at IS NULL
|
|
43341
|
-
AND scopes_json = ${JSON.stringify(expectedScopes)}::jsonb
|
|
43342
|
-
RETURNING scopes_json
|
|
43343
|
-
`;
|
|
43426
|
+
const updated = await this.casUpdateApiKeyScopes(tx, keyId, actor.orgId, expectedScopes, addScopes);
|
|
43344
43427
|
if (!updated[0]) {
|
|
43345
43428
|
const latest = await tx`SELECT scopes_json FROM api_keys WHERE id = ${keyId} AND org_id = ${actor.orgId} AND revoked_at IS NULL LIMIT 1`;
|
|
43346
43429
|
return { kind: "stale", scopes: latest[0] ? parseJsonArray(latest[0].scopes_json) : current };
|
|
43347
43430
|
}
|
|
43348
43431
|
await tx`
|
|
43349
43432
|
INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, metadata_json)
|
|
43350
|
-
VALUES (${actor.orgId}, ${actor.userId}, ${actor.apiKeyId}, ${"api_key_scopes_added"}, ${"api_key"}, ${keyId}, ${JSON.stringify({ added: addScopes, scopes })}::jsonb)
|
|
43433
|
+
VALUES (${actor.orgId}, ${actor.userId}, ${actor.apiKeyId}, ${"api_key_scopes_added"}, ${"api_key"}, ${keyId}, ${JSON.stringify({ added: addScopes, scopes: parseJsonArray(updated[0].scopes_json) })}::jsonb)
|
|
43434
|
+
`;
|
|
43435
|
+
return { kind: "updated", scopes: parseJsonArray(updated[0].scopes_json) };
|
|
43436
|
+
});
|
|
43437
|
+
}
|
|
43438
|
+
async casUpdateApiKeyScopes(tx, keyId, orgId, expectedScopes, addScopes) {
|
|
43439
|
+
const current = expectedScopes;
|
|
43440
|
+
const scopes = [...current, ...addScopes.filter((scope2) => !current.includes(scope2))];
|
|
43441
|
+
return tx`
|
|
43442
|
+
UPDATE api_keys SET scopes_json = ${JSON.stringify(scopes)}::jsonb
|
|
43443
|
+
WHERE id = ${keyId} AND org_id = ${orgId} AND revoked_at IS NULL
|
|
43444
|
+
AND scopes_json = ${JSON.stringify(expectedScopes)}::jsonb
|
|
43445
|
+
RETURNING scopes_json
|
|
43446
|
+
`;
|
|
43447
|
+
}
|
|
43448
|
+
async enrollPublishScopeByOperator(input) {
|
|
43449
|
+
if (!validOperatorScopeEnrollmentInput(input))
|
|
43450
|
+
return { kind: "invalid" };
|
|
43451
|
+
if (input.expectedScopes.includes("skills:publish"))
|
|
43452
|
+
return { kind: "stale", scopes: input.expectedScopes };
|
|
43453
|
+
return this.sql.begin(async (tx) => {
|
|
43454
|
+
const operation = await tx`
|
|
43455
|
+
SELECT org_id, target_id FROM skills_audit_events
|
|
43456
|
+
WHERE action = ${"api_key_scopes_added"} AND operator_operation_id = ${input.operationId}
|
|
43457
|
+
LIMIT 1
|
|
43458
|
+
`;
|
|
43459
|
+
if (operation[0] && (String(operation[0].org_id) !== input.orgId || String(operation[0].target_id) !== input.keyId))
|
|
43460
|
+
return { kind: "target_mismatch" };
|
|
43461
|
+
const rows = await tx`
|
|
43462
|
+
SELECT id, org_id, name, scopes_json FROM api_keys
|
|
43463
|
+
WHERE id = ${input.keyId} AND revoked_at IS NULL
|
|
43464
|
+
FOR UPDATE
|
|
43465
|
+
`;
|
|
43466
|
+
const row = rows[0];
|
|
43467
|
+
if (!row)
|
|
43468
|
+
return { kind: "not_found" };
|
|
43469
|
+
if (String(row.org_id) !== input.orgId)
|
|
43470
|
+
return { kind: "target_mismatch" };
|
|
43471
|
+
const current = parseJsonArray(row.scopes_json);
|
|
43472
|
+
const prior = await tx`
|
|
43473
|
+
SELECT metadata_json, metadata_json->>'target_manifest_digest' AS manifest_digest FROM skills_audit_events
|
|
43474
|
+
WHERE action = ${"api_key_scopes_added"} AND target_type = ${"api_key"} AND target_id = ${input.keyId}
|
|
43475
|
+
AND operator_operation_id = ${input.operationId}
|
|
43476
|
+
LIMIT 1
|
|
43477
|
+
`;
|
|
43478
|
+
if (prior[0]) {
|
|
43479
|
+
const priorMetadata = typeof prior[0].metadata_json === "string" ? JSON.parse(prior[0].metadata_json) : prior[0].metadata_json;
|
|
43480
|
+
const priorDigest = prior[0].manifest_digest ?? priorMetadata?.target_manifest_digest;
|
|
43481
|
+
if (String(priorDigest) !== input.manifestDigest)
|
|
43482
|
+
return { kind: "target_mismatch" };
|
|
43483
|
+
if (!current.includes("skills:publish"))
|
|
43484
|
+
return { kind: "stale", scopes: current };
|
|
43485
|
+
return { kind: "already_applied", scopes: current };
|
|
43486
|
+
}
|
|
43487
|
+
if (current.length !== input.expectedScopes.length || current.some((scope2, index) => scope2 !== input.expectedScopes[index])) {
|
|
43488
|
+
return { kind: "stale", scopes: current };
|
|
43489
|
+
}
|
|
43490
|
+
const updated = await this.casUpdateApiKeyScopes(tx, input.keyId, input.orgId, input.expectedScopes, ["skills:publish"]);
|
|
43491
|
+
if (!updated[0])
|
|
43492
|
+
return { kind: "stale", scopes: current };
|
|
43493
|
+
const scopes = parseJsonArray(updated[0].scopes_json);
|
|
43494
|
+
await tx`
|
|
43495
|
+
INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, operator_operation_id, metadata_json)
|
|
43496
|
+
VALUES (${input.orgId}, NULL, NULL, ${"api_key_scopes_added"}, ${"api_key"}, ${input.keyId}, ${input.operationId}, ${JSON.stringify({
|
|
43497
|
+
added: ["skills:publish"],
|
|
43498
|
+
scopes,
|
|
43499
|
+
operator_operation_id: input.operationId,
|
|
43500
|
+
operator_job_id: input.operatorJobId,
|
|
43501
|
+
operator_task_arn: input.operatorTaskArn,
|
|
43502
|
+
target_manifest_digest: input.manifestDigest,
|
|
43503
|
+
station_id: input.stationId
|
|
43504
|
+
})}::jsonb)
|
|
43351
43505
|
`;
|
|
43352
43506
|
return { kind: "updated", scopes };
|
|
43353
43507
|
});
|
|
43354
43508
|
}
|
|
43509
|
+
async inspectOperatorScopeTarget(keyId, orgId) {
|
|
43510
|
+
const rows = await this.sql`SELECT org_id, scopes_json FROM api_keys WHERE id = ${keyId} AND revoked_at IS NULL LIMIT 1`;
|
|
43511
|
+
const row = rows[0];
|
|
43512
|
+
if (!row)
|
|
43513
|
+
return { kind: "not_found" };
|
|
43514
|
+
if (String(row.org_id) !== orgId)
|
|
43515
|
+
return { kind: "target_mismatch" };
|
|
43516
|
+
return { kind: "found", scopes: parseJsonArray(row.scopes_json) };
|
|
43517
|
+
}
|
|
43355
43518
|
async withContext(orgId, worker, fn) {
|
|
43356
43519
|
return this.sql.begin(async (tx) => {
|
|
43357
43520
|
await tx`SELECT set_config('app.skills_org_id', ${orgId ?? ""}, true)`;
|
package/bin/worker.js
CHANGED
|
@@ -24125,7 +24125,7 @@ import { randomUUID as randomUUID6 } from "crypto";
|
|
|
24125
24125
|
// package.json
|
|
24126
24126
|
var package_default = {
|
|
24127
24127
|
name: "@hasna/skills",
|
|
24128
|
-
version: "0.9.
|
|
24128
|
+
version: "0.9.4",
|
|
24129
24129
|
description: "Skills library for AI coding agents",
|
|
24130
24130
|
type: "module",
|
|
24131
24131
|
bin: {
|
|
@@ -24134,6 +24134,7 @@ var package_default = {
|
|
|
24134
24134
|
"skills-serve": "bin/server.js",
|
|
24135
24135
|
"skills-server": "bin/server.js",
|
|
24136
24136
|
"skills-worker": "bin/worker.js",
|
|
24137
|
+
"skills-maintenance": "bin/maintenance.js",
|
|
24137
24138
|
"skills-migrate": "bin/migrate.js"
|
|
24138
24139
|
},
|
|
24139
24140
|
exports: {
|
|
@@ -24174,7 +24175,7 @@ var package_default = {
|
|
|
24174
24175
|
types: "./dist/index.d.ts",
|
|
24175
24176
|
scripts: {
|
|
24176
24177
|
clean: "rm -rf bin/ dist/",
|
|
24177
|
-
build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
24178
|
+
build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/server/maintenance.ts --outfile ./bin/maintenance.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
24178
24179
|
"build:js": "rm -rf dist && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
24179
24180
|
test: "bun test --timeout 30000",
|
|
24180
24181
|
dev: "bun run ./src/cli/index.tsx",
|
|
@@ -35067,6 +35068,10 @@ class SkillLifecycleConflictError extends Error {
|
|
|
35067
35068
|
this.name = "SkillLifecycleConflictError";
|
|
35068
35069
|
}
|
|
35069
35070
|
}
|
|
35071
|
+
function validOperatorScopeEnrollmentInput(input) {
|
|
35072
|
+
const bounded = (value, max = 256) => typeof value === "string" && value.length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
|
|
35073
|
+
return bounded(input.keyId) && bounded(input.stationId) && bounded(input.orgId) && bounded(input.operationId) && bounded(input.manifestDigest, 64) && /^[a-f0-9]{64}$/.test(input.manifestDigest) && bounded(input.operatorJobId) && bounded(input.operatorTaskArn, 512) && input.expectedScopes.length > 0 && input.expectedScopes.length <= 32 && input.expectedScopes.every((scope) => bounded(scope, 128) && /^[a-z][a-z0-9_-]*:[a-z][a-z0-9_-]*$/.test(scope));
|
|
35074
|
+
}
|
|
35070
35075
|
|
|
35071
35076
|
// src/lib/revision.ts
|
|
35072
35077
|
import { createHash as createHash4 } from "crypto";
|
|
@@ -35214,6 +35219,54 @@ class SqliteSkillsStore {
|
|
|
35214
35219
|
}
|
|
35215
35220
|
return { kind: "updated", scopes };
|
|
35216
35221
|
}
|
|
35222
|
+
async enrollPublishScopeByOperator(input) {
|
|
35223
|
+
if (!validOperatorScopeEnrollmentInput(input))
|
|
35224
|
+
return { kind: "invalid" };
|
|
35225
|
+
if (input.expectedScopes.includes("skills:publish"))
|
|
35226
|
+
return { kind: "stale", scopes: input.expectedScopes };
|
|
35227
|
+
const operation = this.get("SELECT org_id, target_id FROM skills_audit_events WHERE action = ? AND operator_operation_id = ? LIMIT 1", ["api_key_scopes_added", input.operationId]);
|
|
35228
|
+
if (operation && (String(operation.org_id) !== input.orgId || String(operation.target_id) !== input.keyId))
|
|
35229
|
+
return { kind: "target_mismatch" };
|
|
35230
|
+
const row = this.get("SELECT id, org_id, name, scopes_json FROM api_keys WHERE id = ? AND revoked_at IS NULL LIMIT 1", [input.keyId]);
|
|
35231
|
+
if (!row)
|
|
35232
|
+
return { kind: "not_found" };
|
|
35233
|
+
if (String(row.org_id) !== input.orgId)
|
|
35234
|
+
return { kind: "target_mismatch" };
|
|
35235
|
+
const current = parseScopes(row.scopes_json);
|
|
35236
|
+
const prior = this.get("SELECT metadata_json FROM skills_audit_events WHERE action = ? AND target_type = ? AND target_id = ? AND json_extract(metadata_json, '$.operator_operation_id') = ? LIMIT 1", ["api_key_scopes_added", "api_key", input.keyId, input.operationId]);
|
|
35237
|
+
if (prior) {
|
|
35238
|
+
try {
|
|
35239
|
+
if (JSON.parse(String(prior.metadata_json)).target_manifest_digest !== input.manifestDigest)
|
|
35240
|
+
return { kind: "target_mismatch" };
|
|
35241
|
+
} catch {
|
|
35242
|
+
return { kind: "target_mismatch" };
|
|
35243
|
+
}
|
|
35244
|
+
return current.includes("skills:publish") ? { kind: "already_applied", scopes: current } : { kind: "stale", scopes: current };
|
|
35245
|
+
}
|
|
35246
|
+
if (current.length !== input.expectedScopes.length || current.some((scope, index) => scope !== input.expectedScopes[index]))
|
|
35247
|
+
return { kind: "stale", scopes: current };
|
|
35248
|
+
const scopes = [...current, "skills:publish"];
|
|
35249
|
+
const tx = this.db.transaction(() => {
|
|
35250
|
+
const changed = this.db.run("UPDATE api_keys SET scopes_json = ? WHERE id = ? AND org_id = ? AND revoked_at IS NULL AND scopes_json = ?", [JSON.stringify(scopes), input.keyId, input.orgId, JSON.stringify(input.expectedScopes)]);
|
|
35251
|
+
if (changed.changes !== 1)
|
|
35252
|
+
return false;
|
|
35253
|
+
this.db.run("INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, operator_operation_id, metadata_json) VALUES (?, NULL, NULL, ?, ?, ?, ?, ?)", [input.orgId, "api_key_scopes_added", "api_key", input.keyId, input.operationId, JSON.stringify({ added: ["skills:publish"], scopes, operator_operation_id: input.operationId, operator_job_id: input.operatorJobId, operator_task_arn: input.operatorTaskArn, target_manifest_digest: input.manifestDigest, station_id: input.stationId })]);
|
|
35254
|
+
return true;
|
|
35255
|
+
});
|
|
35256
|
+
if (!tx()) {
|
|
35257
|
+
const latest = this.get("SELECT scopes_json FROM api_keys WHERE id = ? AND org_id = ? AND revoked_at IS NULL LIMIT 1", [input.keyId, input.orgId]);
|
|
35258
|
+
return { kind: "stale", scopes: latest ? parseScopes(latest.scopes_json) : current };
|
|
35259
|
+
}
|
|
35260
|
+
return { kind: "updated", scopes };
|
|
35261
|
+
}
|
|
35262
|
+
async inspectOperatorScopeTarget(keyId, orgId) {
|
|
35263
|
+
const row = this.get("SELECT org_id, scopes_json FROM api_keys WHERE id = ? AND revoked_at IS NULL LIMIT 1", [keyId]);
|
|
35264
|
+
if (!row)
|
|
35265
|
+
return { kind: "not_found" };
|
|
35266
|
+
if (String(row.org_id) !== orgId)
|
|
35267
|
+
return { kind: "target_mismatch" };
|
|
35268
|
+
return { kind: "found", scopes: parseScopes(row.scopes_json) };
|
|
35269
|
+
}
|
|
35217
35270
|
async createRun(input) {
|
|
35218
35271
|
if (input.idempotencyKey) {
|
|
35219
35272
|
const existing = this.get("SELECT * FROM skills_runs WHERE org_id = ? AND idempotency_key = ? LIMIT 1", [input.principal.orgId, input.idempotencyKey]);
|
|
@@ -36427,6 +36480,7 @@ class MemorySkillsStore {
|
|
|
36427
36480
|
bundles = new Map;
|
|
36428
36481
|
versions = new Map;
|
|
36429
36482
|
pins = new Map;
|
|
36483
|
+
operatorEnrollments = new Map;
|
|
36430
36484
|
constructor(apiKeys = []) {
|
|
36431
36485
|
for (const key of apiKeys)
|
|
36432
36486
|
this.addApiKey(key.token, key.principal);
|
|
@@ -36456,6 +36510,41 @@ class MemorySkillsStore {
|
|
|
36456
36510
|
}
|
|
36457
36511
|
return { kind: "not_found" };
|
|
36458
36512
|
}
|
|
36513
|
+
async enrollPublishScopeByOperator(input) {
|
|
36514
|
+
if (!validOperatorScopeEnrollmentInput(input))
|
|
36515
|
+
return { kind: "invalid" };
|
|
36516
|
+
const previous = this.operatorEnrollments.get(input.operationId);
|
|
36517
|
+
if (previous) {
|
|
36518
|
+
if (previous.keyId !== input.keyId || previous.manifestDigest !== input.manifestDigest)
|
|
36519
|
+
return { kind: "target_mismatch" };
|
|
36520
|
+
for (const target of this.apiKeys.values()) {
|
|
36521
|
+
if (target.apiKeyId === input.keyId && target.orgId === input.orgId && target.scopes.includes("skills:publish")) {
|
|
36522
|
+
return { kind: "already_applied", scopes: [...target.scopes] };
|
|
36523
|
+
}
|
|
36524
|
+
}
|
|
36525
|
+
return { kind: "stale", scopes: [] };
|
|
36526
|
+
}
|
|
36527
|
+
for (const [hash, target] of this.apiKeys) {
|
|
36528
|
+
if (target.apiKeyId !== input.keyId || target.orgId !== input.orgId)
|
|
36529
|
+
continue;
|
|
36530
|
+
if (target.scopes.length !== input.expectedScopes.length || target.scopes.some((scope, index) => scope !== input.expectedScopes[index])) {
|
|
36531
|
+
return { kind: "stale", scopes: [...target.scopes] };
|
|
36532
|
+
}
|
|
36533
|
+
const scopes = target.scopes.includes("skills:publish") ? [...target.scopes] : [...target.scopes, "skills:publish"];
|
|
36534
|
+
this.apiKeys.set(hash, { ...target, scopes });
|
|
36535
|
+
this.operatorEnrollments.set(input.operationId, { keyId: input.keyId, manifestDigest: input.manifestDigest });
|
|
36536
|
+
return { kind: "updated", scopes };
|
|
36537
|
+
}
|
|
36538
|
+
return { kind: "not_found" };
|
|
36539
|
+
}
|
|
36540
|
+
async inspectOperatorScopeTarget(keyId, orgId) {
|
|
36541
|
+
for (const target of this.apiKeys.values()) {
|
|
36542
|
+
if (target.apiKeyId !== keyId)
|
|
36543
|
+
continue;
|
|
36544
|
+
return target.orgId === orgId ? { kind: "found", scopes: [...target.scopes] } : { kind: "target_mismatch" };
|
|
36545
|
+
}
|
|
36546
|
+
return { kind: "not_found" };
|
|
36547
|
+
}
|
|
36459
36548
|
async createRun(input) {
|
|
36460
36549
|
const idemKey = input.idempotencyKey?.trim();
|
|
36461
36550
|
const idemMapKey = idemKey ? `${input.principal.orgId}:${idemKey}` : undefined;
|
|
@@ -36870,24 +36959,98 @@ class PostgresSkillsStore {
|
|
|
36870
36959
|
if (current.length !== expectedScopes.length || current.some((scope, index) => scope !== expectedScopes[index])) {
|
|
36871
36960
|
return { kind: "stale", scopes: current };
|
|
36872
36961
|
}
|
|
36873
|
-
const
|
|
36874
|
-
const updated = await tx`
|
|
36875
|
-
UPDATE api_keys SET scopes_json = ${JSON.stringify(scopes)}::jsonb
|
|
36876
|
-
WHERE id = ${keyId} AND org_id = ${actor.orgId} AND revoked_at IS NULL
|
|
36877
|
-
AND scopes_json = ${JSON.stringify(expectedScopes)}::jsonb
|
|
36878
|
-
RETURNING scopes_json
|
|
36879
|
-
`;
|
|
36962
|
+
const updated = await this.casUpdateApiKeyScopes(tx, keyId, actor.orgId, expectedScopes, addScopes);
|
|
36880
36963
|
if (!updated[0]) {
|
|
36881
36964
|
const latest = await tx`SELECT scopes_json FROM api_keys WHERE id = ${keyId} AND org_id = ${actor.orgId} AND revoked_at IS NULL LIMIT 1`;
|
|
36882
36965
|
return { kind: "stale", scopes: latest[0] ? parseJsonArray(latest[0].scopes_json) : current };
|
|
36883
36966
|
}
|
|
36884
36967
|
await tx`
|
|
36885
36968
|
INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, metadata_json)
|
|
36886
|
-
VALUES (${actor.orgId}, ${actor.userId}, ${actor.apiKeyId}, ${"api_key_scopes_added"}, ${"api_key"}, ${keyId}, ${JSON.stringify({ added: addScopes, scopes })}::jsonb)
|
|
36969
|
+
VALUES (${actor.orgId}, ${actor.userId}, ${actor.apiKeyId}, ${"api_key_scopes_added"}, ${"api_key"}, ${keyId}, ${JSON.stringify({ added: addScopes, scopes: parseJsonArray(updated[0].scopes_json) })}::jsonb)
|
|
36970
|
+
`;
|
|
36971
|
+
return { kind: "updated", scopes: parseJsonArray(updated[0].scopes_json) };
|
|
36972
|
+
});
|
|
36973
|
+
}
|
|
36974
|
+
async casUpdateApiKeyScopes(tx, keyId, orgId, expectedScopes, addScopes) {
|
|
36975
|
+
const current = expectedScopes;
|
|
36976
|
+
const scopes = [...current, ...addScopes.filter((scope) => !current.includes(scope))];
|
|
36977
|
+
return tx`
|
|
36978
|
+
UPDATE api_keys SET scopes_json = ${JSON.stringify(scopes)}::jsonb
|
|
36979
|
+
WHERE id = ${keyId} AND org_id = ${orgId} AND revoked_at IS NULL
|
|
36980
|
+
AND scopes_json = ${JSON.stringify(expectedScopes)}::jsonb
|
|
36981
|
+
RETURNING scopes_json
|
|
36982
|
+
`;
|
|
36983
|
+
}
|
|
36984
|
+
async enrollPublishScopeByOperator(input) {
|
|
36985
|
+
if (!validOperatorScopeEnrollmentInput(input))
|
|
36986
|
+
return { kind: "invalid" };
|
|
36987
|
+
if (input.expectedScopes.includes("skills:publish"))
|
|
36988
|
+
return { kind: "stale", scopes: input.expectedScopes };
|
|
36989
|
+
return this.sql.begin(async (tx) => {
|
|
36990
|
+
const operation = await tx`
|
|
36991
|
+
SELECT org_id, target_id FROM skills_audit_events
|
|
36992
|
+
WHERE action = ${"api_key_scopes_added"} AND operator_operation_id = ${input.operationId}
|
|
36993
|
+
LIMIT 1
|
|
36994
|
+
`;
|
|
36995
|
+
if (operation[0] && (String(operation[0].org_id) !== input.orgId || String(operation[0].target_id) !== input.keyId))
|
|
36996
|
+
return { kind: "target_mismatch" };
|
|
36997
|
+
const rows = await tx`
|
|
36998
|
+
SELECT id, org_id, name, scopes_json FROM api_keys
|
|
36999
|
+
WHERE id = ${input.keyId} AND revoked_at IS NULL
|
|
37000
|
+
FOR UPDATE
|
|
37001
|
+
`;
|
|
37002
|
+
const row = rows[0];
|
|
37003
|
+
if (!row)
|
|
37004
|
+
return { kind: "not_found" };
|
|
37005
|
+
if (String(row.org_id) !== input.orgId)
|
|
37006
|
+
return { kind: "target_mismatch" };
|
|
37007
|
+
const current = parseJsonArray(row.scopes_json);
|
|
37008
|
+
const prior = await tx`
|
|
37009
|
+
SELECT metadata_json, metadata_json->>'target_manifest_digest' AS manifest_digest FROM skills_audit_events
|
|
37010
|
+
WHERE action = ${"api_key_scopes_added"} AND target_type = ${"api_key"} AND target_id = ${input.keyId}
|
|
37011
|
+
AND operator_operation_id = ${input.operationId}
|
|
37012
|
+
LIMIT 1
|
|
37013
|
+
`;
|
|
37014
|
+
if (prior[0]) {
|
|
37015
|
+
const priorMetadata = typeof prior[0].metadata_json === "string" ? JSON.parse(prior[0].metadata_json) : prior[0].metadata_json;
|
|
37016
|
+
const priorDigest = prior[0].manifest_digest ?? priorMetadata?.target_manifest_digest;
|
|
37017
|
+
if (String(priorDigest) !== input.manifestDigest)
|
|
37018
|
+
return { kind: "target_mismatch" };
|
|
37019
|
+
if (!current.includes("skills:publish"))
|
|
37020
|
+
return { kind: "stale", scopes: current };
|
|
37021
|
+
return { kind: "already_applied", scopes: current };
|
|
37022
|
+
}
|
|
37023
|
+
if (current.length !== input.expectedScopes.length || current.some((scope, index) => scope !== input.expectedScopes[index])) {
|
|
37024
|
+
return { kind: "stale", scopes: current };
|
|
37025
|
+
}
|
|
37026
|
+
const updated = await this.casUpdateApiKeyScopes(tx, input.keyId, input.orgId, input.expectedScopes, ["skills:publish"]);
|
|
37027
|
+
if (!updated[0])
|
|
37028
|
+
return { kind: "stale", scopes: current };
|
|
37029
|
+
const scopes = parseJsonArray(updated[0].scopes_json);
|
|
37030
|
+
await tx`
|
|
37031
|
+
INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, operator_operation_id, metadata_json)
|
|
37032
|
+
VALUES (${input.orgId}, NULL, NULL, ${"api_key_scopes_added"}, ${"api_key"}, ${input.keyId}, ${input.operationId}, ${JSON.stringify({
|
|
37033
|
+
added: ["skills:publish"],
|
|
37034
|
+
scopes,
|
|
37035
|
+
operator_operation_id: input.operationId,
|
|
37036
|
+
operator_job_id: input.operatorJobId,
|
|
37037
|
+
operator_task_arn: input.operatorTaskArn,
|
|
37038
|
+
target_manifest_digest: input.manifestDigest,
|
|
37039
|
+
station_id: input.stationId
|
|
37040
|
+
})}::jsonb)
|
|
36887
37041
|
`;
|
|
36888
37042
|
return { kind: "updated", scopes };
|
|
36889
37043
|
});
|
|
36890
37044
|
}
|
|
37045
|
+
async inspectOperatorScopeTarget(keyId, orgId) {
|
|
37046
|
+
const rows = await this.sql`SELECT org_id, scopes_json FROM api_keys WHERE id = ${keyId} AND revoked_at IS NULL LIMIT 1`;
|
|
37047
|
+
const row = rows[0];
|
|
37048
|
+
if (!row)
|
|
37049
|
+
return { kind: "not_found" };
|
|
37050
|
+
if (String(row.org_id) !== orgId)
|
|
37051
|
+
return { kind: "target_mismatch" };
|
|
37052
|
+
return { kind: "found", scopes: parseJsonArray(row.scopes_json) };
|
|
37053
|
+
}
|
|
36891
37054
|
async withContext(orgId, worker, fn) {
|
|
36892
37055
|
return this.sql.begin(async (tx) => {
|
|
36893
37056
|
await tx`SELECT set_config('app.skills_org_id', ${orgId ?? ""}, true)`;
|