@hasna/instructions 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +141 -10
  2. package/dist/chunks/{apply-ynfn1av7.js → apply-c3sd4agz.js} +5 -3
  3. package/dist/chunks/{apply-sfzja1f3.js → apply-pnn7a3y7.js} +6 -3
  4. package/dist/chunks/database-wstmr56z.js +23 -0
  5. package/dist/chunks/{index-pkscjnj5.js → index-3djthcm6.js} +0 -1
  6. package/dist/chunks/index-5d7zdftc.js +186 -0
  7. package/dist/chunks/{index-1hpwskwd.js → index-6jncnnw7.js} +318 -329
  8. package/dist/chunks/index-8rb7ng2y.js +148 -0
  9. package/dist/chunks/{index-v7dkb384.js → index-aj66f6xr.js} +243 -100
  10. package/dist/chunks/index-fpeyzn3n.js +186 -0
  11. package/dist/chunks/{index-9p9gn7gh.js → index-jg8ptcwh.js} +5 -7
  12. package/dist/chunks/index-mvckkf15.js +363 -0
  13. package/dist/chunks/index-n3fjspr0.js +119 -0
  14. package/dist/chunks/{index-85n5wnd8.js → index-pdpg8a44.js} +5 -7
  15. package/dist/chunks/{index-e9epbsvs.js → index-r7nfef2g.js} +15 -4
  16. package/dist/chunks/{index-axhwg61p.js → index-ypcef3a2.js} +15 -4
  17. package/dist/chunks/legacy-store-migration-z2m5yzyk.js +322 -0
  18. package/dist/chunks/{local-a9nen65z.js → local-0a9gja58.js} +142 -2
  19. package/dist/chunks/{local-0d8qrc92.js → local-969ye538.js} +153 -188
  20. package/dist/chunks/s3-backup-hxe3zpfz.js +240 -0
  21. package/dist/chunks/s3-config-hhght91k.js +14 -0
  22. package/dist/chunks/s3-object-store-n0s82swt.js +14 -0
  23. package/dist/chunks/{sync-vy1yjvs6.js → sync-q5hvwq6e.js} +4 -4
  24. package/dist/chunks/{sync-m46fc257.js → sync-xtkbm5br.js} +5 -4
  25. package/dist/chunks/{template-02wjvv05.js → template-fsfxpe4p.js} +1 -1
  26. package/dist/chunks/{template-ke972qqe.js → template-wgnzdn7h.js} +1 -1
  27. package/dist/cli/index.js +1236 -256
  28. package/dist/data/config-store.d.ts +10 -1
  29. package/dist/data/config-store.d.ts.map +1 -1
  30. package/dist/db/configs.d.ts +4 -1
  31. package/dist/db/configs.d.ts.map +1 -1
  32. package/dist/db/local.d.ts +1 -1
  33. package/dist/db/local.d.ts.map +1 -1
  34. package/dist/index.js +1086 -215
  35. package/dist/lib/apply.d.ts +7 -0
  36. package/dist/lib/apply.d.ts.map +1 -1
  37. package/dist/lib/compact-output.d.ts +3 -18
  38. package/dist/lib/compact-output.d.ts.map +1 -1
  39. package/dist/lib/export.d.ts +22 -3
  40. package/dist/lib/export.d.ts.map +1 -1
  41. package/dist/lib/import.d.ts +18 -0
  42. package/dist/lib/import.d.ts.map +1 -1
  43. package/dist/lib/legacy-store-migration.d.ts +39 -0
  44. package/dist/lib/legacy-store-migration.d.ts.map +1 -0
  45. package/dist/lib/managed-skill-runtimes.d.ts +1 -0
  46. package/dist/lib/managed-skill-runtimes.d.ts.map +1 -1
  47. package/dist/lib/sync-dir.d.ts.map +1 -1
  48. package/dist/mcp/index.js +41 -30
  49. package/dist/mcp/server.d.ts.map +1 -1
  50. package/dist/sdk/index.d.ts +2 -0
  51. package/dist/sdk/index.d.ts.map +1 -1
  52. package/dist/sdk/index.js +229 -10
  53. package/dist/sdk/resolve.d.ts +1 -1
  54. package/dist/sdk/resolve.d.ts.map +1 -1
  55. package/dist/sdk/v1-client.d.ts +58 -0
  56. package/dist/sdk/v1-client.d.ts.map +1 -0
  57. package/dist/sdk/v1.generated.d.ts +242 -21
  58. package/dist/sdk/v1.generated.d.ts.map +1 -1
  59. package/dist/server/cloud.d.ts.map +1 -1
  60. package/dist/server/index.d.ts +13 -5
  61. package/dist/server/index.d.ts.map +1 -1
  62. package/dist/server/index.js +1261 -427
  63. package/dist/server/openapi.d.ts +1379 -93
  64. package/dist/server/openapi.d.ts.map +1 -1
  65. package/dist/server/v1.d.ts +25 -1
  66. package/dist/server/v1.d.ts.map +1 -1
  67. package/dist/storage/cloud-store.d.ts +45 -2
  68. package/dist/storage/cloud-store.d.ts.map +1 -1
  69. package/dist/storage/index.d.ts +7 -0
  70. package/dist/storage/index.d.ts.map +1 -0
  71. package/dist/storage/index.js +744 -0
  72. package/dist/storage/s3-backup.d.ts +76 -0
  73. package/dist/storage/s3-backup.d.ts.map +1 -0
  74. package/dist/storage/s3-config.d.ts +46 -0
  75. package/dist/storage/s3-config.d.ts.map +1 -0
  76. package/dist/storage/s3-object-store.d.ts +70 -0
  77. package/dist/storage/s3-object-store.d.ts.map +1 -0
  78. package/dist/types/index.d.ts +98 -3
  79. package/dist/types/index.d.ts.map +1 -1
  80. package/hasna.contract.json +110 -0
  81. package/migrations/0001_instructions.sql +63 -0
  82. package/migrations/0002_api_keys.sql +20 -0
  83. package/migrations/0003_idempotency_receipts.sql +24 -0
  84. package/package.json +24 -13
  85. package/assets/skills/inbox/SKILL.md +0 -86
@@ -70,9 +70,9 @@ function parseCursor(value) {
70
70
  const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
71
71
  if (!Number.isFinite(parsed) || parsed < 0)
72
72
  return 0;
73
- return Math.floor(parsed);
73
+ return Math.min(Math.floor(parsed), MAX_LIST_CURSOR);
74
74
  }
75
- var DEFAULT_LIST_LIMIT = 20, MAX_LIST_LIMIT = 100;
75
+ var DEFAULT_LIST_LIMIT = 20, MAX_LIST_LIMIT = 100, MAX_LIST_CURSOR = 1e5;
76
76
 
77
77
  // src/lib/bounded-read.ts
78
78
  function normalizeBoundedReadOptions(options = {}) {
@@ -105,7 +105,7 @@ function boundedReadPage(items, total, options = {}) {
105
105
  }
106
106
  var init_bounded_read = () => {};
107
107
 
108
- // ../contracts/dist/client/transport.js
108
+ // ../../node_modules/.bun/@hasna+contracts@1.0.2+e8014c875821e0be/node_modules/@hasna/contracts/dist/client/transport.js
109
109
  import { createRequire } from "module";
110
110
  var MAX_CREDENTIAL_FILE_BYTES, INSPECT_CUSTOM, CREDENTIAL_SEAL, AMBIENT_ENVIRONMENT, SECRETS_PACKAGE_SPECIFIER, requireSecretsSdk, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
111
111
  var init_transport = __esm(() => {
@@ -125,7 +125,7 @@ var init_transport = __esm(() => {
125
125
  ]);
126
126
  });
127
127
 
128
- // ../contracts/dist/client/storage.js
128
+ // ../../node_modules/.bun/@hasna+contracts@1.0.2+e8014c875821e0be/node_modules/@hasna/contracts/dist/client/storage.js
129
129
  import { createRequire as createRequire2 } from "module";
130
130
  var MAX_CREDENTIAL_FILE_BYTES2, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2, SECRETS_PACKAGE_SPECIFIER2, requireSecretsSdk2, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2;
131
131
  var init_storage = __esm(() => {
@@ -5156,7 +5156,7 @@ var init_machine = __esm(() => {
5156
5156
  init_template();
5157
5157
  });
5158
5158
 
5159
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/compose.js
5159
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/compose.js
5160
5160
  var compose = (middleware, onError, onNotFound) => {
5161
5161
  return (context, next) => {
5162
5162
  let index = -1;
@@ -5200,10 +5200,10 @@ var compose = (middleware, onError, onNotFound) => {
5200
5200
  };
5201
5201
  };
5202
5202
 
5203
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/request/constants.js
5203
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/request/constants.js
5204
5204
  var GET_MATCH_RESULT = /* @__PURE__ */ Symbol();
5205
5205
 
5206
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/utils/buffer.js
5206
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/utils/buffer.js
5207
5207
  var bufferToFormData = (arrayBuffer, contentType) => {
5208
5208
  const response = new Response(arrayBuffer, {
5209
5209
  headers: {
@@ -5213,7 +5213,9 @@ var bufferToFormData = (arrayBuffer, contentType) => {
5213
5213
  return response.formData();
5214
5214
  };
5215
5215
 
5216
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/utils/body.js
5216
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/utils/body.js
5217
+ var MAX_NESTING_DEPTH = 32;
5218
+ var MAX_NESTED_OBJECTS = 1e4;
5217
5219
  var isRawRequest = (request) => ("headers" in request);
5218
5220
  var parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) => {
5219
5221
  const { all = false, dot = false } = options;
@@ -5243,6 +5245,7 @@ async function parseFormData(request, options) {
5243
5245
  }
5244
5246
  function convertFormDataToBodyData(formData, options) {
5245
5247
  const form = /* @__PURE__ */ Object.create(null);
5248
+ const nestingState = { count: 0 };
5246
5249
  formData.forEach((value, key) => {
5247
5250
  const shouldParseAllValues = options.all || key.endsWith("[]");
5248
5251
  if (!shouldParseAllValues) {
@@ -5255,7 +5258,7 @@ function convertFormDataToBodyData(formData, options) {
5255
5258
  Object.entries(form).forEach(([key, value]) => {
5256
5259
  const shouldParseDotValues = key.includes(".");
5257
5260
  if (shouldParseDotValues) {
5258
- handleParsingNestedValues(form, key, value);
5261
+ handleParsingNestedValues(form, key, value, nestingState);
5259
5262
  delete form[key];
5260
5263
  }
5261
5264
  });
@@ -5277,25 +5280,34 @@ var handleParsingAllValues = (form, key, value) => {
5277
5280
  }
5278
5281
  }
5279
5282
  };
5280
- var handleParsingNestedValues = (form, key, value) => {
5283
+ var handleParsingNestedValues = (form, key, value, state) => {
5281
5284
  if (/(?:^|\.)__proto__\./.test(key)) {
5282
5285
  return;
5283
5286
  }
5284
5287
  let nestedForm = form;
5285
- const keys = key.split(".");
5288
+ const keys = key.split(".", MAX_NESTING_DEPTH + 2);
5289
+ if (keys.length > MAX_NESTING_DEPTH + 1) {
5290
+ throwNestingLimitExceeded();
5291
+ }
5286
5292
  keys.forEach((key2, index) => {
5287
5293
  if (index === keys.length - 1) {
5288
5294
  nestedForm[key2] = value;
5289
5295
  } else {
5290
5296
  if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
5297
+ if (state.count++ >= MAX_NESTED_OBJECTS) {
5298
+ throwNestingLimitExceeded();
5299
+ }
5291
5300
  nestedForm[key2] = /* @__PURE__ */ Object.create(null);
5292
5301
  }
5293
5302
  nestedForm = nestedForm[key2];
5294
5303
  }
5295
5304
  });
5296
5305
  };
5306
+ var throwNestingLimitExceeded = () => {
5307
+ throw new Error("Nesting limit exceeded");
5308
+ };
5297
5309
 
5298
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/utils/url.js
5310
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/utils/url.js
5299
5311
  var splitPath = (path) => {
5300
5312
  const paths = path.split("/");
5301
5313
  if (paths[0] === "") {
@@ -5425,6 +5437,10 @@ var _decodeURI = (value) => {
5425
5437
  return tryDecodeURIComponent(value);
5426
5438
  };
5427
5439
  var _getQueryParam = (url, key, multiple) => {
5440
+ const hashIndex = url.indexOf("#", 8);
5441
+ if (hashIndex !== -1) {
5442
+ url = url.slice(0, hashIndex);
5443
+ }
5428
5444
  let encoded;
5429
5445
  if (!multiple && key && key.indexOf("%") === -1 && key.indexOf("+") === -1) {
5430
5446
  let keyIndex2 = url.indexOf("?", 8);
@@ -5493,7 +5509,7 @@ var getQueryParams = (url, key) => {
5493
5509
  };
5494
5510
  var decodeURIComponent_ = decodeURIComponent;
5495
5511
 
5496
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/request.js
5512
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/request.js
5497
5513
  var HonoRequest = class {
5498
5514
  raw;
5499
5515
  #validatedData;
@@ -5510,13 +5526,13 @@ var HonoRequest = class {
5510
5526
  return key ? this.#getDecodedParam(key) : this.#getAllDecodedParams();
5511
5527
  }
5512
5528
  #getDecodedParam(key) {
5513
- const paramKey = this.#matchResult[0][this.routeIndex][1][key];
5529
+ const paramKey = this.#matchResult[0][this.routeIndex]?.[1][key];
5514
5530
  const param = this.#getParamValue(paramKey);
5515
5531
  return param && tryDecodeURIComponent(param);
5516
5532
  }
5517
5533
  #getAllDecodedParams() {
5518
5534
  const decoded = {};
5519
- const keys = Object.keys(this.#matchResult[0][this.routeIndex][1]);
5535
+ const keys = Object.keys(this.#matchResult[0][this.routeIndex]?.[1] ?? {});
5520
5536
  for (const key of keys) {
5521
5537
  const value = this.#getParamValue(this.#matchResult[0][this.routeIndex][1][key]);
5522
5538
  if (value !== undefined) {
@@ -5604,7 +5620,7 @@ var HonoRequest = class {
5604
5620
  }
5605
5621
  };
5606
5622
 
5607
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/utils/html.js
5623
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/utils/html.js
5608
5624
  var HtmlEscapedCallbackPhase = {
5609
5625
  Stringify: 1,
5610
5626
  BeforeStream: 2,
@@ -5642,7 +5658,7 @@ var resolveCallback = async (str, phase, preserveCallbacks, context, buffer) =>
5642
5658
  }
5643
5659
  };
5644
5660
 
5645
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/context.js
5661
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/context.js
5646
5662
  var TEXT_PLAIN = "text/plain; charset=UTF-8";
5647
5663
  var setDefaultContentType = (contentType, headers) => {
5648
5664
  return {
@@ -5824,7 +5840,7 @@ var Context = class {
5824
5840
  };
5825
5841
  };
5826
5842
 
5827
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router.js
5843
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router.js
5828
5844
  var METHOD_NAME_ALL = "ALL";
5829
5845
  var METHOD_NAME_ALL_LOWERCASE = "all";
5830
5846
  var METHODS = ["get", "post", "put", "delete", "options", "patch", "query"];
@@ -5832,10 +5848,10 @@ var MESSAGE_MATCHER_IS_ALREADY_BUILT = "Can not add a route since the matcher is
5832
5848
  var UnsupportedPathError = class extends Error {
5833
5849
  };
5834
5850
 
5835
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/utils/constants.js
5851
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/utils/constants.js
5836
5852
  var COMPOSED_HANDLER = "__COMPOSED_HANDLER";
5837
5853
 
5838
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/hono-base.js
5854
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/hono-base.js
5839
5855
  var notFoundHandler = (c) => {
5840
5856
  return c.text("404 Not Found", 404);
5841
5857
  };
@@ -5867,13 +5883,14 @@ var Hono = class _Hono {
5867
5883
  const allMethods = [...METHODS, METHOD_NAME_ALL_LOWERCASE];
5868
5884
  allMethods.forEach((method) => {
5869
5885
  this[method] = (args1, ...args) => {
5886
+ const methodName = method.toUpperCase();
5870
5887
  if (typeof args1 === "string") {
5871
5888
  this.#path = args1;
5872
5889
  } else {
5873
- this.#addRoute(method, this.#path, args1);
5890
+ this.#addRoute(methodName, this.#path, args1);
5874
5891
  }
5875
5892
  args.forEach((handler) => {
5876
- this.#addRoute(method, this.#path, handler);
5893
+ this.#addRoute(methodName, this.#path, handler);
5877
5894
  });
5878
5895
  return this;
5879
5896
  };
@@ -5882,9 +5899,10 @@ var Hono = class _Hono {
5882
5899
  for (const p of [path].flat()) {
5883
5900
  this.#path = p;
5884
5901
  for (const m of [method].flat()) {
5885
- handlers.map((handler) => {
5886
- this.#addRoute(m.toUpperCase(), this.#path, handler);
5887
- });
5902
+ const methodName = m.toUpperCase();
5903
+ for (const handler of handlers) {
5904
+ this.#addRoute(methodName, this.#path, handler);
5905
+ }
5888
5906
  }
5889
5907
  }
5890
5908
  return this;
@@ -5989,7 +6007,6 @@ var Hono = class _Hono {
5989
6007
  return this;
5990
6008
  }
5991
6009
  #addRoute(method, path, handler, baseRoutePath) {
5992
- method = method.toUpperCase();
5993
6010
  path = mergePath(this._basePath, path);
5994
6011
  const r = {
5995
6012
  basePath: baseRoutePath !== undefined ? mergePath(this._basePath, baseRoutePath) : this._basePath,
@@ -6060,7 +6077,10 @@ var Hono = class _Hono {
6060
6077
  };
6061
6078
  };
6062
6079
 
6063
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/reg-exp-router/matcher.js
6080
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/utils.js
6081
+ var createNullObject = () => /* @__PURE__ */ Object.create(null);
6082
+
6083
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/matcher.js
6064
6084
  var emptyParam = [];
6065
6085
  function match(method, path) {
6066
6086
  const matchers = this.buildAllMatchers();
@@ -6081,7 +6101,7 @@ function match(method, path) {
6081
6101
  return match2(method, path);
6082
6102
  }
6083
6103
 
6084
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/reg-exp-router/node.js
6104
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/node.js
6085
6105
  var LABEL_REG_EXP_STR = "[^/]+";
6086
6106
  var ONLY_WILDCARD_REG_EXP_STR = ".*";
6087
6107
  var TAIL_WILDCARD_REG_EXP_STR = "(?:|/.*)";
@@ -6109,7 +6129,7 @@ function compareKey(a, b) {
6109
6129
  var Node = class _Node {
6110
6130
  #index;
6111
6131
  #varIndex;
6112
- #children = /* @__PURE__ */ Object.create(null);
6132
+ #children = createNullObject();
6113
6133
  insert(tokens, index, paramMap, context, isStatic) {
6114
6134
  let node = this;
6115
6135
  for (let i = 0, len = tokens.length;i < len; i++) {
@@ -6184,12 +6204,12 @@ var Node = class _Node {
6184
6204
  }
6185
6205
  };
6186
6206
 
6187
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/reg-exp-router/trie.js
6207
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/trie.js
6188
6208
  var Trie = class {
6189
6209
  #context = { varIndex: 0 };
6190
6210
  #root = new Node;
6191
6211
  #index = 0;
6192
- paths = /* @__PURE__ */ Object.create(null);
6212
+ paths = createNullObject();
6193
6213
  insert(path, isStatic) {
6194
6214
  if (isStatic) {
6195
6215
  this.#root.insert(path.split(""), 0, [], this.#context, true);
@@ -6247,18 +6267,12 @@ var Trie = class {
6247
6267
  }
6248
6268
  };
6249
6269
 
6250
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/reg-exp-router/router.js
6251
- var wildcardRegExpCache = /* @__PURE__ */ Object.create(null);
6270
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/router.js
6271
+ var wildcardRegExpCache = createNullObject();
6252
6272
  function buildWildcardRegExp(path) {
6253
- return wildcardRegExpCache[path] ??= new RegExp(path === "*" ? "" : `^${path.replace(/\/\*$|([.\\+*[^\]$()])/g, (_, metaChar) => metaChar ? `\\${metaChar}` : "(?:|/.*)")}$`);
6254
- }
6255
- function clearWildcardRegExpCache() {
6256
- wildcardRegExpCache = /* @__PURE__ */ Object.create(null);
6273
+ return wildcardRegExpCache[path] ??= new RegExp(`^${path.replace(/\/:[^/{}]+(?:\{\[\^\/]\+})?(?=[/{]|$)|\/?\*$|([.\\+*[^\]$()?{}|])/g, (match2, metaChar) => metaChar ? `\\${metaChar}` : match2 === "/*" ? TAIL_WILDCARD_REG_EXP_STR : match2 === "*" ? ONLY_WILDCARD_REG_EXP_STR : `/:${LABEL_REG_EXP_STR}`)}$`);
6257
6274
  }
6258
6275
  function findMiddleware(middleware, path) {
6259
- if (!middleware) {
6260
- return;
6261
- }
6262
6276
  for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) {
6263
6277
  if (buildWildcardRegExp(k).test(path)) {
6264
6278
  return [...middleware[k]];
@@ -6272,8 +6286,8 @@ var RegExpRouter = class {
6272
6286
  #routes;
6273
6287
  #tries;
6274
6288
  constructor() {
6275
- this.#middleware = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
6276
- this.#routes = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
6289
+ this.#middleware = { [METHOD_NAME_ALL]: createNullObject() };
6290
+ this.#routes = { [METHOD_NAME_ALL]: createNullObject() };
6277
6291
  this.#tries = { [METHOD_NAME_ALL]: new Trie };
6278
6292
  }
6279
6293
  #insertPath(method, path) {
@@ -6286,119 +6300,90 @@ var RegExpRouter = class {
6286
6300
  add(method, path, handler) {
6287
6301
  const middleware = this.#middleware;
6288
6302
  const routes = this.#routes;
6289
- if (!middleware || !routes) {
6303
+ if (!middleware) {
6290
6304
  throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
6291
6305
  }
6292
6306
  if (!middleware[method]) {
6293
6307
  this.#tries[method] = new Trie;
6294
- [middleware, routes].forEach((handlerMap) => {
6295
- handlerMap[method] = /* @__PURE__ */ Object.create(null);
6296
- Object.keys(handlerMap[METHOD_NAME_ALL]).forEach((p) => {
6308
+ for (const handlerMap of [middleware, routes]) {
6309
+ handlerMap[method] = createNullObject();
6310
+ for (const p in handlerMap[METHOD_NAME_ALL]) {
6297
6311
  handlerMap[method][p] = [...handlerMap[METHOD_NAME_ALL][p]];
6298
6312
  this.#insertPath(method, p);
6299
- });
6300
- });
6313
+ }
6314
+ }
6301
6315
  }
6302
6316
  if (path === "/*") {
6303
6317
  path = "*";
6304
6318
  }
6305
- const paramCount = (path.match(/\/:/g) || []).length;
6319
+ const methods = method === METHOD_NAME_ALL ? Object.keys(middleware) : [method];
6306
6320
  if (/\*$/.test(path)) {
6307
6321
  const re = buildWildcardRegExp(path);
6308
- Object.keys(middleware).forEach((m) => {
6309
- if ((method === METHOD_NAME_ALL || method === m) && !middleware[m][path]) {
6322
+ for (const m of methods) {
6323
+ if (!middleware[m][path]) {
6310
6324
  this.#insertPath(m, path);
6311
6325
  middleware[m][path] = findMiddleware(middleware[m], path) || findMiddleware(middleware[METHOD_NAME_ALL], path) || [];
6312
6326
  }
6313
- });
6314
- Object.keys(middleware).forEach((m) => {
6315
- if (method === METHOD_NAME_ALL || method === m) {
6316
- Object.keys(middleware[m]).forEach((p) => {
6317
- re.test(p) && middleware[m][p].push([handler, paramCount]);
6318
- });
6319
- }
6320
- });
6321
- Object.keys(routes).forEach((m) => {
6322
- if (method === METHOD_NAME_ALL || method === m) {
6323
- Object.keys(routes[m]).forEach((p) => re.test(p) && routes[m][p].push([handler, paramCount]));
6327
+ }
6328
+ for (const handlerMap of [middleware, routes]) {
6329
+ for (const m of methods) {
6330
+ for (const p in handlerMap[m]) {
6331
+ re.test(p) && handlerMap[m][p].push([handler, path]);
6332
+ }
6324
6333
  }
6325
- });
6334
+ }
6326
6335
  return;
6327
6336
  }
6328
6337
  const paths = checkOptionalParameter(path) || [path];
6329
- for (let i = 0, len = paths.length;i < len; i++) {
6330
- const path2 = paths[i];
6331
- Object.keys(routes).forEach((m) => {
6332
- if (method === METHOD_NAME_ALL || method === m) {
6333
- if (!routes[m][path2]) {
6334
- this.#insertPath(m, path2);
6335
- routes[m][path2] = [
6336
- ...findMiddleware(middleware[m], path2) || findMiddleware(middleware[METHOD_NAME_ALL], path2) || []
6337
- ];
6338
- }
6339
- routes[m][path2].push([handler, paramCount - len + i + 1]);
6338
+ for (const path2 of paths) {
6339
+ for (const m of methods) {
6340
+ if (!routes[m][path2]) {
6341
+ this.#insertPath(m, path2);
6342
+ routes[m][path2] = findMiddleware(middleware[m], path2) || findMiddleware(middleware[METHOD_NAME_ALL], path2) || [];
6340
6343
  }
6341
- });
6344
+ routes[m][path2].push([handler, path2]);
6345
+ }
6342
6346
  }
6343
6347
  }
6344
6348
  match = match;
6345
6349
  buildAllMatchers() {
6346
- const matchers = /* @__PURE__ */ Object.create(null);
6347
- Object.keys(this.#routes).concat(Object.keys(this.#middleware)).forEach((method) => {
6348
- matchers[method] ||= this.#buildMatcher(method);
6349
- });
6350
+ const matchers = createNullObject();
6351
+ for (const method of Object.keys(this.#routes)) {
6352
+ matchers[method] = this.#buildMatcher(method);
6353
+ }
6350
6354
  this.#middleware = this.#routes = this.#tries = undefined;
6351
- clearWildcardRegExpCache();
6355
+ wildcardRegExpCache = createNullObject();
6352
6356
  return matchers;
6353
6357
  }
6354
6358
  #buildMatcher(method) {
6355
6359
  const middleware = this.#middleware[method];
6356
6360
  const routes = this.#routes[method];
6357
6361
  const trie = this.#tries[method];
6358
- const staticMap = /* @__PURE__ */ Object.create(null);
6362
+ const staticMap = createNullObject();
6359
6363
  const handlerData = [];
6360
- [middleware, routes].forEach((r) => {
6364
+ const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp();
6365
+ for (const r of [middleware, routes]) {
6361
6366
  for (const path in r) {
6362
6367
  const handlers = r[path];
6363
6368
  const pathData = trie.paths[path];
6364
6369
  if (!pathData) {
6365
- staticMap[path] = [handlers.map(([h]) => [h, /* @__PURE__ */ Object.create(null)]), emptyParam];
6366
- continue;
6367
- }
6368
- const paramAssoc = pathData[1];
6369
- handlerData[pathData[0]] = handlers.map(([h, paramCount]) => {
6370
- const paramIndexMap = /* @__PURE__ */ Object.create(null);
6371
- paramCount -= 1;
6372
- for (;paramCount >= 0; paramCount--) {
6373
- const [key, value] = paramAssoc[paramCount];
6374
- paramIndexMap[key] = value;
6375
- }
6376
- return [h, paramIndexMap];
6377
- });
6378
- }
6379
- });
6380
- const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp();
6381
- for (let i = 0, len = handlerData.length;i < len; i++) {
6382
- for (let j = 0, len2 = handlerData[i].length;j < len2; j++) {
6383
- const map = handlerData[i][j]?.[1];
6384
- if (!map) {
6370
+ staticMap[path] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam];
6385
6371
  continue;
6386
6372
  }
6387
- const keys = Object.keys(map);
6388
- for (let k = 0, len3 = keys.length;k < len3; k++) {
6389
- map[keys[k]] = paramReplacementMap[map[keys[k]]];
6390
- }
6373
+ handlerData[pathData[0]] = handlers.map(([h, handlerPath]) => [
6374
+ h,
6375
+ trie.paths[handlerPath][1].reduceRight((map, [key], i) => {
6376
+ map[key] = paramReplacementMap[pathData[1][i][1]];
6377
+ return map;
6378
+ }, createNullObject())
6379
+ ]);
6391
6380
  }
6392
6381
  }
6393
- const handlerMap = [];
6394
- for (const i in indexReplacementMap) {
6395
- handlerMap[i] = handlerData[indexReplacementMap[i]];
6396
- }
6397
- return [regexp, handlerMap, staticMap];
6382
+ return [regexp, indexReplacementMap.map((i) => handlerData[i]), staticMap];
6398
6383
  }
6399
6384
  };
6400
6385
 
6401
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/reg-exp-router/prepared-router.js
6386
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/prepared-router.js
6402
6387
  var PreparedRegExpRouter = class {
6403
6388
  name = "PreparedRegExpRouter";
6404
6389
  #matchers;
@@ -6470,7 +6455,7 @@ var PreparedRegExpRouter = class {
6470
6455
  match = match;
6471
6456
  };
6472
6457
 
6473
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/smart-router/router.js
6458
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/smart-router/router.js
6474
6459
  var SmartRouter = class {
6475
6460
  name = "SmartRouter";
6476
6461
  #routers = [];
@@ -6525,12 +6510,12 @@ var SmartRouter = class {
6525
6510
  }
6526
6511
  };
6527
6512
 
6528
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/trie-router/node.js
6529
- var emptyParams = /* @__PURE__ */ Object.create(null);
6513
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/trie-router/node.js
6514
+ var emptyParams = createNullObject();
6530
6515
  var order = 0;
6531
6516
  var Node2 = class _Node2 {
6532
6517
  #methods = [];
6533
- #children = /* @__PURE__ */ Object.create(null);
6518
+ #children = createNullObject();
6534
6519
  #patterns = [];
6535
6520
  #pattern;
6536
6521
  #params = emptyParams;
@@ -6567,7 +6552,7 @@ var Node2 = class _Node2 {
6567
6552
  const m = node.#methods[i];
6568
6553
  const handlerSet = m[method] || m[METHOD_NAME_ALL];
6569
6554
  if (handlerSet) {
6570
- handlerSet.params = /* @__PURE__ */ Object.create(null);
6555
+ handlerSet.params = createNullObject();
6571
6556
  handlerSets.push(handlerSet);
6572
6557
  for (let i2 = 0, len2 = handlerSet.possibleKeys.length;i2 < len2; i2++) {
6573
6558
  const key = handlerSet.possibleKeys[i2];
@@ -6673,7 +6658,7 @@ var Node2 = class _Node2 {
6673
6658
  }
6674
6659
  };
6675
6660
 
6676
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/router/trie-router/router.js
6661
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/router/trie-router/router.js
6677
6662
  var TrieRouter = class {
6678
6663
  name = "TrieRouter";
6679
6664
  #node = new Node2;
@@ -6687,7 +6672,7 @@ var TrieRouter = class {
6687
6672
  }
6688
6673
  };
6689
6674
 
6690
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/hono.js
6675
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/hono.js
6691
6676
  var Hono2 = class extends Hono {
6692
6677
  constructor(options = {}) {
6693
6678
  super(options);
@@ -6697,7 +6682,7 @@ var Hono2 = class extends Hono {
6697
6682
  }
6698
6683
  };
6699
6684
 
6700
- // ../../node_modules/.bun/hono@4.13.3/node_modules/hono/dist/middleware/cors/index.js
6685
+ // ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/middleware/cors/index.js
6701
6686
  var cors = (options) => {
6702
6687
  const opts = {
6703
6688
  origin: "*",
@@ -6814,7 +6799,7 @@ function getPackageVersion() {
6814
6799
  // src/server/v1.ts
6815
6800
  init_types();
6816
6801
 
6817
- // ../contracts/dist/auth/index.js
6802
+ // ../../node_modules/.bun/@hasna+contracts@1.0.2+e8014c875821e0be/node_modules/@hasna/contracts/dist/auth/index.js
6818
6803
  import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
6819
6804
  var MAX_TENANT_ID_LENGTH = 64;
6820
6805
  var TENANT_ID_PATTERN = new RegExp(`^[A-Za-z0-9][A-Za-z0-9._-]{0,${MAX_TENANT_ID_LENGTH - 1}}$`);
@@ -7769,119 +7754,178 @@ function instructionsSchemaSql() {
7769
7754
  ];
7770
7755
  }
7771
7756
 
7772
- // src/server/cloud.ts
7773
- var INSTRUCTIONS_APP_SLUG = "instructions";
7774
- function resolveCloudDatabaseUrl(env = process.env) {
7775
- return env.HASNA_INSTRUCTIONS_DATABASE_URL || env.INSTRUCTIONS_DATABASE_URL || env.DATABASE_URL || undefined;
7776
- }
7777
- function resolveSigningSecret(env = process.env) {
7778
- return env.HASNA_INSTRUCTIONS_API_SIGNING_KEY?.trim() || env.HASNA_API_SIGNING_KEY?.trim() || env.API_KEY_SIGNING_SECRET?.trim() || undefined;
7779
- }
7780
- function isPostgresBackendEnabled(env = process.env) {
7781
- return Boolean(resolveCloudDatabaseUrl(env));
7782
- }
7783
- var cachedClient = null;
7784
- var cachedStore = null;
7785
- var cachedVerifier = null;
7786
- var schemaEnsured = null;
7787
- function getClient() {
7788
- if (cachedClient)
7789
- return cachedClient;
7790
- const url = resolveCloudDatabaseUrl();
7791
- if (!url) {
7792
- throw new Error("Cloud /v1 requires a remote database URL (HASNA_INSTRUCTIONS_DATABASE_URL / INSTRUCTIONS_DATABASE_URL / DATABASE_URL).");
7757
+ // src/storage/cloud-store.ts
7758
+ init_types();
7759
+ init_bounded_read();
7760
+ init_instruction_graph();
7761
+ init_asset_plan();
7762
+ init_machine();
7763
+ import { createHash as createHash2, randomUUID } from "crypto";
7764
+
7765
+ class CollectionChangedWhilePagingError extends Error {
7766
+ constructor(label, detail) {
7767
+ super(`${label} changed while paging: ${detail}`);
7768
+ this.name = "CollectionChangedWhilePagingError";
7793
7769
  }
7794
- const pool2 = createPgPool({
7795
- connectionString: url,
7796
- max: 6,
7797
- idleTimeoutMillis: 30000,
7798
- connectionTimeoutMillis: 15000,
7799
- applicationName: "instructions-serve"
7800
- });
7801
- cachedClient = createQueryClient(pool2);
7802
- return cachedClient;
7803
7770
  }
7804
- function getCloudClient() {
7805
- return getClient();
7771
+ function isBoundedPageConsistencyError(error) {
7772
+ return error instanceof Error && (error.message.startsWith("bounded read returned ") || error.message.startsWith("bounded read did not advance "));
7806
7773
  }
7807
- function authClient() {
7808
- const client = getClient();
7809
- return {
7810
- async many(sql, params = []) {
7811
- return client.many(sql, params);
7812
- },
7813
- async get(sql, params = []) {
7814
- return client.get(sql, params);
7815
- },
7816
- async execute(sql, params = []) {
7817
- await client.execute(sql, params);
7774
+ async function aggregateBoundedCollectionRead(label, readPage, identity, requireAscendingIdentity = false, retryBoundedPageConsistencyErrors = false) {
7775
+ let lastError = null;
7776
+ for (let attempt = 0;attempt < 2; attempt += 1) {
7777
+ try {
7778
+ const items = [];
7779
+ const seen = new Set;
7780
+ let expectedTotal = null;
7781
+ let previousIdentity = null;
7782
+ let cursor = 0;
7783
+ while (true) {
7784
+ let page;
7785
+ try {
7786
+ page = await readPage(cursor);
7787
+ } catch (error) {
7788
+ if (!retryBoundedPageConsistencyErrors || !isBoundedPageConsistencyError(error))
7789
+ throw error;
7790
+ throw new CollectionChangedWhilePagingError(label, error.message);
7791
+ }
7792
+ if (page.cursor !== cursor) {
7793
+ throw new CollectionChangedWhilePagingError(label, `server returned cursor ${page.cursor} for requested cursor ${cursor}`);
7794
+ }
7795
+ if (expectedTotal === null)
7796
+ expectedTotal = page.total;
7797
+ else if (page.total !== expectedTotal) {
7798
+ throw new CollectionChangedWhilePagingError(label, `total changed from ${expectedTotal} to ${page.total}`);
7799
+ }
7800
+ for (const item of page.items) {
7801
+ const key = identity(item);
7802
+ if (!key)
7803
+ throw new CollectionChangedWhilePagingError(label, "an item had no stable identity");
7804
+ if (seen.has(key))
7805
+ throw new CollectionChangedWhilePagingError(label, `duplicate identity ${key}`);
7806
+ if (requireAscendingIdentity && previousIdentity !== null && key <= previousIdentity) {
7807
+ throw new CollectionChangedWhilePagingError(label, `identity order was not strictly increasing at ${key}`);
7808
+ }
7809
+ seen.add(key);
7810
+ previousIdentity = key;
7811
+ items.push(item);
7812
+ }
7813
+ if (page.complete) {
7814
+ if (items.length !== expectedTotal) {
7815
+ throw new CollectionChangedWhilePagingError(label, `received ${items.length} unique rows for total ${expectedTotal}`);
7816
+ }
7817
+ return { items, total: expectedTotal };
7818
+ }
7819
+ if (page.next_cursor === null || page.next_cursor <= cursor) {
7820
+ throw new CollectionChangedWhilePagingError(label, `cursor did not advance from ${cursor}`);
7821
+ }
7822
+ cursor = page.next_cursor;
7823
+ }
7824
+ } catch (error) {
7825
+ if (!(error instanceof CollectionChangedWhilePagingError))
7826
+ throw error;
7827
+ lastError = error;
7818
7828
  }
7819
- };
7820
- }
7821
- function getApiKeyStore() {
7822
- if (cachedStore)
7823
- return cachedStore;
7824
- cachedStore = new ApiKeyStore(authClient());
7825
- return cachedStore;
7829
+ }
7830
+ throw lastError ?? new CollectionChangedWhilePagingError(label, "read did not stabilize");
7826
7831
  }
7827
- async function ensureCloudSchema() {
7828
- if (schemaEnsured)
7829
- return schemaEnsured;
7830
- schemaEnsured = (async () => {
7831
- const client = getClient();
7832
- for (const sql of instructionsSchemaSql()) {
7833
- await client.execute(sql);
7834
- }
7835
- await getApiKeyStore().ensureSchema();
7836
- })().catch((e) => {
7837
- schemaEnsured = null;
7838
- throw e;
7839
- });
7840
- return schemaEnsured;
7832
+ class StoreValidationError extends Error {
7833
+ code;
7834
+ constructor(message, code) {
7835
+ super(message);
7836
+ this.code = code;
7837
+ this.name = "StoreValidationError";
7838
+ }
7841
7839
  }
7842
- var honoMiddlewareCache = new Map;
7843
- function getHonoAuthMiddleware(requiredScopes) {
7844
- const key = requiredScopes.join(",");
7845
- const cached2 = honoMiddlewareCache.get(key);
7846
- if (cached2)
7847
- return cached2;
7848
- const signingSecret = resolveSigningSecret();
7849
- if (!signingSecret) {
7850
- throw new Error("Cloud /v1 auth requires a signing secret (HASNA_INSTRUCTIONS_API_SIGNING_KEY / HASNA_API_SIGNING_KEY / API_KEY_SIGNING_SECRET).");
7840
+
7841
+ class IdempotencyConflictError extends Error {
7842
+ code = "IDEMPOTENCY_KEY_REUSED";
7843
+ constructor() {
7844
+ super("Idempotency-Key was already used for a different request body");
7845
+ this.name = "IdempotencyConflictError";
7846
+ }
7847
+ }
7848
+ var IDEMPOTENCY_SCHEMA_SQL = `CREATE TABLE IF NOT EXISTS instruction_idempotency_receipts (
7849
+ principal TEXT NOT NULL,
7850
+ operation TEXT NOT NULL,
7851
+ idempotency_key TEXT NOT NULL,
7852
+ request_sha256 TEXT NOT NULL,
7853
+ response_status INTEGER,
7854
+ response_body JSONB,
7855
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
7856
+ completed_at TIMESTAMPTZ,
7857
+ PRIMARY KEY (principal, operation, idempotency_key),
7858
+ CHECK (length(principal) BETWEEN 1 AND 512),
7859
+ CHECK (length(operation) BETWEEN 1 AND 255),
7860
+ CHECK (length(idempotency_key) BETWEEN 1 AND 255),
7861
+ CHECK (length(request_sha256) = 64),
7862
+ CHECK (
7863
+ (response_status IS NULL AND response_body IS NULL AND completed_at IS NULL)
7864
+ OR
7865
+ (response_status BETWEEN 200 AND 599 AND response_body IS NOT NULL AND completed_at IS NOT NULL)
7866
+ )
7867
+ )`;
7868
+ function canonicalJson(value) {
7869
+ if (value === null || typeof value === "string" || typeof value === "boolean")
7870
+ return JSON.stringify(value);
7871
+ if (typeof value === "number")
7872
+ return JSON.stringify(value);
7873
+ if (Array.isArray(value))
7874
+ return `[${value.map(canonicalJson).join(",")}]`;
7875
+ if (typeof value === "object") {
7876
+ const record = value;
7877
+ return `{${Object.keys(record).sort().filter((key) => record[key] !== undefined).map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
7851
7878
  }
7852
- const store = getApiKeyStore();
7853
- const mw = honoApiKey({
7854
- app: INSTRUCTIONS_APP_SLUG,
7855
- signingSecret,
7856
- keyStatus: store.keyStatus,
7857
- requiredScopes
7858
- });
7859
- honoMiddlewareCache.set(key, mw);
7860
- return mw;
7879
+ return JSON.stringify(null);
7861
7880
  }
7862
- async function pingCloud() {
7863
- const client = getClient();
7864
- const row = await client.get("SELECT 1 AS ok");
7865
- return row?.ok === 1;
7881
+ function idempotencyBodyDigest(body) {
7882
+ return createHash2("sha256").update(canonicalJson(body)).digest("hex");
7866
7883
  }
7867
- async function closeCloud() {
7868
- if (cachedClient)
7869
- await cachedClient.close();
7870
- cachedClient = null;
7871
- cachedStore = null;
7872
- cachedVerifier = null;
7873
- honoMiddlewareCache.clear();
7874
- schemaEnsured = null;
7875
- honoMiddlewareCache.clear();
7884
+ function hasTransaction(client) {
7885
+ return typeof client.transaction === "function";
7886
+ }
7887
+ var idempotencySchemaReady = new WeakSet;
7888
+ async function ensureIdempotencySchema(client) {
7889
+ if (idempotencySchemaReady.has(client))
7890
+ return;
7891
+ await client.execute(IDEMPOTENCY_SCHEMA_SQL);
7892
+ await client.execute("CREATE INDEX IF NOT EXISTS instruction_idempotency_receipts_created_at_idx ON instruction_idempotency_receipts (created_at)");
7893
+ idempotencySchemaReady.add(client);
7894
+ }
7895
+ async function executeIdempotentRequest(client, input, perform) {
7896
+ if (!hasTransaction(client)) {
7897
+ throw new Error("durable idempotency requires a transactional PostgreSQL client");
7898
+ }
7899
+ await ensureIdempotencySchema(client);
7900
+ const digest = idempotencyBodyDigest(input.body);
7901
+ return client.transaction(async (tx) => {
7902
+ const identity = [input.principal, input.operation, input.key];
7903
+ const inserted = await tx.query(`INSERT INTO instruction_idempotency_receipts
7904
+ (principal, operation, idempotency_key, request_sha256)
7905
+ VALUES ($1,$2,$3,$4)
7906
+ ON CONFLICT (principal, operation, idempotency_key) DO NOTHING
7907
+ RETURNING true AS inserted`, [...identity, digest]);
7908
+ const receipt = await tx.get(`SELECT request_sha256, response_status, response_body
7909
+ FROM instruction_idempotency_receipts
7910
+ WHERE principal = $1 AND operation = $2 AND idempotency_key = $3
7911
+ FOR UPDATE`, identity);
7912
+ if (!receipt)
7913
+ throw new Error("idempotency receipt disappeared during transaction");
7914
+ if (receipt.request_sha256 !== digest)
7915
+ throw new IdempotencyConflictError;
7916
+ if (inserted.rowCount === 0) {
7917
+ if (receipt.response_status === null || receipt.response_body === null) {
7918
+ throw new Error("idempotency receipt is incomplete after serialization");
7919
+ }
7920
+ return { status: Number(receipt.response_status), body: receipt.response_body, replayed: true };
7921
+ }
7922
+ const response = await perform(tx);
7923
+ await tx.execute(`UPDATE instruction_idempotency_receipts
7924
+ SET response_status = $5, response_body = $6::jsonb, completed_at = now()
7925
+ WHERE principal = $1 AND operation = $2 AND idempotency_key = $3 AND request_sha256 = $4`, [...identity, digest, response.status, JSON.stringify(response.body)]);
7926
+ return { ...response, replayed: false };
7927
+ });
7876
7928
  }
7877
-
7878
- // src/storage/cloud-store.ts
7879
- init_types();
7880
- init_bounded_read();
7881
- init_instruction_graph();
7882
- init_asset_plan();
7883
- init_machine();
7884
- import { randomUUID } from "crypto";
7885
7929
  function slugify(name) {
7886
7930
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
7887
7931
  }
@@ -7949,7 +7993,7 @@ async function uniqueSlug(client, name, excludeId) {
7949
7993
  }
7950
7994
  throw new Error(`could not allocate a unique slug for '${name}'`);
7951
7995
  }
7952
- async function listConfigs(client, filter = {}) {
7996
+ function configFilterSql(filter) {
7953
7997
  const conditions = [];
7954
7998
  const params = [];
7955
7999
  const add = (sql, value) => {
@@ -7966,12 +8010,66 @@ async function listConfigs(client, filter = {}) {
7966
8010
  add("is_template = $?", filter.is_template);
7967
8011
  if (filter.search) {
7968
8012
  params.push(`%${filter.search}%`);
7969
- const p = `$${params.length}`;
7970
- conditions.push(`(name ILIKE ${p} OR description ILIKE ${p} OR content ILIKE ${p})`);
8013
+ const parameter = `$${params.length}`;
8014
+ conditions.push(`(name ILIKE ${parameter} OR description ILIKE ${parameter} OR content ILIKE ${parameter})`);
7971
8015
  }
7972
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
7973
- const rows = await client.many(`SELECT * FROM configs ${where} ORDER BY category, name`, params);
7974
- return rows.map(rowToConfig);
8016
+ if (filter.tags?.length) {
8017
+ for (const tag of filter.tags)
8018
+ add("tags @> $?::jsonb", JSON.stringify([tag]));
8019
+ }
8020
+ return {
8021
+ where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "",
8022
+ params
8023
+ };
8024
+ }
8025
+ async function listConfigsPage(client, filter = {}, options = {}) {
8026
+ const normalized = normalizeBoundedReadOptions(options);
8027
+ const { where, params } = configFilterSql(filter);
8028
+ const count = await client.get(`SELECT COUNT(*) AS total FROM configs ${where}`, params);
8029
+ const rows = await client.many(`SELECT * FROM configs ${where} ORDER BY id LIMIT $${params.length + 1} OFFSET $${params.length + 2}`, [...params, normalized.limit, normalized.cursor]);
8030
+ return boundedReadPage(rows.map(rowToConfig), Number(count?.total ?? 0), normalized);
8031
+ }
8032
+ async function listConfigSummariesPage(client, filter = {}, options = {}) {
8033
+ const normalized = normalizeBoundedReadOptions(options);
8034
+ const { where, params } = configFilterSql(filter);
8035
+ const count = await client.get(`SELECT COUNT(*) AS total FROM configs ${where}`, params);
8036
+ const rows = await client.many(`SELECT id, name, slug, kind, category, agent, target_path, format,
8037
+ jsonb_array_length(outputs) AS output_count, description, tags,
8038
+ is_template, version, updated_at
8039
+ FROM configs ${where} ORDER BY id LIMIT $${params.length + 1} OFFSET $${params.length + 2}`, [...params, normalized.limit, normalized.cursor]);
8040
+ return boundedReadPage(rows.map((row) => ({
8041
+ id: row.id,
8042
+ slug: row.slug,
8043
+ name: row.name,
8044
+ category: row.category,
8045
+ agent: row.agent,
8046
+ kind: row.kind,
8047
+ format: row.format,
8048
+ target_path: row.target_path,
8049
+ output_count: Number(row.output_count ?? 0),
8050
+ version: row.version,
8051
+ is_template: Boolean(row.is_template),
8052
+ updated_at: toIso2(row.updated_at),
8053
+ description: row.description,
8054
+ tags: Array.isArray(row.tags) ? row.tags.map(String) : []
8055
+ })), Number(count?.total ?? 0), normalized);
8056
+ }
8057
+ async function listConfigIdentitiesPage(client, filterOrOptions = {}, maybeOptions) {
8058
+ const filterKeys = ["category", "agent", "kind", "is_template", "search", "tags"];
8059
+ const isFilterOnly = maybeOptions === undefined && filterKeys.some((key) => (key in filterOrOptions));
8060
+ const filter = maybeOptions === undefined && !isFilterOnly ? {} : filterOrOptions;
8061
+ const options = maybeOptions === undefined && !isFilterOnly ? filterOrOptions : maybeOptions ?? {};
8062
+ const normalized = normalizeBoundedReadOptions(options);
8063
+ const { where, params } = configFilterSql(filter);
8064
+ const count = await client.get(`SELECT COUNT(*) AS total FROM configs ${where}`, params);
8065
+ const rows = await client.many(`SELECT id, name, slug, kind, category, agent, format, is_template, version, created_at, updated_at, synced_at
8066
+ FROM configs ${where} ORDER BY id LIMIT $${params.length + 1} OFFSET $${params.length + 2}`, [...params, normalized.limit, normalized.cursor]);
8067
+ return boundedReadPage(rows.map((row) => ({
8068
+ ...row,
8069
+ created_at: toIso2(row.created_at),
8070
+ updated_at: toIso2(row.updated_at),
8071
+ synced_at: row.synced_at == null ? null : toIso2(row.synced_at)
8072
+ })), Number(count?.total ?? 0), normalized);
7975
8073
  }
7976
8074
  async function getConfig(client, idOrSlug) {
7977
8075
  const row = await client.get("SELECT * FROM configs WHERE id = $1 OR slug = $1", [idOrSlug]);
@@ -7980,10 +8078,10 @@ async function getConfig(client, idOrSlug) {
7980
8078
  return rowToConfig(row);
7981
8079
  }
7982
8080
  async function createConfig(client, input) {
7983
- if (!input.name || !input.name.trim())
7984
- throw new Error("name is required");
7985
- if (!input.category)
7986
- throw new Error("category is required");
8081
+ if (typeof input.name !== "string" || !input.name.trim())
8082
+ throw new StoreValidationError("name is required", "NAME_REQUIRED");
8083
+ if (typeof input.category !== "string" || !input.category.trim())
8084
+ throw new StoreValidationError("category is required", "CATEGORY_REQUIRED");
7987
8085
  const id = randomUUID();
7988
8086
  const slug = await uniqueSlug(client, input.name);
7989
8087
  const snapshotId = randomUUID();
@@ -8092,10 +8190,20 @@ async function createSnapshotContent(client, idOrSlug, content, version) {
8092
8190
  throw new Error("snapshot insert failed");
8093
8191
  return { id: row.id, config_id: row.config_id, content: row.content, version: Number(row.version), created_at: toIso2(row.created_at) };
8094
8192
  }
8095
- async function listSnapshots(client, idOrSlug) {
8193
+ async function listSnapshotsPage(client, idOrSlug, options = {}) {
8096
8194
  const config = await getConfig(client, idOrSlug);
8097
- const rows = await client.many("SELECT id, config_id, content, version, created_at FROM config_snapshots WHERE config_id = $1 ORDER BY version DESC", [config.id]);
8098
- return rows.map((r) => ({ id: r.id, config_id: r.config_id, content: r.content, version: Number(r.version), created_at: toIso2(r.created_at) }));
8195
+ const normalized = normalizeBoundedReadOptions(options);
8196
+ const count = await client.get("SELECT COUNT(*) AS total FROM config_snapshots WHERE config_id = $1", [config.id]);
8197
+ const rows = await client.many(`SELECT id, config_id, content, version, created_at
8198
+ FROM config_snapshots WHERE config_id = $1
8199
+ ORDER BY version DESC, id LIMIT $2 OFFSET $3`, [config.id, normalized.limit, normalized.cursor]);
8200
+ return boundedReadPage(rows.map((row) => ({
8201
+ id: row.id,
8202
+ config_id: row.config_id,
8203
+ content: row.content,
8204
+ version: Number(row.version),
8205
+ created_at: toIso2(row.created_at)
8206
+ })), Number(count?.total ?? 0), normalized);
8099
8207
  }
8100
8208
  async function getSnapshotById(client, snapshotId) {
8101
8209
  const row = await client.get("SELECT id, config_id, content, version, created_at FROM config_snapshots WHERE id = $1", [snapshotId]);
@@ -8125,40 +8233,31 @@ function rowToProfile(row) {
8125
8233
  updated_at: toIso2(row.updated_at)
8126
8234
  };
8127
8235
  }
8128
- async function listProfiles(client) {
8129
- const profiles = [];
8130
- let cursor = 0;
8131
- while (true) {
8132
- const page = await listProfilesPage(client, { limit: 100, cursor });
8133
- profiles.push(...page.items);
8134
- if (page.complete)
8135
- return profiles;
8136
- cursor = page.next_cursor;
8137
- }
8138
- }
8139
8236
  async function listProfilesPage(client, options = {}) {
8140
8237
  const normalized = normalizeBoundedReadOptions(options);
8141
8238
  const count = await client.get("SELECT COUNT(*) AS total FROM profiles");
8142
- const rows = await client.many("SELECT * FROM profiles ORDER BY name LIMIT $1 OFFSET $2", [normalized.limit, normalized.cursor]);
8239
+ const rows = await client.many("SELECT * FROM profiles ORDER BY id LIMIT $1 OFFSET $2", [normalized.limit, normalized.cursor]);
8143
8240
  return boundedReadPage(rows.map(rowToProfile), Number(count?.total ?? 0), normalized);
8144
8241
  }
8242
+ async function listProfileIdentitiesPage(client, options = {}) {
8243
+ const normalized = normalizeBoundedReadOptions(options);
8244
+ const count = await client.get("SELECT COUNT(*) AS total FROM profiles");
8245
+ const rows = await client.many(`SELECT id, name, slug, created_at, updated_at
8246
+ FROM profiles ORDER BY id LIMIT $1 OFFSET $2`, [normalized.limit, normalized.cursor]);
8247
+ return boundedReadPage(rows.map((row) => ({
8248
+ id: row.id,
8249
+ name: row.name,
8250
+ slug: row.slug,
8251
+ created_at: toIso2(row.created_at),
8252
+ updated_at: toIso2(row.updated_at)
8253
+ })), Number(count?.total ?? 0), normalized);
8254
+ }
8145
8255
  async function getProfile(client, idOrSlug) {
8146
8256
  const row = await client.get("SELECT * FROM profiles WHERE id = $1 OR slug = $1", [idOrSlug]);
8147
8257
  if (!row)
8148
8258
  throw new ProfileNotFoundError(idOrSlug);
8149
8259
  return rowToProfile(row);
8150
8260
  }
8151
- async function getProfileConfigs(client, idOrSlug) {
8152
- const configs = [];
8153
- let cursor = 0;
8154
- while (true) {
8155
- const page = await getProfileConfigsPage(client, idOrSlug, { limit: 100, cursor });
8156
- configs.push(...page.items);
8157
- if (page.complete)
8158
- return configs;
8159
- cursor = page.next_cursor;
8160
- }
8161
- }
8162
8261
  async function getProfileConfigsPage(client, idOrSlug, options = {}) {
8163
8262
  const profile = await getProfile(client, idOrSlug);
8164
8263
  const normalized = normalizeBoundedReadOptions(options);
@@ -8166,13 +8265,13 @@ async function getProfileConfigsPage(client, idOrSlug, options = {}) {
8166
8265
  const rows = await client.many(`SELECT c.* FROM configs c
8167
8266
  JOIN profile_configs pc ON pc.config_id = c.id
8168
8267
  WHERE pc.profile_id = $1
8169
- ORDER BY pc.sort_order
8268
+ ORDER BY pc.sort_order, c.id
8170
8269
  LIMIT $2 OFFSET $3`, [profile.id, normalized.limit, normalized.cursor]);
8171
8270
  return boundedReadPage(rows.map(rowToConfig), Number(count?.total ?? 0), normalized);
8172
8271
  }
8173
8272
  async function createProfile(client, input) {
8174
- if (!input.name || !input.name.trim())
8175
- throw new Error("name is required");
8273
+ if (typeof input.name !== "string" || !input.name.trim())
8274
+ throw new StoreValidationError("name is required", "NAME_REQUIRED");
8176
8275
  const id = randomUUID();
8177
8276
  const slug = await uniqueProfileSlug(client, input.name);
8178
8277
  await client.execute(`INSERT INTO profiles (id, name, slug, description, selectors, variables, created_at, updated_at)
@@ -8224,18 +8323,32 @@ async function addConfigToProfile(client, profileIdOrSlug, configId) {
8224
8323
  await client.execute(`INSERT INTO profile_configs (profile_id, config_id, sort_order)
8225
8324
  VALUES ($1,$2,$3) ON CONFLICT (profile_id, config_id) DO NOTHING`, [profile.id, configId, order2]);
8226
8325
  }
8227
- async function getProfileConfigBindings(client, profileIdOrSlug) {
8228
- const profile = await getProfile(client, profileIdOrSlug);
8229
- const rows = await client.many(`SELECT profile_id, config_id, sort_order, binding
8230
- FROM profile_configs
8231
- WHERE profile_id = $1
8232
- ORDER BY sort_order, config_id`, [profile.id]);
8233
- return rows.map((row) => ({
8326
+ function rowToProfileConfigBinding(row) {
8327
+ return {
8234
8328
  profile_id: row.profile_id,
8235
8329
  config_id: row.config_id,
8236
8330
  sort_order: Number(row.sort_order),
8237
8331
  binding: row.binding == null ? legacyProfileConfigBinding() : normalizeProfileConfigBinding(row.binding)
8238
- }));
8332
+ };
8333
+ }
8334
+ async function getProfileConfigBindingsPage(client, profileIdOrSlug, options = {}) {
8335
+ const profile = await getProfile(client, profileIdOrSlug);
8336
+ const normalized = normalizeBoundedReadOptions(options);
8337
+ const count = await client.get("SELECT COUNT(*) AS total FROM profile_configs WHERE profile_id = $1", [profile.id]);
8338
+ const rows = await client.many(`SELECT profile_id, config_id, sort_order, binding
8339
+ FROM profile_configs
8340
+ WHERE profile_id = $1
8341
+ ORDER BY sort_order, config_id
8342
+ LIMIT $2 OFFSET $3`, [profile.id, normalized.limit, normalized.cursor]);
8343
+ return boundedReadPage(rows.map(rowToProfileConfigBinding), Number(count?.total ?? 0), normalized);
8344
+ }
8345
+ async function getProfileConfigBinding(client, profileId, configId) {
8346
+ const row = await client.get(`SELECT profile_id, config_id, sort_order, binding
8347
+ FROM profile_configs
8348
+ WHERE profile_id = $1 AND config_id = $2`, [profileId, configId]);
8349
+ if (!row)
8350
+ throw new Error(`Config ${configId} is not a member of profile ${profileId}.`);
8351
+ return rowToProfileConfigBinding(row);
8239
8352
  }
8240
8353
  async function setProfileConfigBinding(client, profileIdOrSlug, configId, binding) {
8241
8354
  const profile = await getProfile(client, profileIdOrSlug);
@@ -8243,12 +8356,39 @@ async function setProfileConfigBinding(client, profileIdOrSlug, configId, bindin
8243
8356
  const result = await client.query("UPDATE profile_configs SET binding = $1 WHERE profile_id = $2 AND config_id = $3", [JSON.stringify(normalized), profile.id, configId]);
8244
8357
  if ((result.rowCount ?? 0) !== 1)
8245
8358
  throw new Error(`Config ${configId} is not a member of profile ${profile.slug}.`);
8246
- return (await getProfileConfigBindings(client, profile.id)).find((row) => row.config_id === configId);
8359
+ return getProfileConfigBinding(client, profile.id, configId);
8247
8360
  }
8248
8361
  async function removeConfigFromProfile(client, profileIdOrSlug, configId) {
8249
8362
  const profile = await getProfile(client, profileIdOrSlug);
8250
8363
  await client.execute("DELETE FROM profile_configs WHERE profile_id = $1 AND config_id = $2", [profile.id, configId]);
8251
8364
  }
8365
+ function rowToProfileAssetBinding(row) {
8366
+ return {
8367
+ profile_id: row.profile_id,
8368
+ source_config_id: row.source_config_id,
8369
+ sort_order: Number(row.sort_order),
8370
+ binding: normalizeProfileAssetBinding(row.binding)
8371
+ };
8372
+ }
8373
+ async function getProfileAssetBindingsPage(client, profileIdOrSlug, options = {}) {
8374
+ const profile = await getProfile(client, profileIdOrSlug);
8375
+ const normalized = normalizeBoundedReadOptions(options);
8376
+ const count = await client.get("SELECT COUNT(*) AS total FROM profile_assets WHERE profile_id = $1", [profile.id]);
8377
+ const rows = await client.many(`SELECT profile_id, source_config_id, sort_order, binding
8378
+ FROM profile_assets
8379
+ WHERE profile_id = $1
8380
+ ORDER BY sort_order, asset_key
8381
+ LIMIT $2 OFFSET $3`, [profile.id, normalized.limit, normalized.cursor]);
8382
+ return boundedReadPage(rows.map(rowToProfileAssetBinding), Number(count?.total ?? 0), normalized);
8383
+ }
8384
+ async function getProfileAssetBinding(client, profileId, assetKey) {
8385
+ const row = await client.get(`SELECT profile_id, source_config_id, sort_order, binding
8386
+ FROM profile_assets
8387
+ WHERE profile_id = $1 AND asset_key = $2`, [profileId, assetKey]);
8388
+ if (!row)
8389
+ throw new Error(`Asset ${assetKey} is not a member of profile ${profileId}.`);
8390
+ return rowToProfileAssetBinding(row);
8391
+ }
8252
8392
  async function addAssetToProfile(client, profileIdOrSlug, sourceConfigId, binding) {
8253
8393
  const profile = await getProfile(client, profileIdOrSlug);
8254
8394
  await getConfig(client, sourceConfigId);
@@ -8256,7 +8396,7 @@ async function addAssetToProfile(client, profileIdOrSlug, sourceConfigId, bindin
8256
8396
  const maxRow = await client.get("SELECT MAX(sort_order) AS max_order FROM profile_assets WHERE profile_id = $1", [profile.id]);
8257
8397
  const order2 = (maxRow?.max_order ?? -1) + 1;
8258
8398
  await client.execute("INSERT INTO profile_assets (profile_id, source_config_id, asset_key, sort_order, binding) VALUES ($1,$2,$3,$4,$5::jsonb)", [profile.id, sourceConfigId, normalized.assetKey, order2, JSON.stringify(normalized)]);
8259
- return (await getProfileAssetBindings(client, profile.id)).find((row) => row.binding.assetKey === normalized.assetKey);
8399
+ return getProfileAssetBinding(client, profile.id, normalized.assetKey);
8260
8400
  }
8261
8401
  async function setProfileAssetBinding(client, profileIdOrSlug, assetKey, binding) {
8262
8402
  const profile = await getProfile(client, profileIdOrSlug);
@@ -8266,17 +8406,7 @@ async function setProfileAssetBinding(client, profileIdOrSlug, assetKey, binding
8266
8406
  const result = await client.query("UPDATE profile_assets SET binding = $1::jsonb WHERE profile_id = $2 AND asset_key = $3", [JSON.stringify(normalized), profile.id, assetKey]);
8267
8407
  if ((result.rowCount ?? 0) !== 1)
8268
8408
  throw new Error(`Asset ${assetKey} is not a member of profile ${profile.slug}.`);
8269
- return (await getProfileAssetBindings(client, profile.id)).find((row) => row.binding.assetKey === assetKey);
8270
- }
8271
- async function getProfileAssetBindings(client, profileIdOrSlug) {
8272
- const profile = await getProfile(client, profileIdOrSlug);
8273
- const rows = await client.many("SELECT profile_id, source_config_id, sort_order, binding FROM profile_assets WHERE profile_id = $1 ORDER BY sort_order, asset_key", [profile.id]);
8274
- return rows.map((row) => ({
8275
- profile_id: row.profile_id,
8276
- source_config_id: row.source_config_id,
8277
- sort_order: Number(row.sort_order),
8278
- binding: normalizeProfileAssetBinding(row.binding)
8279
- }));
8409
+ return getProfileAssetBinding(client, profile.id, assetKey);
8280
8410
  }
8281
8411
  async function removeAssetFromProfile(client, profileIdOrSlug, assetKey) {
8282
8412
  const profile = await getProfile(client, profileIdOrSlug);
@@ -8291,39 +8421,33 @@ async function resolveProfileForMachineRead(client, machine, options = {}) {
8291
8421
  const os = (machine.os ?? "").trim().toLowerCase();
8292
8422
  const osFamily = normalizeOsFamily(machine.os);
8293
8423
  const arch = (machine.arch ?? "").trim().toLowerCase();
8294
- let cursor = 0;
8295
- let scanned = 0;
8296
- let total = 0;
8424
+ const readStableProfiles = (queryClient) => aggregateBoundedCollectionRead("profile resolution", (cursor) => listProfilesPage(queryClient, { limit, cursor }), (profile) => profile.id, true, true);
8425
+ const stableRead = hasTransaction(client) ? await client.transaction(async (transaction) => {
8426
+ await transaction.execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY");
8427
+ return readStableProfiles(transaction);
8428
+ }) : await readStableProfiles(client);
8297
8429
  let selected = null;
8298
- while (true) {
8299
- const page = await listProfilesPage(client, { limit, cursor });
8300
- total = page.total;
8301
- scanned += page.items.length;
8302
- for (const p of page.items) {
8303
- if (!profileHasSelectors(p.selectors))
8304
- continue;
8305
- const s = p.selectors;
8306
- const osOk = !s.os?.length || s.os.some((c) => {
8307
- const value = c.trim().toLowerCase();
8308
- return value === os || normalizeOsFamily(c) === osFamily;
8309
- });
8310
- const archOk = !s.arch?.length || s.arch.some((c) => c.trim().toLowerCase() === arch);
8311
- const hostOk = !s.hostnames?.length || s.hostnames.some((c) => c.trim().toLowerCase() === host);
8312
- if (!osOk || !archOk || !hostOk)
8313
- continue;
8314
- const score = (p.selectors.hostnames?.length ? 100 : 0) + (p.selectors.os?.length ? 10 : 0) + (p.selectors.arch?.length ? 10 : 0);
8315
- if (!selected || score > selected.score || score === selected.score && p.name.localeCompare(selected.profile.name) < 0) {
8316
- selected = { profile: p, score };
8317
- }
8430
+ for (const p of stableRead.items) {
8431
+ if (!profileHasSelectors(p.selectors))
8432
+ continue;
8433
+ const s = p.selectors;
8434
+ const osOk = !s.os?.length || s.os.some((c) => {
8435
+ const value = c.trim().toLowerCase();
8436
+ return value === os || normalizeOsFamily(c) === osFamily;
8437
+ });
8438
+ const archOk = !s.arch?.length || s.arch.some((c) => c.trim().toLowerCase() === arch);
8439
+ const hostOk = !s.hostnames?.length || s.hostnames.some((c) => c.trim().toLowerCase() === host);
8440
+ if (!osOk || !archOk || !hostOk)
8441
+ continue;
8442
+ const score = (p.selectors.hostnames?.length ? 100 : 0) + (p.selectors.os?.length ? 10 : 0) + (p.selectors.arch?.length ? 10 : 0);
8443
+ if (!selected || score > selected.score || score === selected.score && p.name.localeCompare(selected.profile.name) < 0) {
8444
+ selected = { profile: p, score };
8318
8445
  }
8319
- if (page.complete)
8320
- break;
8321
- cursor = page.next_cursor;
8322
8446
  }
8323
8447
  return {
8324
8448
  profile: selected?.profile ?? null,
8325
- scanned,
8326
- total,
8449
+ scanned: stableRead.items.length,
8450
+ total: stableRead.total,
8327
8451
  batch_limit: limit,
8328
8452
  source_bounded: true,
8329
8453
  complete: true,
@@ -8340,11 +8464,21 @@ function rowToMachine(row) {
8340
8464
  created_at: toIso2(row.created_at)
8341
8465
  };
8342
8466
  }
8343
- async function listMachines(client) {
8344
- const rows = await client.many("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST");
8345
- return rows.map(rowToMachine);
8467
+ async function listMachinesPage(client, options = {}) {
8468
+ const normalized = normalizeBoundedReadOptions(options);
8469
+ const count = await client.get("SELECT COUNT(*) AS total FROM machines");
8470
+ const rows = await client.many(`SELECT * FROM machines
8471
+ ORDER BY id LIMIT $1 OFFSET $2`, [normalized.limit, normalized.cursor]);
8472
+ return boundedReadPage(rows.map(rowToMachine), Number(count?.total ?? 0), normalized);
8346
8473
  }
8347
- async function registerMachine(client, hostname, os, arch) {
8474
+ async function listMachineIdentitiesPage(client, options = {}) {
8475
+ const normalized = normalizeBoundedReadOptions(options);
8476
+ const count = await client.get("SELECT COUNT(*) AS total FROM machines");
8477
+ const rows = await client.many(`SELECT id, hostname, os, arch, last_applied_at, created_at
8478
+ FROM machines ORDER BY id LIMIT $1 OFFSET $2`, [normalized.limit, normalized.cursor]);
8479
+ return boundedReadPage(rows.map(rowToMachine), Number(count?.total ?? 0), normalized);
8480
+ }
8481
+ async function registerMachine(client, hostname, os, arch) {
8348
8482
  const existing = await client.get("SELECT * FROM machines WHERE hostname = $1", [hostname]);
8349
8483
  if (existing) {
8350
8484
  if (existing.os !== os || existing.arch !== arch) {
@@ -8365,38 +8499,229 @@ async function insertFeedback(client, input) {
8365
8499
  await client.execute("INSERT INTO feedback (id, message, email, category, version, created_at) VALUES ($1,$2,$3,$4,$5,now())", [randomUUID(), input.message, input.email ?? null, input.category ?? "general", input.version ?? null]);
8366
8500
  }
8367
8501
 
8502
+ // src/server/cloud.ts
8503
+ var INSTRUCTIONS_APP_SLUG = "instructions";
8504
+ function resolveCloudDatabaseUrl(env = process.env) {
8505
+ return env.HASNA_INSTRUCTIONS_DATABASE_URL || env.INSTRUCTIONS_DATABASE_URL || env.DATABASE_URL || undefined;
8506
+ }
8507
+ function resolveSigningSecret(env = process.env) {
8508
+ return env.HASNA_INSTRUCTIONS_API_SIGNING_KEY?.trim() || env.HASNA_API_SIGNING_KEY?.trim() || env.API_KEY_SIGNING_SECRET?.trim() || undefined;
8509
+ }
8510
+ function isPostgresBackendEnabled(env = process.env) {
8511
+ return Boolean(resolveCloudDatabaseUrl(env));
8512
+ }
8513
+ var cachedClient = null;
8514
+ var cachedStore = null;
8515
+ var cachedVerifier = null;
8516
+ var schemaEnsured = null;
8517
+ function getClient() {
8518
+ if (cachedClient)
8519
+ return cachedClient;
8520
+ const url = resolveCloudDatabaseUrl();
8521
+ if (!url) {
8522
+ throw new Error("Cloud /v1 requires a remote database URL (HASNA_INSTRUCTIONS_DATABASE_URL / INSTRUCTIONS_DATABASE_URL / DATABASE_URL).");
8523
+ }
8524
+ const pool2 = createPgPool({
8525
+ connectionString: url,
8526
+ max: 6,
8527
+ idleTimeoutMillis: 30000,
8528
+ connectionTimeoutMillis: 15000,
8529
+ applicationName: "instructions-serve"
8530
+ });
8531
+ cachedClient = createQueryClient(pool2);
8532
+ return cachedClient;
8533
+ }
8534
+ function getCloudClient() {
8535
+ return getClient();
8536
+ }
8537
+ function authClient() {
8538
+ const client = getClient();
8539
+ return {
8540
+ async many(sql, params = []) {
8541
+ return client.many(sql, params);
8542
+ },
8543
+ async get(sql, params = []) {
8544
+ return client.get(sql, params);
8545
+ },
8546
+ async execute(sql, params = []) {
8547
+ await client.execute(sql, params);
8548
+ }
8549
+ };
8550
+ }
8551
+ function getApiKeyStore() {
8552
+ if (cachedStore)
8553
+ return cachedStore;
8554
+ cachedStore = new ApiKeyStore(authClient());
8555
+ return cachedStore;
8556
+ }
8557
+ async function ensureCloudSchema() {
8558
+ if (schemaEnsured)
8559
+ return schemaEnsured;
8560
+ schemaEnsured = (async () => {
8561
+ const client = getClient();
8562
+ for (const sql of instructionsSchemaSql()) {
8563
+ await client.execute(sql);
8564
+ }
8565
+ await ensureIdempotencySchema(client);
8566
+ await getApiKeyStore().ensureSchema();
8567
+ })().catch((e) => {
8568
+ schemaEnsured = null;
8569
+ throw e;
8570
+ });
8571
+ return schemaEnsured;
8572
+ }
8573
+ var honoMiddlewareCache = new Map;
8574
+ function getHonoAuthMiddleware(requiredScopes) {
8575
+ const key = requiredScopes.join(",");
8576
+ const cached2 = honoMiddlewareCache.get(key);
8577
+ if (cached2)
8578
+ return cached2;
8579
+ const signingSecret = resolveSigningSecret();
8580
+ if (!signingSecret) {
8581
+ throw new Error("Cloud /v1 auth requires a signing secret (HASNA_INSTRUCTIONS_API_SIGNING_KEY / HASNA_API_SIGNING_KEY / API_KEY_SIGNING_SECRET).");
8582
+ }
8583
+ const store = getApiKeyStore();
8584
+ const mw = honoApiKey({
8585
+ app: INSTRUCTIONS_APP_SLUG,
8586
+ signingSecret,
8587
+ keyStatus: store.keyStatus,
8588
+ requiredScopes
8589
+ });
8590
+ honoMiddlewareCache.set(key, mw);
8591
+ return mw;
8592
+ }
8593
+ async function pingCloud() {
8594
+ const client = getClient();
8595
+ const row = await client.get("SELECT 1 AS ok");
8596
+ return row?.ok === 1;
8597
+ }
8598
+ async function closeCloud() {
8599
+ if (cachedClient)
8600
+ await cachedClient.close();
8601
+ cachedClient = null;
8602
+ cachedStore = null;
8603
+ cachedVerifier = null;
8604
+ honoMiddlewareCache.clear();
8605
+ schemaEnsured = null;
8606
+ honoMiddlewareCache.clear();
8607
+ }
8608
+
8368
8609
  // src/server/v1.ts
8369
8610
  var JSON_HEADERS = { "Content-Type": "application/json" };
8611
+ var MAX_REQUEST_BODY_BYTES = 1024 * 1024;
8612
+ var MAX_SEARCH_QUERY_CHARS = 512;
8613
+ var MAX_IDEMPOTENCY_KEY_CHARS = 255;
8614
+
8615
+ class HttpInputError extends Error {
8616
+ status;
8617
+ code;
8618
+ constructor(status, code, message) {
8619
+ super(message);
8620
+ this.status = status;
8621
+ this.code = code;
8622
+ this.name = "HttpInputError";
8623
+ }
8624
+ }
8370
8625
  function json(body, status = 200) {
8371
8626
  return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
8372
8627
  }
8373
8628
  function errorResponse(status, message, extra) {
8374
8629
  return json({ error: message, ...extra ?? {} }, status);
8375
8630
  }
8376
- function completeLegacyPage(items) {
8631
+ function pagePayload(alias, page) {
8377
8632
  return {
8378
- items,
8379
- total: items.length,
8380
- limit: Math.max(items.length, 1),
8381
- cursor: 0,
8382
- next_cursor: null,
8383
- has_more: false,
8384
- complete: true,
8385
- truncated: false,
8386
- source_bounded: false
8633
+ ...page,
8634
+ [alias]: page.items,
8635
+ count: page.items.length
8387
8636
  };
8388
8637
  }
8638
+ function authenticatedPrincipalAuthority(principal) {
8639
+ if (!principal)
8640
+ throw new Error("authenticated principal is unavailable");
8641
+ return [
8642
+ principal.app,
8643
+ `tenant:${principal.tid ?? "-"}`,
8644
+ `agent:${principal.agent ?? "-"}`,
8645
+ `kid:${principal.kid}`
8646
+ ].join("|");
8647
+ }
8648
+ function readIdempotencyKey(req) {
8649
+ const raw2 = req.headers.get("idempotency-key");
8650
+ if (raw2 === null)
8651
+ return null;
8652
+ const key = raw2.trim();
8653
+ if (key.length < 1 || key.length > MAX_IDEMPOTENCY_KEY_CHARS || /[^\x21-\x7e]/.test(key)) {
8654
+ throw new HttpInputError(400, "INVALID_IDEMPOTENCY_KEY", `Idempotency-Key must contain 1-${MAX_IDEMPOTENCY_KEY_CHARS} visible ASCII characters`);
8655
+ }
8656
+ return key;
8657
+ }
8658
+ async function mutationResponse(req, client, context, operation, requestBody, status, perform) {
8659
+ const key = readIdempotencyKey(req);
8660
+ if (!key)
8661
+ return json(await perform(client), status);
8662
+ const result = await executeIdempotentRequest(client, {
8663
+ principal: authenticatedPrincipalAuthority(context.principal),
8664
+ operation,
8665
+ key,
8666
+ body: requestBody
8667
+ }, async (transaction) => ({ status, body: await perform(transaction) }));
8668
+ return json(result.body, result.status);
8669
+ }
8389
8670
  async function readJson(req) {
8671
+ const declaredLength = req.headers.get("content-length");
8672
+ if (declaredLength !== null) {
8673
+ const parsed = Number(declaredLength);
8674
+ if (!Number.isSafeInteger(parsed) || parsed < 0)
8675
+ return null;
8676
+ if (parsed > MAX_REQUEST_BODY_BYTES) {
8677
+ throw new HttpInputError(413, "REQUEST_BODY_TOO_LARGE", "request body exceeds the 1 MiB limit");
8678
+ }
8679
+ }
8680
+ if (!req.body)
8681
+ return {};
8682
+ const reader = req.body.getReader();
8683
+ const chunks = [];
8684
+ let total = 0;
8390
8685
  try {
8391
- const text = await req.text();
8392
- if (!text)
8393
- return {};
8394
- return JSON.parse(text);
8686
+ while (true) {
8687
+ const { done, value } = await reader.read();
8688
+ if (done)
8689
+ break;
8690
+ if (!value)
8691
+ continue;
8692
+ total += value.byteLength;
8693
+ if (total > MAX_REQUEST_BODY_BYTES) {
8694
+ await reader.cancel();
8695
+ throw new HttpInputError(413, "REQUEST_BODY_TOO_LARGE", "request body exceeds the 1 MiB limit");
8696
+ }
8697
+ chunks.push(value);
8698
+ }
8699
+ } finally {
8700
+ reader.releaseLock();
8701
+ }
8702
+ if (total === 0)
8703
+ return {};
8704
+ const bytes = new Uint8Array(total);
8705
+ let offset = 0;
8706
+ for (const chunk of chunks) {
8707
+ bytes.set(chunk, offset);
8708
+ offset += chunk.byteLength;
8709
+ }
8710
+ try {
8711
+ return JSON.parse(new TextDecoder().decode(bytes));
8395
8712
  } catch {
8396
8713
  return null;
8397
8714
  }
8398
8715
  }
8399
- async function handleV1Request(req, url) {
8716
+ function boundedSearchQuery(value) {
8717
+ if (value === null || value === "")
8718
+ return;
8719
+ if (value.length > MAX_SEARCH_QUERY_CHARS) {
8720
+ throw new HttpInputError(400, "SEARCH_QUERY_TOO_LONG", `search query exceeds ${MAX_SEARCH_QUERY_CHARS} characters`);
8721
+ }
8722
+ return value;
8723
+ }
8724
+ async function handleV1Request(req, url, context = {}) {
8400
8725
  const path = url.pathname;
8401
8726
  if (path !== "/v1" && !path.startsWith("/v1/"))
8402
8727
  return null;
@@ -8404,7 +8729,8 @@ async function handleV1Request(req, url) {
8404
8729
  try {
8405
8730
  await ensureCloudSchema();
8406
8731
  } catch (e) {
8407
- return errorResponse(503, `database unavailable: ${e.message}`);
8732
+ console.error("instructions /v1: database unavailable");
8733
+ return errorResponse(503, "Instructions database is unavailable", { code: "DATABASE_UNAVAILABLE" });
8408
8734
  }
8409
8735
  const client = getCloudClient();
8410
8736
  const segments = path.split("/").filter(Boolean);
@@ -8415,25 +8741,40 @@ async function handleV1Request(req, url) {
8415
8741
  if (resource === "configs") {
8416
8742
  if (!id) {
8417
8743
  if (method === "GET") {
8744
+ const search = boundedSearchQuery(url.searchParams.get("search"));
8418
8745
  const filter = {
8419
8746
  ...url.searchParams.get("category") ? { category: url.searchParams.get("category") } : {},
8420
8747
  ...url.searchParams.get("agent") ? { agent: url.searchParams.get("agent") } : {},
8421
8748
  ...url.searchParams.get("kind") ? { kind: url.searchParams.get("kind") } : {},
8422
- ...url.searchParams.get("search") ? { search: url.searchParams.get("search") } : {}
8749
+ ...search ? { search } : {},
8750
+ ...url.searchParams.getAll("tag").length ? { tags: url.searchParams.getAll("tag") } : {}
8751
+ };
8752
+ const options = {
8753
+ limit: url.searchParams.get("limit") ?? undefined,
8754
+ cursor: url.searchParams.get("cursor") ?? undefined
8423
8755
  };
8424
- const configs = await listConfigs(client, filter);
8425
- return json({ configs, count: configs.length });
8756
+ const view = url.searchParams.get("view");
8757
+ if (view !== null && view !== "identity" && view !== "summary") {
8758
+ return errorResponse(400, "unsupported config view", { code: "INVALID_CONFIG_VIEW" });
8759
+ }
8760
+ if (view === "identity") {
8761
+ const page2 = await listConfigIdentitiesPage(client, filter, options);
8762
+ return json(pagePayload("configs", page2));
8763
+ }
8764
+ if (view === "summary") {
8765
+ const page2 = await listConfigSummariesPage(client, filter, options);
8766
+ return json({ ...page2, count: page2.items.length });
8767
+ }
8768
+ const page = await listConfigsPage(client, filter, options);
8769
+ return json(pagePayload("configs", page));
8426
8770
  }
8427
8771
  if (method === "POST") {
8428
8772
  const body = await readJson(req);
8429
8773
  if (!body)
8430
8774
  return errorResponse(400, "invalid JSON body");
8431
- try {
8432
- const config = await createConfig(client, body);
8433
- return json({ config }, 201);
8434
- } catch (e) {
8435
- return errorResponse(400, e.message);
8436
- }
8775
+ return await mutationResponse(req, client, context, "POST /v1/configs", body, 201, async (transaction) => ({
8776
+ config: await createConfig(transaction, body)
8777
+ }));
8437
8778
  }
8438
8779
  return errorResponse(405, `method ${method} not allowed on /v1/configs`);
8439
8780
  }
@@ -8455,13 +8796,17 @@ async function handleV1Request(req, url) {
8455
8796
  return json({ snapshot });
8456
8797
  }
8457
8798
  if (method === "GET") {
8458
- const snapshots = await listSnapshots(client, id);
8459
- return json({ snapshots, count: snapshots.length });
8799
+ const page = await listSnapshotsPage(client, id, {
8800
+ limit: url.searchParams.get("limit") ?? undefined,
8801
+ cursor: url.searchParams.get("cursor") ?? undefined
8802
+ });
8803
+ return json(pagePayload("snapshots", page));
8460
8804
  }
8461
8805
  if (method === "POST") {
8462
8806
  const body = await readJson(req);
8463
- const snapshot = body && typeof body.content === "string" && typeof body.version === "number" ? await createSnapshotContent(client, id, body.content, body.version) : await createSnapshot(client, id);
8464
- return json({ snapshot }, 201);
8807
+ return await mutationResponse(req, client, context, "POST /v1/configs/:id/snapshots", { config_id: id, ...body ?? {} }, 201, async (transaction) => ({
8808
+ snapshot: body && typeof body.content === "string" && typeof body.version === "number" ? await createSnapshotContent(transaction, id, body.content, body.version) : await createSnapshot(transaction, id)
8809
+ }));
8465
8810
  }
8466
8811
  return errorResponse(405, `method ${method} not allowed on /v1/configs/:id/snapshots`);
8467
8812
  }
@@ -8487,26 +8832,24 @@ async function handleV1Request(req, url) {
8487
8832
  if (resource === "profiles") {
8488
8833
  if (!id) {
8489
8834
  if (method === "GET") {
8490
- if (!url.searchParams.has("limit") && !url.searchParams.has("cursor")) {
8491
- const profiles = await listProfiles(client);
8492
- return json({ ...completeLegacyPage(profiles), profiles, count: profiles.length });
8493
- }
8494
- const page = await listProfilesPage(client, {
8835
+ const options = {
8495
8836
  limit: url.searchParams.get("limit") ?? undefined,
8496
8837
  cursor: url.searchParams.get("cursor") ?? undefined
8497
- });
8498
- return json({ ...page, profiles: page.items, count: page.items.length });
8838
+ };
8839
+ if (url.searchParams.get("view") === "identity") {
8840
+ const page2 = await listProfileIdentitiesPage(client, options);
8841
+ return json(pagePayload("profiles", page2));
8842
+ }
8843
+ const page = await listProfilesPage(client, options);
8844
+ return json(pagePayload("profiles", page));
8499
8845
  }
8500
8846
  if (method === "POST") {
8501
8847
  const body = await readJson(req);
8502
8848
  if (!body)
8503
8849
  return errorResponse(400, "invalid JSON body");
8504
- try {
8505
- const profile = await createProfile(client, body);
8506
- return json({ profile }, 201);
8507
- } catch (e) {
8508
- return errorResponse(400, e.message);
8509
- }
8850
+ return await mutationResponse(req, client, context, "POST /v1/profiles", body, 201, async (transaction) => ({
8851
+ profile: await createProfile(transaction, body)
8852
+ }));
8510
8853
  }
8511
8854
  return errorResponse(405, `method ${method} not allowed on /v1/profiles`);
8512
8855
  }
@@ -8523,8 +8866,11 @@ async function handleV1Request(req, url) {
8523
8866
  if (action === "bindings") {
8524
8867
  if (method !== "GET")
8525
8868
  return errorResponse(405, `method ${method} not allowed on /v1/profiles/:id/bindings`);
8526
- const bindings = await getProfileConfigBindings(client, id);
8527
- return json({ bindings });
8869
+ const page = await getProfileConfigBindingsPage(client, id, {
8870
+ limit: url.searchParams.get("limit") ?? undefined,
8871
+ cursor: url.searchParams.get("cursor") ?? undefined
8872
+ });
8873
+ return json(pagePayload("bindings", page));
8528
8874
  }
8529
8875
  if (action === "configs") {
8530
8876
  const configId = segments[4] ? decodeURIComponent(segments[4]) : undefined;
@@ -8532,8 +8878,10 @@ async function handleV1Request(req, url) {
8532
8878
  const body = await readJson(req);
8533
8879
  if (!body?.config_id)
8534
8880
  return errorResponse(400, "config_id is required");
8535
- await addConfigToProfile(client, id, body.config_id);
8536
- return json({ added: true });
8881
+ return await mutationResponse(req, client, context, "POST /v1/profiles/:id/configs", { profile_id: id, config_id: body.config_id }, 200, async (transaction) => {
8882
+ await addConfigToProfile(transaction, id, body.config_id);
8883
+ return { added: true };
8884
+ });
8537
8885
  }
8538
8886
  if (method === "DELETE" && configId) {
8539
8887
  await removeConfigFromProfile(client, id, configId);
@@ -8543,30 +8891,36 @@ async function handleV1Request(req, url) {
8543
8891
  const body = await readJson(req);
8544
8892
  if (!body?.binding)
8545
8893
  return errorResponse(400, "binding is required");
8546
- const binding = await setProfileConfigBinding(client, id, configId, body.binding);
8547
- return json({ binding });
8894
+ return await mutationResponse(req, client, context, "PUT /v1/profiles/:id/configs/:configId", { profile_id: id, config_id: configId, binding: body.binding }, 200, async (transaction) => ({
8895
+ binding: await setProfileConfigBinding(transaction, id, configId, body.binding)
8896
+ }));
8548
8897
  }
8549
8898
  return errorResponse(405, `method ${method} not allowed on /v1/profiles/:id/configs`);
8550
8899
  }
8551
8900
  if (action === "assets") {
8552
8901
  const assetKey = segments[4] ? decodeURIComponent(segments[4]) : undefined;
8553
8902
  if (method === "GET" && !assetKey) {
8554
- const assets = await getProfileAssetBindings(client, id);
8555
- return json({ assets });
8903
+ const page = await getProfileAssetBindingsPage(client, id, {
8904
+ limit: url.searchParams.get("limit") ?? undefined,
8905
+ cursor: url.searchParams.get("cursor") ?? undefined
8906
+ });
8907
+ return json(pagePayload("assets", page));
8556
8908
  }
8557
8909
  if (method === "POST" && !assetKey) {
8558
8910
  const body = await readJson(req);
8559
8911
  if (!body?.source_config_id || !body.binding)
8560
8912
  return errorResponse(400, "source_config_id and binding are required");
8561
- const asset = await addAssetToProfile(client, id, body.source_config_id, body.binding);
8562
- return json({ asset }, 201);
8913
+ return await mutationResponse(req, client, context, "POST /v1/profiles/:id/assets", { profile_id: id, source_config_id: body.source_config_id, binding: body.binding }, 201, async (transaction) => ({
8914
+ asset: await addAssetToProfile(transaction, id, body.source_config_id, body.binding)
8915
+ }));
8563
8916
  }
8564
8917
  if (method === "PUT" && assetKey) {
8565
8918
  const body = await readJson(req);
8566
8919
  if (!body?.binding)
8567
8920
  return errorResponse(400, "binding is required");
8568
- const asset = await setProfileAssetBinding(client, id, assetKey, body.binding);
8569
- return json({ asset });
8921
+ return await mutationResponse(req, client, context, "PUT /v1/profiles/:id/assets/:assetKey", { profile_id: id, asset_key: assetKey, binding: body.binding }, 200, async (transaction) => ({
8922
+ asset: await setProfileAssetBinding(transaction, id, assetKey, body.binding)
8923
+ }));
8570
8924
  }
8571
8925
  if (method === "DELETE" && assetKey) {
8572
8926
  await removeAssetFromProfile(client, id, assetKey);
@@ -8578,13 +8932,6 @@ async function handleV1Request(req, url) {
8578
8932
  return errorResponse(404, `unknown profile action: ${action}`);
8579
8933
  if (method === "GET") {
8580
8934
  const profile = await getProfile(client, id);
8581
- if (!url.searchParams.has("limit") && !url.searchParams.has("cursor")) {
8582
- const legacyConfigs = await getProfileConfigs(client, id);
8583
- return json({
8584
- profile: { ...profile, configs: legacyConfigs },
8585
- configs: completeLegacyPage(legacyConfigs)
8586
- });
8587
- }
8588
8935
  const configs = await getProfileConfigsPage(client, id, {
8589
8936
  limit: url.searchParams.get("limit") ?? undefined,
8590
8937
  cursor: url.searchParams.get("cursor") ?? undefined
@@ -8629,15 +8976,20 @@ async function handleV1Request(req, url) {
8629
8976
  }
8630
8977
  if (!id) {
8631
8978
  if (method === "GET") {
8632
- const machines = await listMachines(client);
8633
- return json({ machines, count: machines.length });
8979
+ const options = {
8980
+ limit: url.searchParams.get("limit") ?? undefined,
8981
+ cursor: url.searchParams.get("cursor") ?? undefined
8982
+ };
8983
+ const page = url.searchParams.get("view") === "identity" ? await listMachineIdentitiesPage(client, options) : await listMachinesPage(client, options);
8984
+ return json(pagePayload("machines", page));
8634
8985
  }
8635
8986
  if (method === "POST") {
8636
8987
  const body = await readJson(req);
8637
8988
  if (!body?.hostname)
8638
8989
  return errorResponse(400, "hostname is required");
8639
- const machine = await registerMachine(client, body.hostname, body.os ?? null, body.arch ?? null);
8640
- return json({ machine }, 201);
8990
+ return await mutationResponse(req, client, context, "POST /v1/machines", body, 201, async (transaction) => ({
8991
+ machine: await registerMachine(transaction, body.hostname, body.os ?? null, body.arch ?? null)
8992
+ }));
8641
8993
  }
8642
8994
  return errorResponse(405, `method ${method} not allowed on /v1/machines`);
8643
8995
  }
@@ -8659,10 +9011,17 @@ async function handleV1Request(req, url) {
8659
9011
  }
8660
9012
  return errorResponse(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
8661
9013
  } catch (e) {
9014
+ if (e instanceof HttpInputError)
9015
+ return errorResponse(e.status, e.message, { code: e.code });
9016
+ if (e instanceof StoreValidationError)
9017
+ return errorResponse(400, e.message, { code: e.code });
9018
+ if (e instanceof IdempotencyConflictError)
9019
+ return errorResponse(409, e.message, { code: e.code });
8662
9020
  if (e instanceof ConfigNotFoundError || e instanceof ProfileNotFoundError) {
8663
9021
  return errorResponse(404, e.message);
8664
9022
  }
8665
- return errorResponse(500, e.message || "internal error");
9023
+ console.error("instructions /v1: request failed");
9024
+ return errorResponse(500, "Instructions request failed", { code: "INTERNAL_ERROR" });
8666
9025
  }
8667
9026
  }
8668
9027
 
@@ -8689,6 +9048,27 @@ var configSchema = {
8689
9048
  synced_at: { type: "string", nullable: true }
8690
9049
  }
8691
9050
  };
9051
+ var configSummarySchema = {
9052
+ type: "object",
9053
+ additionalProperties: false,
9054
+ required: ["id", "name", "slug", "kind", "category", "agent", "target_path", "format", "output_count", "description", "tags", "is_template", "version", "updated_at"],
9055
+ properties: {
9056
+ id: { type: "string" },
9057
+ name: { type: "string" },
9058
+ slug: { type: "string" },
9059
+ kind: { type: "string" },
9060
+ category: { type: "string" },
9061
+ agent: { type: "string" },
9062
+ target_path: { type: "string", nullable: true },
9063
+ format: { type: "string" },
9064
+ output_count: { type: "number" },
9065
+ description: { type: "string", nullable: true },
9066
+ tags: { type: "array", items: { type: "string" } },
9067
+ is_template: { type: "boolean" },
9068
+ version: { type: "number" },
9069
+ updated_at: { type: "string" }
9070
+ }
9071
+ };
8692
9072
  var profileSchema = {
8693
9073
  type: "object",
8694
9074
  properties: {
@@ -8702,6 +9082,95 @@ var profileSchema = {
8702
9082
  updated_at: { type: "string" }
8703
9083
  }
8704
9084
  };
9085
+ var configIdentitySchema = {
9086
+ type: "object",
9087
+ additionalProperties: false,
9088
+ required: ["id", "name", "slug", "kind", "category", "agent", "format", "is_template", "version", "created_at", "updated_at", "synced_at"],
9089
+ properties: {
9090
+ id: { type: "string" },
9091
+ name: { type: "string" },
9092
+ slug: { type: "string" },
9093
+ kind: { type: "string" },
9094
+ category: { type: "string" },
9095
+ agent: { type: "string" },
9096
+ format: { type: "string" },
9097
+ is_template: { type: "boolean" },
9098
+ version: { type: "number" },
9099
+ created_at: { type: "string" },
9100
+ updated_at: { type: "string" },
9101
+ synced_at: { type: "string", nullable: true }
9102
+ }
9103
+ };
9104
+ var profileIdentitySchema = {
9105
+ type: "object",
9106
+ required: ["id", "name", "slug", "created_at", "updated_at"],
9107
+ properties: {
9108
+ id: { type: "string" },
9109
+ name: { type: "string" },
9110
+ slug: { type: "string" },
9111
+ created_at: { type: "string" },
9112
+ updated_at: { type: "string" }
9113
+ }
9114
+ };
9115
+ var machineSchema = {
9116
+ type: "object",
9117
+ required: ["id", "hostname", "os", "arch", "last_applied_at", "created_at"],
9118
+ properties: {
9119
+ id: { type: "string" },
9120
+ hostname: { type: "string" },
9121
+ os: { type: "string", nullable: true },
9122
+ arch: { type: "string", nullable: true },
9123
+ last_applied_at: { type: "string", nullable: true },
9124
+ created_at: { type: "string" }
9125
+ }
9126
+ };
9127
+ var snapshotSchema = {
9128
+ type: "object",
9129
+ required: ["id", "config_id", "content", "version", "created_at"],
9130
+ properties: {
9131
+ id: { type: "string" },
9132
+ config_id: { type: "string" },
9133
+ content: { type: "string" },
9134
+ version: { type: "number" },
9135
+ created_at: { type: "string" }
9136
+ }
9137
+ };
9138
+ var limitParameter = { name: "limit", in: "query", schema: { type: "integer", minimum: 1, maximum: 100 } };
9139
+ var cursorParameter = { name: "cursor", in: "query", schema: { type: "integer", minimum: 0, maximum: 1e5 } };
9140
+ var configViewParameter = {
9141
+ name: "view",
9142
+ in: "query",
9143
+ description: "Use summary for a content-free list projection, or identity for the smallest metadata-only projection.",
9144
+ schema: { type: "string", enum: ["summary", "identity"] }
9145
+ };
9146
+ var identityViewParameter = {
9147
+ name: "view",
9148
+ in: "query",
9149
+ description: "Use identity for a metadata-only projection that omits instruction content, paths, outputs, and private profile fields.",
9150
+ schema: { type: "string", enum: ["identity"] }
9151
+ };
9152
+ var idempotencyKeyParameter = {
9153
+ name: "Idempotency-Key",
9154
+ in: "header",
9155
+ required: false,
9156
+ description: "Optional visible-ASCII request key (1-255 characters). Replays the first committed response for the same authenticated principal, operation, and canonical JSON body; reuse with a different body returns 409.",
9157
+ schema: { type: "string", minLength: 1, maxLength: 255, pattern: "^[!-~]+$" }
9158
+ };
9159
+ var idempotencyConflictResponse = {
9160
+ description: "The Idempotency-Key was already used for a different request body.",
9161
+ content: {
9162
+ "application/json": {
9163
+ schema: {
9164
+ type: "object",
9165
+ required: ["error", "code"],
9166
+ properties: {
9167
+ error: { type: "string" },
9168
+ code: { type: "string", const: "IDEMPOTENCY_KEY_REUSED" }
9169
+ }
9170
+ }
9171
+ }
9172
+ }
9173
+ };
8705
9174
  function buildV1OpenApiDocument(version = getPackageVersion()) {
8706
9175
  return {
8707
9176
  openapi: "3.1.0",
@@ -8717,7 +9186,12 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
8717
9186
  },
8718
9187
  schemas: {
8719
9188
  Config: configSchema,
9189
+ ConfigSummary: configSummarySchema,
9190
+ ConfigIdentity: configIdentitySchema,
8720
9191
  Profile: profileSchema,
9192
+ ProfileIdentity: profileIdentitySchema,
9193
+ Machine: machineSchema,
9194
+ ConfigSnapshot: snapshotSchema,
8721
9195
  CreateConfigInput: {
8722
9196
  type: "object",
8723
9197
  required: ["name", "category", "content"],
@@ -8738,12 +9212,17 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
8738
9212
  type: "object",
8739
9213
  properties: {
8740
9214
  name: { type: "string" },
9215
+ kind: { type: "string" },
8741
9216
  category: { type: "string" },
8742
9217
  agent: { type: "string" },
9218
+ target_path: { type: "string", nullable: true },
9219
+ outputs: { type: "array", items: { type: "object" } },
9220
+ format: { type: "string" },
8743
9221
  content: { type: "string" },
8744
- description: { type: "string" },
9222
+ description: { type: "string", nullable: true },
8745
9223
  tags: { type: "array", items: { type: "string" } },
8746
- is_template: { type: "boolean" }
9224
+ is_template: { type: "boolean" },
9225
+ synced_at: { type: "string", nullable: true }
8747
9226
  }
8748
9227
  },
8749
9228
  CreateProfileInput: {
@@ -8756,6 +9235,38 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
8756
9235
  variables: { type: "object" }
8757
9236
  }
8758
9237
  },
9238
+ UpdateProfileInput: {
9239
+ type: "object",
9240
+ properties: {
9241
+ name: { type: "string" },
9242
+ description: { type: "string", nullable: true },
9243
+ selectors: { type: "object" },
9244
+ variables: { type: "object" }
9245
+ }
9246
+ },
9247
+ PruneSnapshotsInput: {
9248
+ type: "object",
9249
+ properties: {
9250
+ keep: { type: "integer", minimum: 0, default: 10 }
9251
+ }
9252
+ },
9253
+ MachineAppliedInput: {
9254
+ type: "object",
9255
+ required: ["hostname"],
9256
+ properties: {
9257
+ hostname: { type: "string" }
9258
+ }
9259
+ },
9260
+ FeedbackInput: {
9261
+ type: "object",
9262
+ required: ["message"],
9263
+ properties: {
9264
+ message: { type: "string" },
9265
+ email: { type: "string" },
9266
+ category: { type: "string" },
9267
+ version: { type: "string" }
9268
+ }
9269
+ },
8759
9270
  AddProfileConfigInput: {
8760
9271
  type: "object",
8761
9272
  required: ["config_id"],
@@ -8890,7 +9401,128 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
8890
9401
  type: "object",
8891
9402
  required: ["items", "total", "limit", "cursor", "next_cursor", "has_more", "complete", "truncated", "source_bounded"],
8892
9403
  properties: {
9404
+ configs: { type: "array", items: { $ref: "#/components/schemas/Config" } },
8893
9405
  items: { type: "array", items: { $ref: "#/components/schemas/Config" } },
9406
+ count: { type: "number" },
9407
+ total: { type: "number" },
9408
+ limit: { type: "number" },
9409
+ cursor: { type: "number" },
9410
+ next_cursor: { type: "number", nullable: true },
9411
+ has_more: { type: "boolean" },
9412
+ complete: { type: "boolean" },
9413
+ truncated: { type: "boolean", const: false },
9414
+ source_bounded: { type: "boolean" }
9415
+ }
9416
+ },
9417
+ BoundedConfigSummaryPage: {
9418
+ type: "object",
9419
+ required: ["items", "total", "limit", "cursor", "next_cursor", "has_more", "complete", "truncated", "source_bounded"],
9420
+ properties: {
9421
+ configs: { type: "array", items: { $ref: "#/components/schemas/ConfigSummary" } },
9422
+ items: { type: "array", items: { $ref: "#/components/schemas/ConfigSummary" } },
9423
+ count: { type: "number" },
9424
+ total: { type: "number" },
9425
+ limit: { type: "number" },
9426
+ cursor: { type: "number" },
9427
+ next_cursor: { type: "number", nullable: true },
9428
+ has_more: { type: "boolean" },
9429
+ complete: { type: "boolean" },
9430
+ truncated: { type: "boolean", const: false },
9431
+ source_bounded: { type: "boolean" }
9432
+ }
9433
+ },
9434
+ BoundedConfigIdentityPage: {
9435
+ type: "object",
9436
+ required: ["items", "total", "limit", "cursor", "next_cursor", "has_more", "complete", "truncated", "source_bounded"],
9437
+ properties: {
9438
+ configs: { type: "array", items: { $ref: "#/components/schemas/ConfigIdentity" } },
9439
+ items: { type: "array", items: { $ref: "#/components/schemas/ConfigIdentity" } },
9440
+ count: { type: "number" },
9441
+ total: { type: "number" },
9442
+ limit: { type: "number" },
9443
+ cursor: { type: "number" },
9444
+ next_cursor: { type: "number", nullable: true },
9445
+ has_more: { type: "boolean" },
9446
+ complete: { type: "boolean" },
9447
+ truncated: { type: "boolean", const: false },
9448
+ source_bounded: { type: "boolean" }
9449
+ }
9450
+ },
9451
+ BoundedProfileIdentityPage: {
9452
+ type: "object",
9453
+ required: ["items", "total", "limit", "cursor", "next_cursor", "has_more", "complete", "truncated", "source_bounded"],
9454
+ properties: {
9455
+ profiles: { type: "array", items: { $ref: "#/components/schemas/ProfileIdentity" } },
9456
+ items: { type: "array", items: { $ref: "#/components/schemas/ProfileIdentity" } },
9457
+ count: { type: "number" },
9458
+ total: { type: "number" },
9459
+ limit: { type: "number" },
9460
+ cursor: { type: "number" },
9461
+ next_cursor: { type: "number", nullable: true },
9462
+ has_more: { type: "boolean" },
9463
+ complete: { type: "boolean" },
9464
+ truncated: { type: "boolean", const: false },
9465
+ source_bounded: { type: "boolean" }
9466
+ }
9467
+ },
9468
+ BoundedSnapshotPage: {
9469
+ type: "object",
9470
+ required: ["items", "total", "limit", "cursor", "next_cursor", "has_more", "complete", "truncated", "source_bounded"],
9471
+ properties: {
9472
+ snapshots: { type: "array", items: { $ref: "#/components/schemas/ConfigSnapshot" } },
9473
+ items: { type: "array", items: { $ref: "#/components/schemas/ConfigSnapshot" } },
9474
+ count: { type: "number" },
9475
+ total: { type: "number" },
9476
+ limit: { type: "number" },
9477
+ cursor: { type: "number" },
9478
+ next_cursor: { type: "number", nullable: true },
9479
+ has_more: { type: "boolean" },
9480
+ complete: { type: "boolean" },
9481
+ truncated: { type: "boolean", const: false },
9482
+ source_bounded: { type: "boolean" }
9483
+ }
9484
+ },
9485
+ BoundedMachinePage: {
9486
+ type: "object",
9487
+ required: ["items", "total", "limit", "cursor", "next_cursor", "has_more", "complete", "truncated", "source_bounded"],
9488
+ properties: {
9489
+ machines: { type: "array", items: { $ref: "#/components/schemas/Machine" } },
9490
+ items: { type: "array", items: { $ref: "#/components/schemas/Machine" } },
9491
+ count: { type: "number" },
9492
+ total: { type: "number" },
9493
+ limit: { type: "number" },
9494
+ cursor: { type: "number" },
9495
+ next_cursor: { type: "number", nullable: true },
9496
+ has_more: { type: "boolean" },
9497
+ complete: { type: "boolean" },
9498
+ truncated: { type: "boolean", const: false },
9499
+ source_bounded: { type: "boolean" }
9500
+ }
9501
+ },
9502
+ BoundedProfileConfigBindingPage: {
9503
+ type: "object",
9504
+ required: ["items", "total", "limit", "cursor", "next_cursor", "has_more", "complete", "truncated", "source_bounded"],
9505
+ properties: {
9506
+ bindings: { type: "array", items: { $ref: "#/components/schemas/ProfileConfigBinding" } },
9507
+ items: { type: "array", items: { $ref: "#/components/schemas/ProfileConfigBinding" } },
9508
+ count: { type: "number" },
9509
+ total: { type: "number" },
9510
+ limit: { type: "number" },
9511
+ cursor: { type: "number" },
9512
+ next_cursor: { type: "number", nullable: true },
9513
+ has_more: { type: "boolean" },
9514
+ complete: { type: "boolean" },
9515
+ truncated: { type: "boolean", const: false },
9516
+ source_bounded: { type: "boolean" }
9517
+ }
9518
+ },
9519
+ BoundedProfileAssetBindingPage: {
9520
+ type: "object",
9521
+ required: ["items", "total", "limit", "cursor", "next_cursor", "has_more", "complete", "truncated", "source_bounded"],
9522
+ properties: {
9523
+ assets: { type: "array", items: { $ref: "#/components/schemas/ProfileAssetBinding" } },
9524
+ items: { type: "array", items: { $ref: "#/components/schemas/ProfileAssetBinding" } },
9525
+ count: { type: "number" },
8894
9526
  total: { type: "number" },
8895
9527
  limit: { type: "number" },
8896
9528
  cursor: { type: "number" },
@@ -8934,18 +9566,22 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
8934
9566
  { name: "category", in: "query", schema: { type: "string" } },
8935
9567
  { name: "agent", in: "query", schema: { type: "string" } },
8936
9568
  { name: "kind", in: "query", schema: { type: "string" } },
8937
- { name: "search", in: "query", schema: { type: "string" } }
9569
+ { name: "search", in: "query", schema: { type: "string", maxLength: 512 } },
9570
+ limitParameter,
9571
+ cursorParameter,
9572
+ { name: "tag", in: "query", schema: { type: "array", items: { type: "string" } } },
9573
+ configViewParameter
8938
9574
  ],
8939
9575
  responses: {
8940
9576
  "200": {
8941
9577
  content: {
8942
9578
  "application/json": {
8943
9579
  schema: {
8944
- type: "object",
8945
- properties: {
8946
- configs: { type: "array", items: { $ref: "#/components/schemas/Config" } },
8947
- count: { type: "number" }
8948
- }
9580
+ anyOf: [
9581
+ { $ref: "#/components/schemas/BoundedConfigPage" },
9582
+ { $ref: "#/components/schemas/BoundedConfigSummaryPage" },
9583
+ { $ref: "#/components/schemas/BoundedConfigIdentityPage" }
9584
+ ]
8949
9585
  }
8950
9586
  }
8951
9587
  }
@@ -8955,6 +9591,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
8955
9591
  post: {
8956
9592
  operationId: "createConfig",
8957
9593
  summary: "Create a config",
9594
+ parameters: [idempotencyKeyParameter],
8958
9595
  requestBody: {
8959
9596
  required: true,
8960
9597
  content: { "application/json": { schema: { $ref: "#/components/schemas/CreateConfigInput" } } }
@@ -8966,7 +9603,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
8966
9603
  schema: { type: "object", properties: { config: { $ref: "#/components/schemas/Config" } } }
8967
9604
  }
8968
9605
  }
8969
- }
9606
+ },
9607
+ "409": idempotencyConflictResponse
8970
9608
  }
8971
9609
  }
8972
9610
  },
@@ -9003,6 +9641,24 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9003
9641
  }
9004
9642
  }
9005
9643
  },
9644
+ put: {
9645
+ operationId: "putConfig",
9646
+ summary: "Update a config via PUT",
9647
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9648
+ requestBody: {
9649
+ required: true,
9650
+ content: { "application/json": { schema: { $ref: "#/components/schemas/UpdateConfigInput" } } }
9651
+ },
9652
+ responses: {
9653
+ "200": {
9654
+ content: {
9655
+ "application/json": {
9656
+ schema: { type: "object", properties: { config: { $ref: "#/components/schemas/Config" } } }
9657
+ }
9658
+ }
9659
+ }
9660
+ }
9661
+ },
9006
9662
  delete: {
9007
9663
  operationId: "deleteConfig",
9008
9664
  summary: "Delete a config",
@@ -9022,29 +9678,72 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9022
9678
  get: {
9023
9679
  operationId: "listSnapshots",
9024
9680
  summary: "List a config's version snapshots",
9025
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9026
- responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { snapshots: { type: "array", items: { type: "object" } }, count: { type: "number" } } } } } } }
9681
+ parameters: [
9682
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
9683
+ limitParameter,
9684
+ cursorParameter
9685
+ ],
9686
+ responses: { "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/BoundedSnapshotPage" } } } } }
9027
9687
  },
9028
9688
  post: {
9029
9689
  operationId: "createSnapshot",
9030
9690
  summary: "Snapshot a config's current content",
9691
+ parameters: [
9692
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
9693
+ idempotencyKeyParameter
9694
+ ],
9695
+ requestBody: {
9696
+ required: false,
9697
+ content: { "application/json": { schema: { type: "object", properties: { content: { type: "string" }, version: { type: "integer" } } } } }
9698
+ },
9699
+ responses: {
9700
+ "201": { content: { "application/json": { schema: { type: "object", properties: { snapshot: { $ref: "#/components/schemas/ConfigSnapshot" } } } } } },
9701
+ "409": idempotencyConflictResponse
9702
+ }
9703
+ }
9704
+ },
9705
+ "/v1/configs/{id}/snapshots/prune": {
9706
+ post: {
9707
+ operationId: "pruneSnapshots",
9708
+ summary: "Prune older snapshots for a config",
9031
9709
  parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9032
- responses: { "201": { content: { "application/json": { schema: { type: "object", properties: { snapshot: { type: "object" } } } } } } }
9710
+ requestBody: {
9711
+ required: false,
9712
+ content: { "application/json": { schema: { $ref: "#/components/schemas/PruneSnapshotsInput" } } }
9713
+ },
9714
+ responses: {
9715
+ "200": { content: { "application/json": { schema: { type: "object", required: ["pruned"], properties: { pruned: { type: "integer" } } } } } }
9716
+ }
9717
+ }
9718
+ },
9719
+ "/v1/configs/{id}/snapshots/{version}": {
9720
+ get: {
9721
+ operationId: "getSnapshotByVersion",
9722
+ summary: "Get one config snapshot by version",
9723
+ parameters: [
9724
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
9725
+ { name: "version", in: "path", required: true, schema: { type: "integer" } }
9726
+ ],
9727
+ responses: {
9728
+ "200": { content: { "application/json": { schema: { type: "object", properties: { snapshot: { $ref: "#/components/schemas/ConfigSnapshot" } } } } } }
9729
+ }
9033
9730
  }
9034
9731
  },
9035
9732
  "/v1/profiles": {
9036
9733
  get: {
9037
9734
  operationId: "listProfiles",
9038
9735
  summary: "List profiles with producer-side bounds",
9039
- parameters: [
9040
- { name: "limit", in: "query", schema: { type: "integer", minimum: 1, maximum: 100 } },
9041
- { name: "cursor", in: "query", schema: { type: "integer", minimum: 0 } }
9042
- ],
9736
+ parameters: [limitParameter, cursorParameter, identityViewParameter],
9043
9737
  responses: {
9044
9738
  "200": {
9045
9739
  content: {
9046
9740
  "application/json": {
9047
- schema: { $ref: "#/components/schemas/BoundedProfilePage" }
9741
+ schema: {
9742
+ oneOf: [
9743
+ { $ref: "#/components/schemas/BoundedProfilePage" },
9744
+ { $ref: "#/components/schemas/BoundedProfileIdentityPage" }
9745
+ ]
9746
+ }
9048
9747
  }
9049
9748
  }
9050
9749
  }
@@ -9053,11 +9752,15 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9053
9752
  post: {
9054
9753
  operationId: "createProfile",
9055
9754
  summary: "Create a profile",
9755
+ parameters: [idempotencyKeyParameter],
9056
9756
  requestBody: {
9057
9757
  required: true,
9058
9758
  content: { "application/json": { schema: { $ref: "#/components/schemas/CreateProfileInput" } } }
9059
9759
  },
9060
- responses: { "201": { content: { "application/json": { schema: { type: "object", properties: { profile: { $ref: "#/components/schemas/Profile" } } } } } } }
9760
+ responses: {
9761
+ "201": { content: { "application/json": { schema: { type: "object", properties: { profile: { $ref: "#/components/schemas/Profile" } } } } } },
9762
+ "409": idempotencyConflictResponse
9763
+ }
9061
9764
  }
9062
9765
  },
9063
9766
  "/v1/profiles/resolve": {
@@ -9092,6 +9795,31 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9092
9795
  ],
9093
9796
  responses: { "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProfileShowResponse" } } } } }
9094
9797
  },
9798
+ patch: {
9799
+ operationId: "updateProfile",
9800
+ summary: "Update a profile",
9801
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9802
+ requestBody: {
9803
+ required: true,
9804
+ content: { "application/json": { schema: { $ref: "#/components/schemas/UpdateProfileInput" } } }
9805
+ },
9806
+ responses: {
9807
+ "200": { content: { "application/json": { schema: { type: "object", properties: { profile: { $ref: "#/components/schemas/Profile" } } } } } }
9808
+ }
9809
+ },
9810
+ put: {
9811
+ operationId: "putProfile",
9812
+ summary: "Update a profile via PUT",
9813
+ description: "Compatibility update route. Like PATCH, omitted properties remain unchanged.",
9814
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9815
+ requestBody: {
9816
+ required: true,
9817
+ content: { "application/json": { schema: { $ref: "#/components/schemas/UpdateProfileInput" } } }
9818
+ },
9819
+ responses: {
9820
+ "200": { content: { "application/json": { schema: { type: "object", properties: { profile: { $ref: "#/components/schemas/Profile" } } } } } }
9821
+ }
9822
+ },
9095
9823
  delete: {
9096
9824
  operationId: "deleteProfile",
9097
9825
  summary: "Delete a profile",
@@ -9106,7 +9834,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9106
9834
  description: "Requires an API key with the `instructions:write` scope.",
9107
9835
  security: [{ apiKey: [] }],
9108
9836
  parameters: [
9109
- { name: "id", in: "path", required: true, schema: { type: "string" } }
9837
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
9838
+ idempotencyKeyParameter
9110
9839
  ],
9111
9840
  requestBody: {
9112
9841
  required: true,
@@ -9123,7 +9852,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9123
9852
  schema: { $ref: "#/components/schemas/ProfileConfigAddedResponse" }
9124
9853
  }
9125
9854
  }
9126
- }
9855
+ },
9856
+ "409": idempotencyConflictResponse
9127
9857
  }
9128
9858
  }
9129
9859
  },
@@ -9134,13 +9864,17 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9134
9864
  security: [{ apiKey: [] }],
9135
9865
  parameters: [
9136
9866
  { name: "id", in: "path", required: true, schema: { type: "string" } },
9137
- { name: "configId", in: "path", required: true, schema: { type: "string" } }
9867
+ { name: "configId", in: "path", required: true, schema: { type: "string" } },
9868
+ idempotencyKeyParameter
9138
9869
  ],
9139
9870
  requestBody: {
9140
9871
  required: true,
9141
9872
  content: { "application/json": { schema: { type: "object", required: ["binding"], properties: { binding: { $ref: "#/components/schemas/ProfileConfigBindingSpec" } } } } }
9142
9873
  },
9143
- responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { binding: { $ref: "#/components/schemas/ProfileConfigBinding" } } } } } } }
9874
+ responses: {
9875
+ "200": { content: { "application/json": { schema: { type: "object", properties: { binding: { $ref: "#/components/schemas/ProfileConfigBinding" } } } } } },
9876
+ "409": idempotencyConflictResponse
9877
+ }
9144
9878
  },
9145
9879
  delete: {
9146
9880
  operationId: "removeConfigFromProfile",
@@ -9166,24 +9900,38 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9166
9900
  get: {
9167
9901
  operationId: "getProfileConfigBindings",
9168
9902
  summary: "List schema-versioned config bindings for a profile",
9169
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9170
- responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { bindings: { type: "array", items: { $ref: "#/components/schemas/ProfileConfigBinding" } } } } } } } }
9903
+ parameters: [
9904
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
9905
+ limitParameter,
9906
+ cursorParameter
9907
+ ],
9908
+ responses: { "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/BoundedProfileConfigBindingPage" } } } } }
9171
9909
  }
9172
9910
  },
9173
9911
  "/v1/profiles/{id}/assets": {
9174
9912
  get: {
9175
9913
  operationId: "getProfileAssetBindings",
9176
9914
  summary: "List typed asset bindings for a profile",
9177
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9178
- responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { assets: { type: "array", items: { $ref: "#/components/schemas/ProfileAssetBinding" } } } } } } } }
9915
+ parameters: [
9916
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
9917
+ limitParameter,
9918
+ cursorParameter
9919
+ ],
9920
+ responses: { "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/BoundedProfileAssetBindingPage" } } } } }
9179
9921
  },
9180
9922
  post: {
9181
9923
  operationId: "addAssetToProfile",
9182
9924
  summary: "Add a content-addressed asset binding to a profile",
9183
9925
  security: [{ apiKey: [] }],
9184
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9926
+ parameters: [
9927
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
9928
+ idempotencyKeyParameter
9929
+ ],
9185
9930
  requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/AddProfileAssetInput" } } } },
9186
- responses: { "201": { content: { "application/json": { schema: { type: "object", properties: { asset: { $ref: "#/components/schemas/ProfileAssetBinding" } } } } } } }
9931
+ responses: {
9932
+ "201": { content: { "application/json": { schema: { type: "object", properties: { asset: { $ref: "#/components/schemas/ProfileAssetBinding" } } } } } },
9933
+ "409": idempotencyConflictResponse
9934
+ }
9187
9935
  }
9188
9936
  },
9189
9937
  "/v1/profiles/{id}/assets/{assetKey}": {
@@ -9193,10 +9941,14 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9193
9941
  security: [{ apiKey: [] }],
9194
9942
  parameters: [
9195
9943
  { name: "id", in: "path", required: true, schema: { type: "string" } },
9196
- { name: "assetKey", in: "path", required: true, schema: { type: "string" } }
9944
+ { name: "assetKey", in: "path", required: true, schema: { type: "string" } },
9945
+ idempotencyKeyParameter
9197
9946
  ],
9198
9947
  requestBody: { required: true, content: { "application/json": { schema: { type: "object", required: ["binding"], properties: { binding: { $ref: "#/components/schemas/ProfileAssetBindingSpec" } } } } } },
9199
- responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { asset: { $ref: "#/components/schemas/ProfileAssetBinding" } } } } } } }
9948
+ responses: {
9949
+ "200": { content: { "application/json": { schema: { type: "object", properties: { asset: { $ref: "#/components/schemas/ProfileAssetBinding" } } } } } },
9950
+ "409": idempotencyConflictResponse
9951
+ }
9200
9952
  },
9201
9953
  delete: {
9202
9954
  operationId: "removeAssetFromProfile",
@@ -9209,6 +9961,77 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
9209
9961
  responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { removed: { type: "boolean", const: true } } } } } } }
9210
9962
  }
9211
9963
  },
9964
+ "/v1/snapshots/{id}": {
9965
+ get: {
9966
+ operationId: "getSnapshot",
9967
+ summary: "Get a snapshot by id",
9968
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
9969
+ responses: {
9970
+ "200": { content: { "application/json": { schema: { type: "object", properties: { snapshot: { $ref: "#/components/schemas/ConfigSnapshot" } } } } } }
9971
+ }
9972
+ }
9973
+ },
9974
+ "/v1/machines/applied": {
9975
+ post: {
9976
+ operationId: "markMachineApplied",
9977
+ summary: "Mark a machine as having applied its resolved instructions",
9978
+ requestBody: {
9979
+ required: true,
9980
+ content: { "application/json": { schema: { $ref: "#/components/schemas/MachineAppliedInput" } } }
9981
+ },
9982
+ responses: {
9983
+ "200": { content: { "application/json": { schema: { type: "object", required: ["updated"], properties: { updated: { type: "boolean", const: true } } } } } }
9984
+ }
9985
+ }
9986
+ },
9987
+ "/v1/machines": {
9988
+ get: {
9989
+ operationId: "listMachines",
9990
+ summary: "List registered machines with producer-side bounds",
9991
+ parameters: [limitParameter, cursorParameter, identityViewParameter],
9992
+ responses: {
9993
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/BoundedMachinePage" } } } }
9994
+ }
9995
+ },
9996
+ post: {
9997
+ operationId: "registerMachine",
9998
+ summary: "Register or refresh a machine",
9999
+ parameters: [idempotencyKeyParameter],
10000
+ requestBody: {
10001
+ required: true,
10002
+ content: {
10003
+ "application/json": {
10004
+ schema: {
10005
+ type: "object",
10006
+ required: ["hostname"],
10007
+ properties: {
10008
+ hostname: { type: "string" },
10009
+ os: { type: "string", nullable: true },
10010
+ arch: { type: "string", nullable: true }
10011
+ }
10012
+ }
10013
+ }
10014
+ }
10015
+ },
10016
+ responses: {
10017
+ "201": { content: { "application/json": { schema: { type: "object", properties: { machine: { $ref: "#/components/schemas/Machine" } } } } } },
10018
+ "409": idempotencyConflictResponse
10019
+ }
10020
+ }
10021
+ },
10022
+ "/v1/feedback": {
10023
+ post: {
10024
+ operationId: "createFeedback",
10025
+ summary: "Submit Instructions feedback",
10026
+ requestBody: {
10027
+ required: true,
10028
+ content: { "application/json": { schema: { $ref: "#/components/schemas/FeedbackInput" } } }
10029
+ },
10030
+ responses: {
10031
+ "201": { content: { "application/json": { schema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean", const: true } } } } } }
10032
+ }
10033
+ }
10034
+ },
9212
10035
  "/v1/stats": {
9213
10036
  get: {
9214
10037
  operationId: "getStats",
@@ -9241,7 +10064,7 @@ if (process.argv.includes("--version") || process.argv.includes("-V")) {
9241
10064
  if (process.argv.includes("--help") || process.argv.includes("-h")) {
9242
10065
  console.log(`Usage: instructions-serve [options]
9243
10066
 
9244
- HTTP API server for @hasna/instructions (backend: sqlite or postgresql).
10067
+ HTTP API server for @hasna/instructions (PostgreSQL is required for /v1).
9245
10068
 
9246
10069
  Options:
9247
10070
  -V, --version output the version number
@@ -9252,26 +10075,28 @@ Environment:
9252
10075
  PORT / INSTRUCTIONS_PORT HTTP port (default: 3457)
9253
10076
  HOST / INSTRUCTIONS_HOST bind host (default: localhost)
9254
10077
  HASNA_INSTRUCTIONS_DATABASE_URL / INSTRUCTIONS_DATABASE_URL / DATABASE_URL
9255
- postgresql backend URL (enables the cloud backend)`);
10078
+ PostgreSQL backend URL (required for /v1)`);
9256
10079
  process.exit(0);
9257
10080
  }
9258
10081
  var PORT = Number(process.env["PORT"] ?? process.env["INSTRUCTIONS_PORT"] ?? 3457);
9259
10082
  var app = new Hono2;
9260
10083
  app.use("*", cors());
9261
10084
  function serviceBackend() {
9262
- return isPostgresBackendEnabled() ? "postgresql" : "sqlite";
10085
+ return isPostgresBackendEnabled() ? "postgresql" : "unconfigured";
9263
10086
  }
9264
10087
  app.get("/health", (c) => c.json({ status: "ok", version: getPackageVersion(), backend: serviceBackend(), name: "instructions" }));
9265
10088
  app.get("/version", (c) => c.json({ status: "ok", version: getPackageVersion(), backend: serviceBackend(), name: "instructions" }));
9266
10089
  app.get("/ready", async (c) => {
9267
10090
  const version = getPackageVersion();
9268
10091
  const backend2 = serviceBackend();
9269
- if (backend2 === "postgresql") {
9270
- try {
9271
- await pingCloud();
9272
- } catch (e) {
9273
- return c.json({ status: "unavailable", version, backend: backend2, error: e.message }, 503);
9274
- }
10092
+ if (backend2 !== "postgresql") {
10093
+ return c.json({ status: "unavailable", version, backend: backend2, code: "SERVER_BACKEND_UNCONFIGURED" }, 503);
10094
+ }
10095
+ try {
10096
+ await pingCloud();
10097
+ } catch {
10098
+ console.error("instructions ready: database unavailable");
10099
+ return c.json({ status: "unavailable", version, backend: backend2, code: "DATABASE_UNAVAILABLE" }, 503);
9275
10100
  }
9276
10101
  return c.json({ status: "ready", version, backend: backend2 });
9277
10102
  });
@@ -9282,18 +10107,27 @@ app.use("/v1/*", async (c, next) => {
9282
10107
  let mw;
9283
10108
  try {
9284
10109
  mw = getHonoAuthMiddleware([isWrite ? "instructions:write" : "instructions:read"]);
9285
- } catch (e) {
9286
- return c.json({ error: e.message }, 503);
10110
+ } catch {
10111
+ return c.json({ error: "Instructions API is unavailable", code: "API_AUTH_UNCONFIGURED" }, 503);
9287
10112
  }
9288
10113
  return mw(c, next);
9289
10114
  });
9290
10115
  app.all("/v1/*", async (c) => {
9291
- const res = await handleV1Request(c.req.raw, new URL(c.req.url));
10116
+ const res = await handleV1Request(c.req.raw, new URL(c.req.url), {
10117
+ principal: c.get("apiKey")
10118
+ });
9292
10119
  return res ?? c.json({ error: "Not found" }, 404);
9293
10120
  });
9294
10121
  var HOST = process.env["HOST"] ?? process.env["INSTRUCTIONS_HOST"] ?? "localhost";
9295
- console.log(`instructions-serve listening on http://${HOST}:${PORT} (backend: ${serviceBackend()})`);
9296
- var server_default = { port: PORT, hostname: HOST, fetch: app.fetch };
10122
+ var serverOptions = { port: PORT, hostname: HOST, fetch: app.fetch };
10123
+ var server;
10124
+ if (import.meta.main) {
10125
+ console.log(`instructions-serve listening on http://${HOST}:${PORT} (backend: ${serviceBackend()})`);
10126
+ server = Bun.serve(serverOptions);
10127
+ }
9297
10128
  export {
9298
- server_default as default
10129
+ serviceBackend,
10130
+ serverOptions,
10131
+ server,
10132
+ app
9299
10133
  };