@hasna/mementos 0.14.80 → 0.14.82

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.
@@ -52908,13 +52908,36 @@ function buildOpenApiDocument(version2) {
52908
52908
  required: true,
52909
52909
  schema: { type: "string" }
52910
52910
  }));
52911
+ if (route.method === "GET" && route.path === "/api/projects/:id/resources") {
52912
+ params.push({
52913
+ name: "limit",
52914
+ in: "query",
52915
+ required: false,
52916
+ schema: { type: "integer", minimum: 1, maximum: 1000, default: 100 }
52917
+ }, {
52918
+ name: "cursor",
52919
+ in: "query",
52920
+ required: false,
52921
+ schema: { type: "string" }
52922
+ }, {
52923
+ name: "resource_kinds",
52924
+ in: "query",
52925
+ required: false,
52926
+ description: "Comma-separated subset of project, knowledge, memory, session",
52927
+ schema: { type: "string" }
52928
+ });
52929
+ }
52930
+ const successSchema = route.method === "GET" && route.path === "/api/projects/:id/resources" ? { $ref: "#/components/schemas/MementosProjectResourcePage" } : route.method === "GET" && route.path === "/api/projects/:id/resources/:kind/:resource_id" ? { $ref: "#/components/schemas/MementosProjectResourceExactResult" } : undefined;
52911
52931
  paths[p] = paths[p] ?? {};
52912
52932
  paths[p][method] = {
52913
52933
  summary: `${route.method} ${p}`,
52914
52934
  operationId: `${method}_${p.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_|_$/g, "")}`,
52915
52935
  ...params.length ? { parameters: params } : {},
52916
52936
  responses: {
52917
- "200": { description: "OK" },
52937
+ "200": {
52938
+ description: "OK",
52939
+ ...successSchema ? { content: { "application/json": { schema: successSchema } } } : {}
52940
+ },
52918
52941
  "401": { description: "Unauthorized" },
52919
52942
  "403": { description: "Forbidden" },
52920
52943
  "404": { description: "Not found" }
@@ -52933,6 +52956,124 @@ function buildOpenApiDocument(version2) {
52933
52956
  securitySchemes: {
52934
52957
  bearerAuth: { type: "http", scheme: "bearer" },
52935
52958
  apiKeyAuth: { type: "apiKey", in: "header", name: "x-api-key" }
52959
+ },
52960
+ schemas: {
52961
+ MementosProjectResource: {
52962
+ type: "object",
52963
+ additionalProperties: false,
52964
+ required: [
52965
+ "authority",
52966
+ "source_package",
52967
+ "project_id",
52968
+ "resource_kind",
52969
+ "stable_id",
52970
+ "revision",
52971
+ "digest",
52972
+ "membership"
52973
+ ],
52974
+ properties: {
52975
+ authority: { const: "mementos" },
52976
+ source_package: { const: "@hasna/mementos" },
52977
+ project_id: { type: "string" },
52978
+ resource_kind: {
52979
+ type: "string",
52980
+ enum: ["project", "knowledge", "memory", "session"]
52981
+ },
52982
+ stable_id: { type: "string" },
52983
+ revision: { type: "string" },
52984
+ digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
52985
+ membership: {
52986
+ type: "string",
52987
+ enum: ["project_aggregate", "explicit_project_id_or_focus"]
52988
+ }
52989
+ }
52990
+ },
52991
+ MementosProjectResourceAuthority: {
52992
+ type: "object",
52993
+ additionalProperties: false,
52994
+ required: [
52995
+ "authority",
52996
+ "authority_id",
52997
+ "tenant_id",
52998
+ "corpus_id",
52999
+ "package_version"
53000
+ ],
53001
+ properties: {
53002
+ authority: { const: "mementos" },
53003
+ authority_id: { type: "string" },
53004
+ tenant_id: { type: "string" },
53005
+ corpus_id: { type: "string" },
53006
+ package_version: { type: "string" }
53007
+ }
53008
+ },
53009
+ MementosProjectResourcePage: {
53010
+ type: "object",
53011
+ additionalProperties: false,
53012
+ required: [
53013
+ "schema",
53014
+ "authority",
53015
+ "project_id",
53016
+ "project_revision",
53017
+ "collection_revision",
53018
+ "resource_kinds",
53019
+ "resources",
53020
+ "count",
53021
+ "total",
53022
+ "limit",
53023
+ "cursor",
53024
+ "next_cursor",
53025
+ "has_more",
53026
+ "complete",
53027
+ "truncated"
53028
+ ],
53029
+ properties: {
53030
+ schema: { const: "mementos.project-resources.v1" },
53031
+ authority: { $ref: "#/components/schemas/MementosProjectResourceAuthority" },
53032
+ project_id: { type: "string" },
53033
+ project_revision: { type: "string" },
53034
+ collection_revision: { type: "string", pattern: "^[0-9a-f]{64}$" },
53035
+ resource_kinds: {
53036
+ type: "array",
53037
+ items: { type: "string", enum: ["project", "knowledge", "memory", "session"] }
53038
+ },
53039
+ resources: {
53040
+ type: "array",
53041
+ items: { $ref: "#/components/schemas/MementosProjectResource" }
53042
+ },
53043
+ count: { type: "integer", minimum: 0 },
53044
+ total: { type: "integer", minimum: 0 },
53045
+ limit: { type: "integer", minimum: 1, maximum: 1000 },
53046
+ cursor: { type: ["string", "null"] },
53047
+ next_cursor: { type: ["string", "null"] },
53048
+ has_more: { type: "boolean" },
53049
+ complete: { const: true },
53050
+ truncated: { const: false }
53051
+ }
53052
+ },
53053
+ MementosProjectResourceExactResult: {
53054
+ type: "object",
53055
+ additionalProperties: false,
53056
+ required: [
53057
+ "schema",
53058
+ "authority",
53059
+ "project_id",
53060
+ "project_revision",
53061
+ "collection_revision",
53062
+ "resource",
53063
+ "complete",
53064
+ "truncated"
53065
+ ],
53066
+ properties: {
53067
+ schema: { const: "mementos.project-resource.v1" },
53068
+ authority: { $ref: "#/components/schemas/MementosProjectResourceAuthority" },
53069
+ project_id: { type: "string" },
53070
+ project_revision: { type: "string" },
53071
+ collection_revision: { type: "string", pattern: "^[0-9a-f]{64}$" },
53072
+ resource: { $ref: "#/components/schemas/MementosProjectResource" },
53073
+ complete: { const: true },
53074
+ truncated: { const: false }
53075
+ }
53076
+ }
52936
53077
  }
52937
53078
  },
52938
53079
  security: [{ bearerAuth: [] }, { apiKeyAuth: [] }],
@@ -56419,6 +56560,97 @@ function getMementosPackageVersion() {
56419
56560
  // src/db/memory-project-link.ts
56420
56561
  init_schema();
56421
56562
 
56563
+ // src/project-registration/types.ts
56564
+ var MEMENTOS_PROJECT_REGISTRATION_ROUTE = "mementos.project-registration.v1";
56565
+ var MEMENTOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1";
56566
+ var MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE = "mementos.project-guarded-update.v1";
56567
+ var MEMENTOS_PROJECT_RESOURCE_ROUTE = "mementos.project-resources.v1";
56568
+ var MEMENTOS_PROJECT_RESOURCE_KINDS = [
56569
+ "project",
56570
+ "knowledge",
56571
+ "memory",
56572
+ "session"
56573
+ ];
56574
+
56575
+ class MementosProjectRegistrationError extends Error {
56576
+ code;
56577
+ details;
56578
+ constructor(code, message, details = {}) {
56579
+ super(message);
56580
+ this.code = code;
56581
+ this.details = details;
56582
+ this.name = "MementosProjectRegistrationError";
56583
+ }
56584
+ }
56585
+
56586
+ // src/project-registration/identity.ts
56587
+ var MEMENTOS_PROJECT_AUTHORITY_ENV = {
56588
+ authorityId: "MEMENTOS_PROJECT_AUTHORITY_ID",
56589
+ tenantId: "MEMENTOS_PROJECT_TENANT_ID",
56590
+ corpusId: "MEMENTOS_PROJECT_CORPUS_ID"
56591
+ };
56592
+ function configuredValue(override, envKey) {
56593
+ return override?.trim() || process.env[envKey]?.trim() || null;
56594
+ }
56595
+
56596
+ class MementosProjectAuthorityIdentityError extends Error {
56597
+ missing_env;
56598
+ code = "MEMENTOS_PROJECT_AUTHORITY_UNCONFIGURED";
56599
+ constructor(missing_env) {
56600
+ super("Mementos project authority identity is not configured; set " + missing_env.join(", "));
56601
+ this.missing_env = missing_env;
56602
+ this.name = "MementosProjectAuthorityIdentityError";
56603
+ }
56604
+ }
56605
+ function resolveMementosProjectAuthorityIdentity(options = {}) {
56606
+ const authorityId = configuredValue(options.authorityId, MEMENTOS_PROJECT_AUTHORITY_ENV.authorityId);
56607
+ const tenantId = configuredValue(options.tenantId, MEMENTOS_PROJECT_AUTHORITY_ENV.tenantId);
56608
+ const corpusId = configuredValue(options.corpusId, MEMENTOS_PROJECT_AUTHORITY_ENV.corpusId);
56609
+ if (!authorityId || !tenantId || !corpusId) {
56610
+ const missingEnv = [];
56611
+ if (!authorityId)
56612
+ missingEnv.push(MEMENTOS_PROJECT_AUTHORITY_ENV.authorityId);
56613
+ if (!tenantId)
56614
+ missingEnv.push(MEMENTOS_PROJECT_AUTHORITY_ENV.tenantId);
56615
+ if (!corpusId)
56616
+ missingEnv.push(MEMENTOS_PROJECT_AUTHORITY_ENV.corpusId);
56617
+ throw new MementosProjectAuthorityIdentityError(missingEnv);
56618
+ }
56619
+ return {
56620
+ authority_id: authorityId,
56621
+ tenant_id: tenantId,
56622
+ corpus_id: corpusId
56623
+ };
56624
+ }
56625
+ function buildMementosProjectRegistrationCapability(options = {}) {
56626
+ const identity = resolveMementosProjectAuthorityIdentity(options);
56627
+ return {
56628
+ authority: "mementos",
56629
+ route: MEMENTOS_PROJECT_REGISTRATION_ROUTE,
56630
+ package_version: options.packageVersion ?? getMementosPackageVersion(),
56631
+ ...identity,
56632
+ supported_resources: ["project"],
56633
+ conditional_create: true,
56634
+ immutable_receipts: true,
56635
+ exact_terminal_lookup: true,
56636
+ exact_readback: true,
56637
+ conditional_inverse: true,
56638
+ ambiguous_outcome_reconciliation: true,
56639
+ guarded_update: true,
56640
+ guarded_update_route: MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE,
56641
+ no_write_dry_run: true,
56642
+ expected_revision_compare_and_swap: true,
56643
+ caller_idempotency: true,
56644
+ exact_inverse_rollback: true,
56645
+ project_resource_enumeration: true,
56646
+ project_resource_route: MEMENTOS_PROJECT_RESOURCE_ROUTE,
56647
+ project_resource_kinds: ["project", "knowledge", "memory", "session"],
56648
+ stable_keyset_pagination: true,
56649
+ explicit_membership_only: true
56650
+ };
56651
+ }
56652
+
56653
+ // src/db/memory-project-link.ts
56422
56654
  class MemoryProjectLinkError extends Error {
56423
56655
  code;
56424
56656
  details;
@@ -56429,12 +56661,17 @@ class MemoryProjectLinkError extends Error {
56429
56661
  this.name = "MemoryProjectLinkError";
56430
56662
  }
56431
56663
  }
56432
- var LINK_AUTHORITY = {
56433
- authority_id: "mementos",
56434
- tenant_id: "default",
56435
- corpus_id: "default"
56436
- };
56437
56664
  var BOUNDED_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
56665
+ function linkAuthority() {
56666
+ try {
56667
+ return resolveMementosProjectAuthorityIdentity();
56668
+ } catch (error) {
56669
+ if (error instanceof MementosProjectAuthorityIdentityError) {
56670
+ throw new MemoryProjectLinkError("MEMORY_PROJECT_LINK_AUTHORITY_MISMATCH", error.message, { authority_code: error.code, missing_env: error.missing_env });
56671
+ }
56672
+ throw error;
56673
+ }
56674
+ }
56438
56675
  function canonicalize(value) {
56439
56676
  if (Array.isArray(value))
56440
56677
  return value.map(canonicalize);
@@ -56542,7 +56779,8 @@ function receiptFromRow(row) {
56542
56779
  };
56543
56780
  }
56544
56781
  function assertIdentity(identity) {
56545
- if (identity.authority_id !== LINK_AUTHORITY.authority_id || identity.tenant_id !== LINK_AUTHORITY.tenant_id || identity.corpus_id !== LINK_AUTHORITY.corpus_id) {
56782
+ const expectedIdentity = linkAuthority();
56783
+ if (identity.authority_id !== expectedIdentity.authority_id || identity.tenant_id !== expectedIdentity.tenant_id || identity.corpus_id !== expectedIdentity.corpus_id) {
56546
56784
  throw new MemoryProjectLinkError("MEMORY_PROJECT_LINK_AUTHORITY_MISMATCH", "memory project link does not match this authority, tenant, and corpus");
56547
56785
  }
56548
56786
  }
@@ -57019,7 +57257,7 @@ function rollbackMemoryProjectLink(memoryId, request, db) {
57019
57257
  };
57020
57258
  });
57021
57259
  }
57022
- function getMemoryProjectLinkReceipt(memoryId, receiptId, identity = LINK_AUTHORITY, db) {
57260
+ function getMemoryProjectLinkReceipt(memoryId, receiptId, identity = linkAuthority(), db) {
57023
57261
  assertIdentity(identity);
57024
57262
  assertBoundedIdentifier(memoryId, "memory_id");
57025
57263
  assertBoundedIdentifier(receiptId, "receipt_id");
@@ -57507,24 +57745,6 @@ addRoute("POST", "/api/locks/clean", () => {
57507
57745
  init_database();
57508
57746
  init_api_mode();
57509
57747
  import { createHash as createHash2 } from "crypto";
57510
-
57511
- // src/project-registration/types.ts
57512
- var MEMENTOS_PROJECT_REGISTRATION_ROUTE = "mementos.project-registration.v1";
57513
- var MEMENTOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1";
57514
- var MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE = "mementos.project-guarded-update.v1";
57515
-
57516
- class MementosProjectRegistrationError extends Error {
57517
- code;
57518
- details;
57519
- constructor(code, message, details = {}) {
57520
- super(message);
57521
- this.code = code;
57522
- this.details = details;
57523
- this.name = "MementosProjectRegistrationError";
57524
- }
57525
- }
57526
-
57527
- // src/db/projects.ts
57528
57748
  function parseProjectRow2(row) {
57529
57749
  return {
57530
57750
  id: row["id"],
@@ -57546,12 +57766,17 @@ class ProjectGuardedUpdateError extends Error {
57546
57766
  this.name = "ProjectGuardedUpdateError";
57547
57767
  }
57548
57768
  }
57549
- var PROJECT_UPDATE_AUTHORITY = {
57550
- authority_id: "mementos",
57551
- tenant_id: "default",
57552
- corpus_id: "default"
57553
- };
57554
57769
  var BOUNDED_IDENTIFIER2 = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
57770
+ function projectUpdateAuthority() {
57771
+ try {
57772
+ return resolveMementosProjectAuthorityIdentity();
57773
+ } catch (error) {
57774
+ if (error instanceof MementosProjectAuthorityIdentityError) {
57775
+ throw new ProjectGuardedUpdateError("PROJECT_UPDATE_AUTHORITY_MISMATCH", error.message, { authority_code: error.code, missing_env: error.missing_env });
57776
+ }
57777
+ throw error;
57778
+ }
57779
+ }
57555
57780
  function canonicalizeProjectUpdateValue(value) {
57556
57781
  if (Array.isArray(value))
57557
57782
  return value.map(canonicalizeProjectUpdateValue);
@@ -57624,8 +57849,8 @@ function normalizeProjectUpdateInput(input) {
57624
57849
  }
57625
57850
  return normalized;
57626
57851
  }
57627
- function assertProjectUpdateIdentity(identity) {
57628
- if (identity.authority_id !== PROJECT_UPDATE_AUTHORITY.authority_id || identity.tenant_id !== PROJECT_UPDATE_AUTHORITY.tenant_id || identity.corpus_id !== PROJECT_UPDATE_AUTHORITY.corpus_id) {
57852
+ function assertProjectUpdateIdentity(identity, expectedIdentity = projectUpdateAuthority()) {
57853
+ if (identity.authority_id !== expectedIdentity.authority_id || identity.tenant_id !== expectedIdentity.tenant_id || identity.corpus_id !== expectedIdentity.corpus_id) {
57629
57854
  throw new ProjectGuardedUpdateError("PROJECT_UPDATE_AUTHORITY_MISMATCH", "guarded project update does not match this authority, tenant, and corpus");
57630
57855
  }
57631
57856
  }
@@ -57634,8 +57859,8 @@ function assertBoundedIdentifier2(value, field) {
57634
57859
  throw new ProjectGuardedUpdateError("PROJECT_UPDATE_INVALID_INPUT", `${field} must be an 8-128 character bounded identifier`);
57635
57860
  }
57636
57861
  }
57637
- function assertProjectUpdateRequest(request) {
57638
- assertProjectUpdateIdentity(request);
57862
+ function assertProjectUpdateRequest(request, expectedIdentity = projectUpdateAuthority()) {
57863
+ assertProjectUpdateIdentity(request, expectedIdentity);
57639
57864
  assertBoundedIdentifier2(request.operation_id, "operation_id");
57640
57865
  assertBoundedIdentifier2(request.step_id, "step_id");
57641
57866
  assertBoundedIdentifier2(request.idempotency_key, "idempotency_key");
@@ -57748,7 +57973,7 @@ function makeProjectUpdateReceipt(input) {
57748
57973
  target_id: input.target_id,
57749
57974
  expected_revision: input.request.expected_revision,
57750
57975
  result_revision: input.after_project.updated_at,
57751
- result_digest: digestProjectUpdateValue(input.after_project),
57976
+ result_digest: input.result_digest ?? digestProjectUpdateValue(input.after_project),
57752
57977
  accepted_receipt_id: input.accepted_receipt_id ?? null,
57753
57978
  before_project: input.before_project,
57754
57979
  after_project: input.after_project,
@@ -57812,8 +58037,8 @@ function listProjects(db) {
57812
58037
  const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
57813
58038
  return rows.map(parseProjectRow2);
57814
58039
  }
57815
- function previewProjectUpdate(id, request, db) {
57816
- assertProjectUpdateRequest(request);
58040
+ function previewProjectUpdate(id, request, db, expectedIdentity = projectUpdateAuthority()) {
58041
+ assertProjectUpdateRequest(request, expectedIdentity);
57817
58042
  const normalized = normalizeProjectUpdateInput(request.updates);
57818
58043
  if (!db && isApiMode()) {
57819
58044
  const { data } = apiJson("POST", `/projects/${encodeURIComponent(id)}/guarded-update`, { ...request, updates: normalized, dry_run: true });
@@ -57835,8 +58060,8 @@ function previewProjectUpdate(id, request, db) {
57835
58060
  receipt: null
57836
58061
  };
57837
58062
  }
57838
- function applyProjectUpdate(id, request, db) {
57839
- assertProjectUpdateRequest(request);
58063
+ function applyProjectUpdate(id, request, db, expectedIdentity = projectUpdateAuthority(), resultDigestForProject) {
58064
+ assertProjectUpdateRequest(request, expectedIdentity);
57840
58065
  const normalized = normalizeProjectUpdateInput(request.updates);
57841
58066
  if (!db && isApiMode()) {
57842
58067
  const { data } = apiJson("POST", `/projects/${encodeURIComponent(id)}/guarded-update`, { ...request, updates: normalized, dry_run: false });
@@ -57897,14 +58122,15 @@ function applyProjectUpdate(id, request, db) {
57897
58122
  request_digest: requestDigest,
57898
58123
  target_id: id,
57899
58124
  before_project: before,
57900
- after_project: readback
58125
+ after_project: readback,
58126
+ result_digest: resultDigestForProject?.(readback)
57901
58127
  });
57902
58128
  insertProjectUpdateReceipt(d, receipt);
57903
58129
  return { dry_run: false, applied: true, project: readback, receipt };
57904
58130
  });
57905
58131
  }
57906
- function rollbackProjectUpdate(id, request, db) {
57907
- assertProjectUpdateRequest(request);
58132
+ function rollbackProjectUpdate(id, request, db, expectedIdentity = projectUpdateAuthority(), resultDigestForProject) {
58133
+ assertProjectUpdateRequest(request, expectedIdentity);
57908
58134
  assertBoundedIdentifier2(request.accepted_receipt_id, "accepted_receipt_id");
57909
58135
  if (!db && isApiMode()) {
57910
58136
  const { data } = apiJson("POST", `/projects/${encodeURIComponent(id)}/guarded-rollback`, request);
@@ -57970,14 +58196,15 @@ function rollbackProjectUpdate(id, request, db) {
57970
58196
  target_id: id,
57971
58197
  before_project: current,
57972
58198
  after_project: readback,
58199
+ result_digest: resultDigestForProject?.(readback),
57973
58200
  accepted_receipt_id: accepted.receipt_id
57974
58201
  });
57975
58202
  insertProjectUpdateReceipt(d, receipt);
57976
58203
  return { dry_run: false, applied: true, project: readback, receipt };
57977
58204
  });
57978
58205
  }
57979
- function getProjectUpdateReceipt(id, receiptId, identity = PROJECT_UPDATE_AUTHORITY, db) {
57980
- assertProjectUpdateIdentity(identity);
58206
+ function getProjectUpdateReceipt(id, receiptId, identity = projectUpdateAuthority(), db, expectedIdentity = projectUpdateAuthority()) {
58207
+ assertProjectUpdateIdentity(identity, expectedIdentity);
57981
58208
  if (!db && isApiMode()) {
57982
58209
  const { data } = apiJson("POST", `/projects/${encodeURIComponent(id)}/update-receipts/lookup`, { ...identity, receipt_id: receiptId });
57983
58210
  return data;
@@ -57990,91 +58217,6 @@ function getProjectUpdateReceipt(id, receiptId, identity = PROJECT_UPDATE_AUTHOR
57990
58217
  return receipt;
57991
58218
  }
57992
58219
 
57993
- // src/server/routes/projects.ts
57994
- init_router();
57995
- addRoute("GET", "/api/projects", (_req, url) => {
57996
- const q = getSearchParams(url);
57997
- const projects = listProjects();
57998
- if (q["fields"]) {
57999
- const fields = q["fields"].split(",").map((f) => f.trim());
58000
- const filtered = projects.map((p) => Object.fromEntries(fields.map((f) => [f, p[f]]).filter(([, v]) => v !== undefined)));
58001
- return json({ projects: filtered, count: filtered.length });
58002
- }
58003
- return json({ projects, count: projects.length });
58004
- });
58005
- addRoute("POST", "/api/projects", async (req) => {
58006
- const body = await readJson(req);
58007
- if (!body || !body["name"] || !body["path"]) {
58008
- return errorResponse("Missing required fields: name, path", 400);
58009
- }
58010
- const project = registerProject(body["name"], body["path"], body["description"], body["memory_prefix"]);
58011
- return json(project, 201);
58012
- });
58013
- addRoute("GET", "/api/projects/:id", (_req, _url, params) => {
58014
- const project = getProject(params["id"]);
58015
- if (!project)
58016
- return errorResponse("Project not found", 404);
58017
- return json(project);
58018
- });
58019
- function guardedUpdateError(error) {
58020
- const status = error.code === "PROJECT_UPDATE_AUTHORITY_MISMATCH" ? 403 : error.code === "PROJECT_UPDATE_NOT_FOUND" || error.code === "PROJECT_UPDATE_RECEIPT_NOT_FOUND" ? 404 : error.code === "PROJECT_UPDATE_INVALID_INPUT" ? 400 : 409;
58021
- return errorResponse(error.message, status, { code: error.code, ...error.details });
58022
- }
58023
- addRoute("PATCH", "/api/projects/:id", () => errorResponse("Unguarded project updates are disabled; use POST /projects/:id/guarded-update", 428));
58024
- addRoute("POST", "/api/projects/:id/guarded-update", async (req, _url, params) => {
58025
- const body = await readJson(req);
58026
- if (!body)
58027
- return errorResponse("Invalid JSON body", 400);
58028
- try {
58029
- const request = body;
58030
- return json(request.dry_run ? previewProjectUpdate(params["id"], request) : applyProjectUpdate(params["id"], request));
58031
- } catch (error) {
58032
- if (error instanceof ProjectGuardedUpdateError)
58033
- return guardedUpdateError(error);
58034
- throw error;
58035
- }
58036
- });
58037
- addRoute("POST", "/api/projects/:id/guarded-rollback", async (req, _url, params) => {
58038
- const body = await readJson(req);
58039
- if (!body)
58040
- return errorResponse("Invalid JSON body", 400);
58041
- try {
58042
- return json(rollbackProjectUpdate(params["id"], body));
58043
- } catch (error) {
58044
- if (error instanceof ProjectGuardedUpdateError)
58045
- return guardedUpdateError(error);
58046
- throw error;
58047
- }
58048
- });
58049
- addRoute("POST", "/api/projects/:id/update-receipts/lookup", async (req, _url, params) => {
58050
- const body = await readJson(req);
58051
- if (!body || typeof body["receipt_id"] !== "string") {
58052
- return errorResponse("receipt_id is required", 400);
58053
- }
58054
- try {
58055
- const identity = {
58056
- authority_id: String(body["authority_id"] ?? ""),
58057
- tenant_id: String(body["tenant_id"] ?? ""),
58058
- corpus_id: String(body["corpus_id"] ?? "")
58059
- };
58060
- return json(getProjectUpdateReceipt(params["id"], body["receipt_id"], identity));
58061
- } catch (error) {
58062
- if (error instanceof ProjectGuardedUpdateError)
58063
- return guardedUpdateError(error);
58064
- throw error;
58065
- }
58066
- });
58067
- addRoute("GET", "/api/projects/:id/agents", (_req, _url, params) => {
58068
- const project = getProject(params["id"]);
58069
- if (!project)
58070
- return errorResponse("Project not found", 404);
58071
- const agents = listAgentsByProject(project.id);
58072
- return json({ agents, count: agents.length });
58073
- });
58074
-
58075
- // src/server/routes/project-registration.ts
58076
- init_database();
58077
-
58078
58220
  // src/project-registration/authority.ts
58079
58221
  import { createHash as createHash3 } from "crypto";
58080
58222
  import { resolve as resolve3 } from "path";
@@ -58205,9 +58347,9 @@ function assertWithinBounds(value, bounds, startedAt) {
58205
58347
  }
58206
58348
  return { response_bytes: bytes, elapsed_ms: elapsed };
58207
58349
  }
58208
- function withResponseControl(receipt, bounds, startedAt) {
58350
+ function withBoundedResponseControl(payload, bounds, startedAt) {
58209
58351
  const result = {
58210
- receipt,
58352
+ ...payload,
58211
58353
  response_control: {
58212
58354
  response_byte_limit: bounds.response_byte_limit,
58213
58355
  time_budget_ms: bounds.time_budget_ms,
@@ -58230,6 +58372,9 @@ function withResponseControl(receipt, bounds, startedAt) {
58230
58372
  result.response_control.elapsed_ms = measured.elapsed_ms;
58231
58373
  return result;
58232
58374
  }
58375
+ function withResponseControl(receipt, bounds, startedAt) {
58376
+ return withBoundedResponseControl({ receipt }, bounds, startedAt);
58377
+ }
58233
58378
  function requireString(value, field, options = {}) {
58234
58379
  const min = options.min ?? 1;
58235
58380
  const max = options.max ?? 512;
@@ -58256,6 +58401,80 @@ function ownedPath(target) {
58256
58401
  }
58257
58402
  return path;
58258
58403
  }
58404
+ function guardedAuthorityIdentity(capability) {
58405
+ return {
58406
+ authority_id: capability.authority_id,
58407
+ tenant_id: capability.tenant_id,
58408
+ corpus_id: capability.corpus_id
58409
+ };
58410
+ }
58411
+ function assertGuardedAuthorityRequest(targetId, request, capability) {
58412
+ assertBounds(request);
58413
+ requireString(targetId, "target_id", { min: 8, max: 128, pattern: OPERATION_PATTERN });
58414
+ if (request.authority !== "mementos" || request.authority_route !== MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE || request.package_version !== capability.package_version || request.authority_id !== capability.authority_id || request.tenant_id !== capability.tenant_id || request.corpus_id !== capability.corpus_id) {
58415
+ throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "guarded project request does not match this authority capability identity");
58416
+ }
58417
+ return guardedAuthorityIdentity(capability);
58418
+ }
58419
+ function assertGuardedOperationFields(request) {
58420
+ requireString(request.operation_id, "operation_id", {
58421
+ min: 8,
58422
+ max: 128,
58423
+ pattern: OPERATION_PATTERN
58424
+ });
58425
+ requireString(request.step_id, "step_id", {
58426
+ min: 8,
58427
+ max: 128,
58428
+ pattern: OPERATION_PATTERN
58429
+ });
58430
+ requireString(request.idempotency_key, "idempotency_key", {
58431
+ min: 8,
58432
+ max: 128,
58433
+ pattern: OPERATION_PATTERN
58434
+ });
58435
+ requireString(request.expected_revision, "expected_revision", { max: 128 });
58436
+ }
58437
+ function assertGuardedUpdateRequest(targetId, request, capability) {
58438
+ const identity = assertGuardedAuthorityRequest(targetId, request, capability);
58439
+ assertGuardedOperationFields(request);
58440
+ if (!request.updates || typeof request.updates !== "object") {
58441
+ throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "guarded project updates must contain exactly one private path handle");
58442
+ }
58443
+ exactKeys(request.updates, ["path"], "updates");
58444
+ return { identity, path: ownedPath(request.updates.path) };
58445
+ }
58446
+ function publicGuardedUpdateReceipt(receipt, capability) {
58447
+ return {
58448
+ receipt_id: receipt.receipt_id,
58449
+ authority: "mementos",
58450
+ route: MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE,
58451
+ package_version: capability.package_version,
58452
+ authority_id: capability.authority_id,
58453
+ tenant_id: capability.tenant_id,
58454
+ corpus_id: capability.corpus_id,
58455
+ operation_id: receipt.operation_id,
58456
+ step_id: receipt.step_id,
58457
+ direction: receipt.direction,
58458
+ idempotency_key: receipt.idempotency_key,
58459
+ request_digest: receipt.request_digest,
58460
+ outcome: "accepted",
58461
+ target_id: receipt.target_id,
58462
+ expected_revision: receipt.expected_revision,
58463
+ result_revision: receipt.result_revision,
58464
+ result_digest: receipt.result_digest,
58465
+ accepted_receipt_id: receipt.accepted_receipt_id,
58466
+ created_at: receipt.created_at
58467
+ };
58468
+ }
58469
+ function guardedProjectError(cause) {
58470
+ if (cause instanceof MementosProjectRegistrationError)
58471
+ throw cause;
58472
+ if (cause instanceof ProjectGuardedUpdateError) {
58473
+ const code = cause.code === "PROJECT_UPDATE_INVALID_INPUT" ? "MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT" : cause.code === "PROJECT_UPDATE_AUTHORITY_MISMATCH" ? "MEMENTOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH" : cause.code === "PROJECT_UPDATE_NOT_FOUND" ? "MEMENTOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND" : cause.code === "PROJECT_UPDATE_RECEIPT_NOT_FOUND" ? "MEMENTOS_PROJECT_REGISTRATION_RECEIPT_NOT_FOUND" : "MEMENTOS_PROJECT_REGISTRATION_CONFLICT";
58474
+ throw new MementosProjectRegistrationError(code, "guarded project operation was rejected without exposing private project data", { project_update_code: cause.code });
58475
+ }
58476
+ throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_ATOMICITY_UNAVAILABLE", "guarded project operation failed before a bounded public result was available");
58477
+ }
58259
58478
  function normalizedCallDigest(request) {
58260
58479
  return digestMementosProjectRegistrationValue({
58261
58480
  authority_route: request.authority_route,
@@ -58627,26 +58846,7 @@ class PackageOwnedMementosProjectRegistrationAuthority {
58627
58846
  this.db = db;
58628
58847
  this.now = options.now ?? (() => new Date().toISOString());
58629
58848
  this.faultInjector = options.faultInjector;
58630
- this.capabilityValue = {
58631
- authority: "mementos",
58632
- route: MEMENTOS_PROJECT_REGISTRATION_ROUTE,
58633
- package_version: options.packageVersion ?? getMementosPackageVersion(),
58634
- authority_id: options.authorityId ?? "mementos",
58635
- tenant_id: options.tenantId ?? "default",
58636
- corpus_id: options.corpusId ?? "default",
58637
- supported_resources: ["project"],
58638
- conditional_create: true,
58639
- immutable_receipts: true,
58640
- exact_terminal_lookup: true,
58641
- exact_readback: true,
58642
- conditional_inverse: true,
58643
- ambiguous_outcome_reconciliation: true,
58644
- guarded_update: true,
58645
- no_write_dry_run: true,
58646
- expected_revision_compare_and_swap: true,
58647
- caller_idempotency: true,
58648
- exact_inverse_rollback: true
58649
- };
58849
+ this.capabilityValue = buildMementosProjectRegistrationCapability(options);
58650
58850
  }
58651
58851
  fault(point, request) {
58652
58852
  this.faultInjector?.(point, {
@@ -58703,6 +58903,90 @@ class PackageOwnedMementosProjectRegistrationAuthority {
58703
58903
  supported_resources: ["project"]
58704
58904
  };
58705
58905
  }
58906
+ boundedGuardedUpdateResult(targetId2, project, receipt, bounds, startedAt) {
58907
+ if (!receipt || project.id !== targetId2 || receipt.target_id !== targetId2) {
58908
+ throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_ATOMICITY_UNAVAILABLE", "guarded project operation did not return its exact immutable receipt");
58909
+ }
58910
+ if (project.updated_at !== receipt.result_revision) {
58911
+ throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_ATOMICITY_UNAVAILABLE", "guarded project result revision did not match its immutable receipt");
58912
+ }
58913
+ const record = {
58914
+ target_id: targetId2,
58915
+ revision: receipt.result_revision,
58916
+ digest: receipt.result_digest
58917
+ };
58918
+ return withBoundedResponseControl({
58919
+ dry_run: false,
58920
+ applied: true,
58921
+ record,
58922
+ receipt: publicGuardedUpdateReceipt(receipt, this.capabilityValue)
58923
+ }, bounds, startedAt);
58924
+ }
58925
+ async guardedUpdateProject(targetId2, request) {
58926
+ const startedAt = Date.now();
58927
+ const { identity, path } = assertGuardedUpdateRequest(targetId2, request, this.capabilityValue);
58928
+ try {
58929
+ const result = applyProjectUpdate(targetId2, {
58930
+ ...identity,
58931
+ operation_id: request.operation_id,
58932
+ step_id: request.step_id,
58933
+ idempotency_key: request.idempotency_key,
58934
+ expected_revision: request.expected_revision,
58935
+ updates: { path }
58936
+ }, this.db, identity, (project) => projectRecord(this.db, project).digest);
58937
+ return this.boundedGuardedUpdateResult(targetId2, result.project, result.receipt, request, startedAt);
58938
+ } catch (cause) {
58939
+ return guardedProjectError(cause);
58940
+ }
58941
+ }
58942
+ async getGuardedProjectUpdateReceipt(targetId2, receiptId2, request) {
58943
+ const startedAt = Date.now();
58944
+ const identity = assertGuardedAuthorityRequest(targetId2, request, this.capabilityValue);
58945
+ requireString(receiptId2, "receipt_id", {
58946
+ min: 8,
58947
+ max: 128,
58948
+ pattern: OPERATION_PATTERN
58949
+ });
58950
+ try {
58951
+ const receipt = getProjectUpdateReceipt(targetId2, receiptId2, identity, this.db, identity);
58952
+ return withBoundedResponseControl({
58953
+ receipt: publicGuardedUpdateReceipt(receipt, this.capabilityValue)
58954
+ }, request, startedAt);
58955
+ } catch (cause) {
58956
+ return guardedProjectError(cause);
58957
+ }
58958
+ }
58959
+ async rollbackGuardedProjectUpdate(targetId2, request) {
58960
+ const startedAt = Date.now();
58961
+ const identity = assertGuardedAuthorityRequest(targetId2, request, this.capabilityValue);
58962
+ assertGuardedOperationFields(request);
58963
+ if (!request.accepted_receipt || typeof request.accepted_receipt !== "object") {
58964
+ throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "conditional rollback requires the exact sanitized accepted receipt");
58965
+ }
58966
+ requireString(request.accepted_receipt.receipt_id, "accepted_receipt.receipt_id", {
58967
+ min: 8,
58968
+ max: 128,
58969
+ pattern: OPERATION_PATTERN
58970
+ });
58971
+ try {
58972
+ const internalAccepted = getProjectUpdateReceipt(targetId2, request.accepted_receipt.receipt_id, identity, this.db, identity);
58973
+ const accepted = publicGuardedUpdateReceipt(internalAccepted, this.capabilityValue);
58974
+ if (accepted.direction !== "forward" || accepted.target_id !== targetId2 || request.expected_revision !== accepted.result_revision || canonicalMementosProjectRegistrationJson(request.accepted_receipt) !== canonicalMementosProjectRegistrationJson(accepted)) {
58975
+ throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "conditional rollback receipt does not exactly match the accepted forward update");
58976
+ }
58977
+ const result = rollbackProjectUpdate(targetId2, {
58978
+ ...identity,
58979
+ operation_id: request.operation_id,
58980
+ step_id: request.step_id,
58981
+ idempotency_key: request.idempotency_key,
58982
+ expected_revision: request.expected_revision,
58983
+ accepted_receipt_id: accepted.receipt_id
58984
+ }, this.db, identity, (project) => projectRecord(this.db, project).digest);
58985
+ return this.boundedGuardedUpdateResult(targetId2, result.project, result.receipt, request, startedAt);
58986
+ } catch (cause) {
58987
+ return guardedProjectError(cause);
58988
+ }
58989
+ }
58706
58990
  async create(request) {
58707
58991
  const startedAt = Date.now();
58708
58992
  const path = assertForwardRequest2(request, this.capabilityValue);
@@ -58799,9 +59083,9 @@ class PackageOwnedMementosProjectRegistrationAuthority {
58799
59083
  throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "resource_kind must be project");
58800
59084
  }
58801
59085
  requireString(request.target_id, "target_id", {
58802
- min: 51,
58803
- max: 51,
58804
- pattern: PROJECT_ID_PATTERN
59086
+ min: 8,
59087
+ max: 128,
59088
+ pattern: OPERATION_PATTERN
58805
59089
  });
58806
59090
  const path = ownedPath(request.target);
58807
59091
  const project = getProjectByExactId3(this.db, request.target_id);
@@ -59059,6 +59343,31 @@ function fromWireReadRequest(body) {
59059
59343
  target: new WirePathHandle(canonicalPath)
59060
59344
  };
59061
59345
  }
59346
+ function fromWireGuardedUpdateRequest(body) {
59347
+ const { target_id: targetId2, updates, ...request } = body;
59348
+ if (typeof targetId2 !== "string") {
59349
+ throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "target_id is required on the private guarded-update transport");
59350
+ }
59351
+ if (!updates || typeof updates !== "object" || Array.isArray(updates) || typeof updates["path"] !== "string") {
59352
+ throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "updates.path is required on the private guarded-update transport");
59353
+ }
59354
+ return {
59355
+ targetId: targetId2,
59356
+ request: {
59357
+ ...request,
59358
+ updates: {
59359
+ path: new WirePathHandle(String(updates["path"]))
59360
+ }
59361
+ }
59362
+ };
59363
+ }
59364
+ function exactWireIdentifier(body, field) {
59365
+ const value = body[field];
59366
+ if (typeof value !== "string") {
59367
+ throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", `${field} is required on the guarded-update transport`);
59368
+ }
59369
+ return value;
59370
+ }
59062
59371
  async function handleMementosProjectRegistrationHttpRequest(request, url, authority, basePath = "/v1/project-registration") {
59063
59372
  const path = url.pathname;
59064
59373
  if (path !== basePath && !path.startsWith(`${basePath}/`))
@@ -59093,6 +59402,21 @@ async function handleMementosProjectRegistrationHttpRequest(request, url, author
59093
59402
  if (action === "verify-inverse") {
59094
59403
  return json2({ verification: await authority.verifyInverse(fromWireRequest(body)) });
59095
59404
  }
59405
+ if (action === "projects/guarded-update") {
59406
+ const guarded = fromWireGuardedUpdateRequest(body);
59407
+ return json2(await authority.guardedUpdateProject(guarded.targetId, guarded.request));
59408
+ }
59409
+ if (action === "projects/update-receipts/lookup") {
59410
+ const targetId2 = exactWireIdentifier(body, "target_id");
59411
+ const receiptId2 = exactWireIdentifier(body, "receipt_id");
59412
+ const { target_id: _targetId, receipt_id: _receiptId, ...lookup } = body;
59413
+ return json2(await authority.getGuardedProjectUpdateReceipt(targetId2, receiptId2, lookup));
59414
+ }
59415
+ if (action === "projects/guarded-rollback") {
59416
+ const targetId2 = exactWireIdentifier(body, "target_id");
59417
+ const { target_id: _targetId, ...rollback } = body;
59418
+ return json2(await authority.rollbackGuardedProjectUpdate(targetId2, rollback));
59419
+ }
59096
59420
  return json2({
59097
59421
  error: "unknown Mementos project-registration route",
59098
59422
  code: "MEMENTOS_PROJECT_REGISTRATION_RECEIPT_NOT_FOUND"
@@ -59112,7 +59436,386 @@ async function handleMementosProjectRegistrationHttpRequest(request, url, author
59112
59436
  }, 500);
59113
59437
  }
59114
59438
  }
59439
+ // src/project-registration/project-resources.ts
59440
+ init_api_mode();
59441
+ init_database();
59442
+ init_memories();
59443
+ var DEFAULT_PAGE_LIMIT = 100;
59444
+ var MAX_PAGE_LIMIT = 1000;
59445
+ var CURSOR_SCHEMA = "mementos.project-resources.cursor.v1";
59446
+
59447
+ class MementosProjectResourceError extends Error {
59448
+ code;
59449
+ details;
59450
+ constructor(code, message, details = {}) {
59451
+ super(message);
59452
+ this.code = code;
59453
+ this.details = details;
59454
+ this.name = "MementosProjectResourceError";
59455
+ }
59456
+ }
59457
+ function timestamp(value) {
59458
+ return value instanceof Date ? value.toISOString() : String(value);
59459
+ }
59460
+ function normalizeSqlValue(value) {
59461
+ if (value instanceof Date)
59462
+ return value.toISOString();
59463
+ if (Array.isArray(value))
59464
+ return value.map(normalizeSqlValue);
59465
+ if (!value || typeof value !== "object")
59466
+ return value;
59467
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, normalizeSqlValue(item)]));
59468
+ }
59469
+ function exactProject(db, projectId) {
59470
+ const row = db.get("SELECT id, name, path, description, memory_prefix, created_at, updated_at FROM projects WHERE id = ? LIMIT 1", projectId);
59471
+ if (!row) {
59472
+ throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_PROJECT_NOT_FOUND", `Mementos project not found: ${projectId}`, { project_id: projectId });
59473
+ }
59474
+ return row;
59475
+ }
59476
+ function resourceKey(resource) {
59477
+ const rank = MEMENTOS_PROJECT_RESOURCE_KINDS.indexOf(resource.resource_kind);
59478
+ return `${String(rank).padStart(2, "0")}:${resource.stable_id}`;
59479
+ }
59480
+ function projectResource(project) {
59481
+ const normalized = normalizeSqlValue(project);
59482
+ return {
59483
+ authority: "mementos",
59484
+ source_package: "@hasna/mementos",
59485
+ project_id: project.id,
59486
+ resource_kind: "project",
59487
+ stable_id: project.id,
59488
+ revision: timestamp(project.updated_at),
59489
+ digest: digestMementosProjectRegistrationValue(normalized),
59490
+ membership: "project_aggregate"
59491
+ };
59492
+ }
59493
+ function memoryResources(db, projectId) {
59494
+ const rows = db.all("SELECT * FROM memories WHERE project_id = ? ORDER BY id ASC", projectId);
59495
+ return rows.map((row) => {
59496
+ const memory = normalizeSqlValue(parseMemoryRow(row));
59497
+ return {
59498
+ authority: "mementos",
59499
+ source_package: "@hasna/mementos",
59500
+ project_id: projectId,
59501
+ resource_kind: row["category"] === "knowledge" ? "knowledge" : "memory",
59502
+ stable_id: String(row["id"]),
59503
+ revision: timestamp(row["updated_at"]),
59504
+ digest: digestMementosProjectRegistrationValue(memory),
59505
+ membership: "explicit_project_id_or_focus"
59506
+ };
59507
+ });
59508
+ }
59509
+ function sessionResources(db, projectId) {
59510
+ const rows = db.all("SELECT * FROM session_memory_jobs WHERE project_id = ? ORDER BY id ASC", projectId);
59511
+ return rows.map((row) => {
59512
+ const normalized = {
59513
+ id: String(row["id"]),
59514
+ session_id: String(row["session_id"]),
59515
+ agent_id: row["agent_id"] === null ? null : String(row["agent_id"] ?? "") || null,
59516
+ project_id: row["project_id"] === null ? null : String(row["project_id"] ?? "") || null,
59517
+ source: String(row["source"]),
59518
+ status: String(row["status"]),
59519
+ transcript: String(row["transcript"]),
59520
+ chunk_count: Number(row["chunk_count"]),
59521
+ memories_extracted: Number(row["memories_extracted"]),
59522
+ error: row["error"] === null ? null : String(row["error"] ?? "") || null,
59523
+ metadata: typeof row["metadata"] === "string" ? JSON.parse(row["metadata"] || "{}") : normalizeSqlValue(row["metadata"] ?? {}),
59524
+ created_at: timestamp(row["created_at"]),
59525
+ started_at: row["started_at"] === null ? null : timestamp(row["started_at"]),
59526
+ completed_at: row["completed_at"] === null ? null : timestamp(row["completed_at"])
59527
+ };
59528
+ return {
59529
+ authority: "mementos",
59530
+ source_package: "@hasna/mementos",
59531
+ project_id: projectId,
59532
+ resource_kind: "session",
59533
+ stable_id: String(row["id"]),
59534
+ revision: timestamp(row["completed_at"] ?? row["started_at"] ?? row["created_at"]),
59535
+ digest: digestMementosProjectRegistrationValue(normalized),
59536
+ membership: "explicit_project_id_or_focus"
59537
+ };
59538
+ });
59539
+ }
59540
+ function normalizeResourceKinds(value) {
59541
+ if (!value)
59542
+ return [...MEMENTOS_PROJECT_RESOURCE_KINDS];
59543
+ if (value.length === 0) {
59544
+ throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INVALID_INPUT", "resource_kinds must contain at least one supported resource kind");
59545
+ }
59546
+ const requested = new Set(value);
59547
+ for (const kind of requested) {
59548
+ if (!MEMENTOS_PROJECT_RESOURCE_KINDS.includes(kind)) {
59549
+ throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INVALID_INPUT", `Unsupported Mementos project resource kind: ${kind}`);
59550
+ }
59551
+ }
59552
+ return MEMENTOS_PROJECT_RESOURCE_KINDS.filter((kind) => requested.has(kind));
59553
+ }
59554
+ function normalizeLimit(value) {
59555
+ const limit = value ?? DEFAULT_PAGE_LIMIT;
59556
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT) {
59557
+ throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INVALID_INPUT", `limit must be an integer between 1 and ${MAX_PAGE_LIMIT}`);
59558
+ }
59559
+ return limit;
59560
+ }
59561
+ function encodeCursor(cursor) {
59562
+ return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
59563
+ }
59564
+ function decodeCursor(raw) {
59565
+ try {
59566
+ const parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
59567
+ if (parsed.schema !== CURSOR_SCHEMA || typeof parsed.project_id !== "string" || typeof parsed.collection_revision !== "string" || !Array.isArray(parsed.resource_kinds) || typeof parsed.after_key !== "string") {
59568
+ throw new Error("invalid cursor shape");
59569
+ }
59570
+ return parsed;
59571
+ } catch {
59572
+ throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INVALID_INPUT", "cursor is not a valid Mementos project-resource cursor");
59573
+ }
59574
+ }
59575
+ function localPopulation(projectId, db, resourceKinds) {
59576
+ const project = exactProject(db, projectId);
59577
+ const selected = new Set(resourceKinds);
59578
+ const resources = [
59579
+ ...selected.has("project") ? [projectResource(project)] : [],
59580
+ ...memoryResources(db, projectId).filter((resource) => selected.has(resource.resource_kind)),
59581
+ ...selected.has("session") ? sessionResources(db, projectId) : []
59582
+ ].sort((left, right) => resourceKey(left).localeCompare(resourceKey(right)));
59583
+ const collectionRevision = digestMementosProjectRegistrationValue({
59584
+ schema: MEMENTOS_PROJECT_RESOURCE_ROUTE,
59585
+ project_id: projectId,
59586
+ project_revision: timestamp(project.updated_at),
59587
+ resource_kinds: resourceKinds,
59588
+ resources: resources.map((resource) => ({
59589
+ resource_kind: resource.resource_kind,
59590
+ stable_id: resource.stable_id,
59591
+ revision: resource.revision,
59592
+ digest: resource.digest
59593
+ }))
59594
+ });
59595
+ return { project, resources, collectionRevision };
59596
+ }
59597
+ function readMementosProjectResourcePage(projectId, options = {}, db, authorityOptions = {}) {
59598
+ const resourceKinds = normalizeResourceKinds(options.resource_kinds);
59599
+ const limit = normalizeLimit(options.limit);
59600
+ if (!db && isApiMode()) {
59601
+ const { data } = apiJson("GET", `/projects/${encodeURIComponent(projectId)}/resources${toQuery({
59602
+ limit,
59603
+ cursor: options.cursor ?? undefined,
59604
+ resource_kinds: resourceKinds.join(",")
59605
+ })}`);
59606
+ return data;
59607
+ }
59608
+ const d = db ?? getDatabase();
59609
+ const { project, resources, collectionRevision } = localPopulation(projectId, d, resourceKinds);
59610
+ let start = 0;
59611
+ if (options.cursor) {
59612
+ const cursor = decodeCursor(options.cursor);
59613
+ if (cursor.project_id !== projectId || JSON.stringify(cursor.resource_kinds) !== JSON.stringify(resourceKinds)) {
59614
+ throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INVALID_INPUT", "cursor does not belong to this project and resource-kind selection");
59615
+ }
59616
+ if (cursor.collection_revision !== collectionRevision) {
59617
+ throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_COLLECTION_CHANGED", "Mementos project resource collection changed; restart from the first page", {
59618
+ cursor_collection_revision: cursor.collection_revision,
59619
+ current_collection_revision: collectionRevision
59620
+ });
59621
+ }
59622
+ const afterIndex = resources.findIndex((resource) => resourceKey(resource) === cursor.after_key);
59623
+ if (afterIndex < 0) {
59624
+ throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_COLLECTION_CHANGED", "Mementos project resource cursor no longer names a member; restart from the first page");
59625
+ }
59626
+ start = afterIndex + 1;
59627
+ }
59628
+ const pageResources = resources.slice(start, start + limit);
59629
+ const hasMore = start + pageResources.length < resources.length;
59630
+ const nextCursor = hasMore && pageResources.length > 0 ? encodeCursor({
59631
+ schema: CURSOR_SCHEMA,
59632
+ project_id: projectId,
59633
+ collection_revision: collectionRevision,
59634
+ resource_kinds: resourceKinds,
59635
+ after_key: resourceKey(pageResources[pageResources.length - 1])
59636
+ }) : null;
59637
+ const capability = buildMementosProjectRegistrationCapability(authorityOptions);
59638
+ return {
59639
+ schema: "mementos.project-resources.v1",
59640
+ authority: {
59641
+ authority: capability.authority,
59642
+ authority_id: capability.authority_id,
59643
+ tenant_id: capability.tenant_id,
59644
+ corpus_id: capability.corpus_id,
59645
+ package_version: capability.package_version
59646
+ },
59647
+ project_id: projectId,
59648
+ project_revision: timestamp(project.updated_at),
59649
+ collection_revision: collectionRevision,
59650
+ resource_kinds: resourceKinds,
59651
+ resources: pageResources,
59652
+ count: pageResources.length,
59653
+ total: resources.length,
59654
+ limit,
59655
+ cursor: options.cursor ?? null,
59656
+ next_cursor: nextCursor,
59657
+ has_more: hasMore,
59658
+ complete: true,
59659
+ truncated: false
59660
+ };
59661
+ }
59662
+ function getMementosProjectResourceExact(projectId, resourceKind, stableId, db, authorityOptions = {}) {
59663
+ if (!MEMENTOS_PROJECT_RESOURCE_KINDS.includes(resourceKind)) {
59664
+ throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INVALID_INPUT", `Unsupported Mementos project resource kind: ${resourceKind}`);
59665
+ }
59666
+ if (!db && isApiMode()) {
59667
+ const { data } = apiJson("GET", `/projects/${encodeURIComponent(projectId)}/resources/${encodeURIComponent(resourceKind)}/${encodeURIComponent(stableId)}`);
59668
+ return data;
59669
+ }
59670
+ const d = db ?? getDatabase();
59671
+ const { project, resources, collectionRevision } = localPopulation(projectId, d, [
59672
+ resourceKind
59673
+ ]);
59674
+ const resource = resources.find((candidate) => candidate.stable_id === stableId);
59675
+ if (!resource) {
59676
+ throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_NOT_FOUND", `Mementos ${resourceKind} resource not found in project ${projectId}: ${stableId}`, { project_id: projectId, resource_kind: resourceKind, stable_id: stableId });
59677
+ }
59678
+ const capability = buildMementosProjectRegistrationCapability(authorityOptions);
59679
+ return {
59680
+ schema: "mementos.project-resource.v1",
59681
+ authority: {
59682
+ authority: capability.authority,
59683
+ authority_id: capability.authority_id,
59684
+ tenant_id: capability.tenant_id,
59685
+ corpus_id: capability.corpus_id,
59686
+ package_version: capability.package_version
59687
+ },
59688
+ project_id: projectId,
59689
+ project_revision: timestamp(project.updated_at),
59690
+ collection_revision: collectionRevision,
59691
+ resource,
59692
+ complete: true,
59693
+ truncated: false
59694
+ };
59695
+ }
59696
+ // src/server/routes/projects.ts
59697
+ init_router();
59698
+ addRoute("GET", "/api/projects", (_req, url) => {
59699
+ const q = getSearchParams(url);
59700
+ const projects = listProjects();
59701
+ if (q["fields"]) {
59702
+ const fields = q["fields"].split(",").map((f) => f.trim());
59703
+ const filtered = projects.map((p) => Object.fromEntries(fields.map((f) => [f, p[f]]).filter(([, v]) => v !== undefined)));
59704
+ return json({ projects: filtered, count: filtered.length });
59705
+ }
59706
+ return json({ projects, count: projects.length });
59707
+ });
59708
+ addRoute("POST", "/api/projects", async (req) => {
59709
+ const body = await readJson(req);
59710
+ if (!body || !body["name"] || !body["path"]) {
59711
+ return errorResponse("Missing required fields: name, path", 400);
59712
+ }
59713
+ const project = registerProject(body["name"], body["path"], body["description"], body["memory_prefix"]);
59714
+ return json(project, 201);
59715
+ });
59716
+ addRoute("GET", "/api/projects/:id", (_req, _url, params) => {
59717
+ const project = getProject(params["id"]);
59718
+ if (!project)
59719
+ return errorResponse("Project not found", 404);
59720
+ return json(project);
59721
+ });
59722
+ function projectResourceError(error) {
59723
+ const status = error.code === "MEMENTOS_PROJECT_RESOURCE_PROJECT_NOT_FOUND" || error.code === "MEMENTOS_PROJECT_RESOURCE_NOT_FOUND" ? 404 : error.code === "MEMENTOS_PROJECT_RESOURCE_INVALID_INPUT" ? 400 : 409;
59724
+ return errorResponse(error.message, status, { code: error.code, ...error.details });
59725
+ }
59726
+ function parseResourceKinds(raw) {
59727
+ if (raw === undefined)
59728
+ return;
59729
+ const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
59730
+ for (const value of values) {
59731
+ if (!MEMENTOS_PROJECT_RESOURCE_KINDS.includes(value)) {
59732
+ throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INVALID_INPUT", `Unsupported Mementos project resource kind: ${value}`);
59733
+ }
59734
+ }
59735
+ return values;
59736
+ }
59737
+ addRoute("GET", "/api/projects/:id/resources", (_req, url, params) => {
59738
+ const q = getSearchParams(url);
59739
+ try {
59740
+ const parsedLimit = q["limit"] === undefined ? undefined : Number(q["limit"]);
59741
+ return json(readMementosProjectResourcePage(params["id"], {
59742
+ limit: parsedLimit,
59743
+ cursor: q["cursor"],
59744
+ resource_kinds: parseResourceKinds(q["resource_kinds"])
59745
+ }));
59746
+ } catch (error) {
59747
+ if (error instanceof MementosProjectResourceError)
59748
+ return projectResourceError(error);
59749
+ throw error;
59750
+ }
59751
+ });
59752
+ addRoute("GET", "/api/projects/:id/resources/:kind/:resource_id", (_req, _url, params) => {
59753
+ try {
59754
+ return json(getMementosProjectResourceExact(params["id"], params["kind"], params["resource_id"]));
59755
+ } catch (error) {
59756
+ if (error instanceof MementosProjectResourceError)
59757
+ return projectResourceError(error);
59758
+ throw error;
59759
+ }
59760
+ });
59761
+ function guardedUpdateError(error) {
59762
+ const status = error.code === "PROJECT_UPDATE_AUTHORITY_MISMATCH" ? 403 : error.code === "PROJECT_UPDATE_NOT_FOUND" || error.code === "PROJECT_UPDATE_RECEIPT_NOT_FOUND" ? 404 : error.code === "PROJECT_UPDATE_INVALID_INPUT" ? 400 : 409;
59763
+ return errorResponse(error.message, status, { code: error.code, ...error.details });
59764
+ }
59765
+ addRoute("PATCH", "/api/projects/:id", () => errorResponse("Unguarded project updates are disabled; use POST /projects/:id/guarded-update", 428));
59766
+ addRoute("POST", "/api/projects/:id/guarded-update", async (req, _url, params) => {
59767
+ const body = await readJson(req);
59768
+ if (!body)
59769
+ return errorResponse("Invalid JSON body", 400);
59770
+ try {
59771
+ const request = body;
59772
+ return json(request.dry_run ? previewProjectUpdate(params["id"], request) : applyProjectUpdate(params["id"], request));
59773
+ } catch (error) {
59774
+ if (error instanceof ProjectGuardedUpdateError)
59775
+ return guardedUpdateError(error);
59776
+ throw error;
59777
+ }
59778
+ });
59779
+ addRoute("POST", "/api/projects/:id/guarded-rollback", async (req, _url, params) => {
59780
+ const body = await readJson(req);
59781
+ if (!body)
59782
+ return errorResponse("Invalid JSON body", 400);
59783
+ try {
59784
+ return json(rollbackProjectUpdate(params["id"], body));
59785
+ } catch (error) {
59786
+ if (error instanceof ProjectGuardedUpdateError)
59787
+ return guardedUpdateError(error);
59788
+ throw error;
59789
+ }
59790
+ });
59791
+ addRoute("POST", "/api/projects/:id/update-receipts/lookup", async (req, _url, params) => {
59792
+ const body = await readJson(req);
59793
+ if (!body || typeof body["receipt_id"] !== "string") {
59794
+ return errorResponse("receipt_id is required", 400);
59795
+ }
59796
+ try {
59797
+ const identity = {
59798
+ authority_id: String(body["authority_id"] ?? ""),
59799
+ tenant_id: String(body["tenant_id"] ?? ""),
59800
+ corpus_id: String(body["corpus_id"] ?? "")
59801
+ };
59802
+ return json(getProjectUpdateReceipt(params["id"], body["receipt_id"], identity));
59803
+ } catch (error) {
59804
+ if (error instanceof ProjectGuardedUpdateError)
59805
+ return guardedUpdateError(error);
59806
+ throw error;
59807
+ }
59808
+ });
59809
+ addRoute("GET", "/api/projects/:id/agents", (_req, _url, params) => {
59810
+ const project = getProject(params["id"]);
59811
+ if (!project)
59812
+ return errorResponse("Project not found", 404);
59813
+ const agents = listAgentsByProject(project.id);
59814
+ return json({ agents, count: agents.length });
59815
+ });
59816
+
59115
59817
  // src/server/routes/project-registration.ts
59818
+ init_database();
59116
59819
  init_router();
59117
59820
  var handle = async (request, url) => {
59118
59821
  const basePath = url.pathname.startsWith("/v1/") ? "/v1/project-registration" : "/api/project-registration";
@@ -59129,6 +59832,9 @@ addRoute("POST", "/api/project-registration/receipts/lookup", handle);
59129
59832
  addRoute("POST", "/api/project-registration/read-exact", handle);
59130
59833
  addRoute("POST", "/api/project-registration/compensate", handle);
59131
59834
  addRoute("POST", "/api/project-registration/verify-inverse", handle);
59835
+ addRoute("POST", "/api/project-registration/projects/guarded-update", handle);
59836
+ addRoute("POST", "/api/project-registration/projects/update-receipts/lookup", handle);
59837
+ addRoute("POST", "/api/project-registration/projects/guarded-rollback", handle);
59132
59838
 
59133
59839
  // src/server/routes/entities.ts
59134
59840
  init_entities();
@@ -60769,7 +61475,7 @@ function parseMemoryLink(row) {
60769
61475
  function createMemoryLink(input, db) {
60770
61476
  const d = db || getDatabase();
60771
61477
  const id = shortUuid();
60772
- const timestamp = now();
61478
+ const timestamp2 = now();
60773
61479
  d.run(`INSERT OR IGNORE INTO memory_links (id, source_memory_id, target_memory_id, relation_type, run_id, metadata, created_at)
60774
61480
  VALUES (?, ?, ?, ?, ?, ?, ?)`, [
60775
61481
  id,
@@ -60778,7 +61484,7 @@ function createMemoryLink(input, db) {
60778
61484
  input.relation_type,
60779
61485
  input.run_id ?? null,
60780
61486
  JSON.stringify(input.metadata ?? {}),
60781
- timestamp
61487
+ timestamp2
60782
61488
  ]);
60783
61489
  const row = d.query(`SELECT * FROM memory_links
60784
61490
  WHERE source_memory_id = ? AND target_memory_id = ? AND relation_type = ? AND COALESCE(run_id, '') = ?