@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/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.2",
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.2",
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.2",
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 scopes = [...current, ...addScopes.filter((scope2) => !current.includes(scope2))];
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.2",
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 scopes = [...current, ...addScopes.filter((scope) => !current.includes(scope))];
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)`;