@semiont/backend 0.5.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import winston from 'winston';
3
3
  import { recordSubscriberConnect, recordSubscriberDisconnect, withTraceparent, withSpan, recordBusEmit, SpanKind, injectTraceparent, getLogTraceContext } from '@semiont/observability';
4
4
  import { z } from 'zod';
5
5
  import jwt from 'jsonwebtoken';
6
- import { email, userId, googleCredential, EventBus, accessToken, resourceId, CHANNEL_SCHEMAS, userToDid, busLog, getPrimaryMediaType, decodeRepresentation } from '@semiont/core';
6
+ import { email, userId, googleCredential, agentToDid, EventBus, accessToken, userToDid, resourceId, CHANNEL_SCHEMAS, busLog, getPrimaryMediaType, decodeRepresentation } from '@semiont/core';
7
7
  import { cors } from 'hono/cors';
8
8
  import { serve } from '@hono/node-server';
9
9
  import { Hono } from 'hono';
@@ -180,6 +180,12 @@ var init_jwt_types = __esm({
180
180
  domain: z.string(),
181
181
  provider: z.string(),
182
182
  isAdmin: z.boolean(),
183
+ // For software-agent tokens: the agent's DID is asserted by the auth
184
+ // route (which knows the (inferenceProvider, model) the token is being
185
+ // issued for) and carried on the JWT. The bus uses this directly as
186
+ // `_userId` instead of recomputing `userToDid(user)`. Unset for human
187
+ // tokens.
188
+ agentDid: z.string().optional(),
183
189
  iat: z.number().optional(),
184
190
  exp: z.number().optional()
185
191
  });
@@ -222,6 +228,18 @@ var init_jwt = __esm({
222
228
  }
223
229
  return this.siteConfig;
224
230
  }
231
+ /**
232
+ * Get the deployment domain to use for issuing agent identities.
233
+ * Used by `/api/tokens/agent` to mint DIDs of the shape
234
+ * `did:web:<domain>:agents:<provider>:<model>`.
235
+ */
236
+ static getDomainForAgent() {
237
+ const config2 = this.getSiteConfig();
238
+ if (!config2.domain) {
239
+ throw new Error("site.domain is required to issue agent tokens");
240
+ }
241
+ return config2.domain;
242
+ }
225
243
  /**
226
244
  * Override configuration for testing purposes
227
245
  * @param config The configuration to use
@@ -10538,48 +10556,103 @@ var openapi_default = {
10538
10556
  ]
10539
10557
  },
10540
10558
  Agent: {
10541
- type: "object",
10542
- description: "Minimal Person/Organization for attribution (W3C PROV compatible)",
10543
- additionalProperties: true,
10544
- properties: {
10545
- "@id": {
10546
- type: "string",
10547
- format: "uri",
10548
- description: "Unique identifier for the agent"
10549
- },
10550
- "@type": {
10551
- description: "Type(s) of agent (Person, Organization, Software)",
10552
- oneOf: [
10553
- {
10559
+ description: "Web Annotation / W3C PROV Agent. Discriminated by @type \u2014 Person, Organization, or Software. Each branch carries fields appropriate to its kind. Software peers are first-class participants, not a sub-class of Person.",
10560
+ oneOf: [
10561
+ {
10562
+ type: "object",
10563
+ additionalProperties: true,
10564
+ properties: {
10565
+ "@type": {
10566
+ type: "string",
10567
+ const: "Person"
10568
+ },
10569
+ "@id": {
10570
+ type: "string",
10571
+ format: "uri",
10572
+ description: "DID-shaped identifier (e.g. did:web:host:users:email%40host)"
10573
+ },
10574
+ name: {
10575
+ type: "string",
10576
+ description: "Display name"
10577
+ },
10578
+ nickname: {
10554
10579
  type: "string"
10555
10580
  },
10556
- {
10557
- type: "array",
10558
- items: {
10559
- type: "string"
10560
- },
10561
- minItems: 1
10581
+ email: {
10582
+ type: "string"
10583
+ },
10584
+ email_sha1: {
10585
+ type: "string"
10586
+ },
10587
+ homepage: {
10588
+ type: "string"
10562
10589
  }
10590
+ },
10591
+ required: [
10592
+ "@type",
10593
+ "name"
10563
10594
  ]
10564
10595
  },
10565
- name: {
10566
- type: "string"
10567
- },
10568
- nickname: {
10569
- type: "string"
10570
- },
10571
- email: {
10572
- type: "string"
10573
- },
10574
- email_sha1: {
10575
- type: "string"
10596
+ {
10597
+ type: "object",
10598
+ additionalProperties: true,
10599
+ properties: {
10600
+ "@type": {
10601
+ type: "string",
10602
+ const: "Organization"
10603
+ },
10604
+ "@id": {
10605
+ type: "string",
10606
+ format: "uri"
10607
+ },
10608
+ name: {
10609
+ type: "string"
10610
+ },
10611
+ homepage: {
10612
+ type: "string"
10613
+ }
10614
+ },
10615
+ required: [
10616
+ "@type",
10617
+ "name"
10618
+ ]
10576
10619
  },
10577
- homepage: {
10578
- type: "string"
10620
+ {
10621
+ type: "object",
10622
+ additionalProperties: true,
10623
+ properties: {
10624
+ "@type": {
10625
+ type: "string",
10626
+ const: "Software"
10627
+ },
10628
+ "@id": {
10629
+ type: "string",
10630
+ format: "uri",
10631
+ description: "DID-shaped identifier (e.g. did:web:host:agents:provider:model)"
10632
+ },
10633
+ name: {
10634
+ type: "string",
10635
+ description: "Stable human-friendly label. Not parsed; UI composes display from structured fields."
10636
+ },
10637
+ provider: {
10638
+ type: "string",
10639
+ description: "Inference provider (e.g. ollama, anthropic)"
10640
+ },
10641
+ model: {
10642
+ type: "string",
10643
+ description: "Model identifier (e.g. gemma2:27b, claude-3-5-sonnet)"
10644
+ },
10645
+ parameters: {
10646
+ type: "object",
10647
+ additionalProperties: true,
10648
+ description: "Inference parameters (temperature, maxTokens, systemPrompt, etc.). Runtime metadata, not part of identity."
10649
+ }
10650
+ },
10651
+ required: [
10652
+ "@type",
10653
+ "name"
10654
+ ]
10579
10655
  }
10580
- },
10581
- required: [
10582
- "name"
10583
10656
  ]
10584
10657
  },
10585
10658
  Annotation: {
@@ -10637,7 +10710,8 @@ var openapi_default = {
10637
10710
  description: "W3C Web Annotation body. Optional per the W3C spec \u2014 annotations whose motivation alone is meaningful (highlighting) legitimately omit it. Present values are either a single body or a non-empty array of bodies; the prior empty-array 'stub' branch has been removed (it was a naming lie shared between highlights and never-actually-emitted stub references, and the source of the #651 reference-annotation validator bug)."
10638
10711
  },
10639
10712
  creator: {
10640
- $ref: "#/components/schemas/Agent"
10713
+ $ref: "#/components/schemas/Agent",
10714
+ description: "Web Annotation creator \u2014 the entity that initiated the annotation. For human-driven work this is a Person; for autonomous-agent work this is a Software peer."
10641
10715
  },
10642
10716
  created: {
10643
10717
  type: "string"
@@ -10646,7 +10720,34 @@ var openapi_default = {
10646
10720
  type: "string"
10647
10721
  },
10648
10722
  generator: {
10649
- $ref: "#/components/schemas/Agent"
10723
+ oneOf: [
10724
+ {
10725
+ $ref: "#/components/schemas/Agent"
10726
+ },
10727
+ {
10728
+ type: "array",
10729
+ items: {
10730
+ $ref: "#/components/schemas/Agent"
10731
+ },
10732
+ minItems: 1
10733
+ }
10734
+ ],
10735
+ description: "Web Annotation generator \u2014 the SoftwareAgent that produced the annotation, when software was involved. Absent for purely manual annotations. Single object is the common case; array supports pipelines that combine multiple software peers."
10736
+ },
10737
+ wasAttributedTo: {
10738
+ oneOf: [
10739
+ {
10740
+ $ref: "#/components/schemas/Agent"
10741
+ },
10742
+ {
10743
+ type: "array",
10744
+ items: {
10745
+ $ref: "#/components/schemas/Agent"
10746
+ },
10747
+ minItems: 1
10748
+ }
10749
+ ],
10750
+ description: "PROV-O wasAttributedTo \u2014 all parties responsible for this annotation. For human-prompted AI work this combines `creator` (the Person) and `generator` (the Software). For purely manual annotations it equals `[creator]`; for autonomous-agent work it equals `[generator]` (and `creator` may be the same Software)."
10650
10751
  }
10651
10752
  },
10652
10753
  required: [
@@ -11731,18 +11832,6 @@ var openapi_default = {
11731
11832
  contentByteSize: {
11732
11833
  type: "integer"
11733
11834
  },
11734
- creationMethod: {
11735
- type: "string",
11736
- enum: [
11737
- "api",
11738
- "upload",
11739
- "ui",
11740
- "reference",
11741
- "cli",
11742
- "clone",
11743
- "generated"
11744
- ]
11745
- },
11746
11835
  entityTypes: {
11747
11836
  type: "array",
11748
11837
  items: {
@@ -11794,8 +11883,7 @@ var openapi_default = {
11794
11883
  required: [
11795
11884
  "name",
11796
11885
  "format",
11797
- "contentChecksum",
11798
- "creationMethod"
11886
+ "contentChecksum"
11799
11887
  ]
11800
11888
  },
11801
11889
  ResourceClonedPayload: {
@@ -11817,18 +11905,6 @@ var openapi_default = {
11817
11905
  parentResourceId: {
11818
11906
  type: "string"
11819
11907
  },
11820
- creationMethod: {
11821
- type: "string",
11822
- enum: [
11823
- "api",
11824
- "upload",
11825
- "ui",
11826
- "reference",
11827
- "cli",
11828
- "clone",
11829
- "generated"
11830
- ]
11831
- },
11832
11908
  entityTypes: {
11833
11909
  type: "array",
11834
11910
  items: {
@@ -11843,8 +11919,7 @@ var openapi_default = {
11843
11919
  "name",
11844
11920
  "format",
11845
11921
  "contentChecksum",
11846
- "parentResourceId",
11847
- "creationMethod"
11922
+ "parentResourceId"
11848
11923
  ]
11849
11924
  },
11850
11925
  ResourceUpdatedPayload: {
@@ -12984,19 +13059,6 @@ var openapi_default = {
12984
13059
  type: "boolean",
12985
13060
  description: "Application-specific: Whether this resource is a draft"
12986
13061
  },
12987
- creationMethod: {
12988
- type: "string",
12989
- enum: [
12990
- "api",
12991
- "upload",
12992
- "ui",
12993
- "reference",
12994
- "cli",
12995
- "clone",
12996
- "generated"
12997
- ],
12998
- description: "Application-specific: How this resource was created"
12999
- },
13000
13062
  sourceAnnotationId: {
13001
13063
  type: "string",
13002
13064
  description: "Application-specific: ID of annotation that triggered generation"
@@ -15799,9 +15861,6 @@ var openapi_default = {
15799
15861
  type: "string"
15800
15862
  }
15801
15863
  },
15802
- creationMethod: {
15803
- type: "string"
15804
- },
15805
15864
  isDraft: {
15806
15865
  type: "boolean"
15807
15866
  },
@@ -16140,7 +16199,15 @@ var OAuthService = class {
16140
16199
  const refreshToken = JWTService.generateToken(jwtPayload, "30d");
16141
16200
  return { user, token, refreshToken, isNewUser };
16142
16201
  }
16143
- static async getUserFromToken(token) {
16202
+ /**
16203
+ * Resolve a JWT to its authenticated principal — the User row plus the
16204
+ * principal's DID when the token was issued for a software agent. The
16205
+ * `agentDid` is set by `/api/tokens/agent` and is the read-side
16206
+ * inverse of "who is this token *acting as*?" — bus middleware uses it
16207
+ * directly so emits attribute to the agent rather than the synthetic
16208
+ * service-account User backing the token.
16209
+ */
16210
+ static async getPrincipalFromToken(token) {
16144
16211
  const payload = JWTService.verifyToken(token);
16145
16212
  if (!payload.userId) {
16146
16213
  throw new Error("Invalid token: missing userId");
@@ -16152,7 +16219,7 @@ var OAuthService = class {
16152
16219
  if (!user || !user.isActive) {
16153
16220
  throw new Error("User not found or inactive");
16154
16221
  }
16155
- return user;
16222
+ return payload.agentDid ? { user, agentDid: payload.agentDid } : { user };
16156
16223
  }
16157
16224
  static async acceptTerms(userId2) {
16158
16225
  const prisma2 = DatabaseConnection.getClient();
@@ -16209,9 +16276,10 @@ var authMiddleware = async (c, next) => {
16209
16276
  return c.json({ error: "Unauthorized" }, 401);
16210
16277
  }
16211
16278
  try {
16212
- const user = await OAuthService.getUserFromToken(accessToken(tokenStr));
16279
+ const { user, agentDid } = await OAuthService.getPrincipalFromToken(accessToken(tokenStr));
16213
16280
  c.set("user", user);
16214
16281
  c.set("token", tokenStr);
16282
+ c.set("principalDid", agentDid ?? userToDid(user));
16215
16283
  logger2.debug("Authentication successful", {
16216
16284
  type: "auth_success",
16217
16285
  userId: user.id,
@@ -16596,10 +16664,10 @@ authRouter.post("/api/tokens/mcp-generate", authMiddleware, async (c) => {
16596
16664
  return c.json({ error: "Failed to generate refresh token" }, 401);
16597
16665
  }
16598
16666
  });
16599
- authRouter.post("/api/tokens/worker", async (c) => {
16667
+ authRouter.post("/api/tokens/agent", async (c) => {
16600
16668
  const workerSecret = process.env.SEMIONT_WORKER_SECRET;
16601
16669
  if (!workerSecret) {
16602
- return c.json({ error: "Worker authentication not configured" }, 503);
16670
+ return c.json({ error: "Agent authentication not configured" }, 503);
16603
16671
  }
16604
16672
  let body;
16605
16673
  try {
@@ -16608,24 +16676,51 @@ authRouter.post("/api/tokens/worker", async (c) => {
16608
16676
  return c.json({ error: "Invalid request body" }, 400);
16609
16677
  }
16610
16678
  if (body.secret !== workerSecret) {
16611
- return c.json({ error: "Invalid worker secret" }, 401);
16612
- }
16679
+ return c.json({ error: "Invalid agent secret" }, 401);
16680
+ }
16681
+ if (!body.provider || typeof body.provider !== "string") {
16682
+ return c.json({ error: "provider is required" }, 400);
16683
+ }
16684
+ if (!body.model || typeof body.model !== "string") {
16685
+ return c.json({ error: "model is required" }, 400);
16686
+ }
16687
+ const inferenceProvider = body.provider;
16688
+ const model = body.model;
16689
+ const siteDomain = JWTService.getDomainForAgent();
16690
+ const emailHost = siteDomain.split(":")[0];
16691
+ const providerId = `${inferenceProvider}:${model}`;
16692
+ const slug = providerId.replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
16693
+ const agentEmail = `${slug}@agents.${emailHost}`;
16694
+ const agentName = `${inferenceProvider} ${model}`;
16613
16695
  const prisma2 = DatabaseConnection.getClient();
16614
- const workerUser = await prisma2.user.findUnique({
16615
- where: { email: "worker@semiont.local" }
16696
+ const agentUser = await prisma2.user.upsert({
16697
+ where: { provider_providerId: { provider: "agent", providerId } },
16698
+ update: {
16699
+ name: agentName,
16700
+ isActive: true,
16701
+ lastLogin: /* @__PURE__ */ new Date()
16702
+ },
16703
+ create: {
16704
+ email: agentEmail,
16705
+ name: agentName,
16706
+ provider: "agent",
16707
+ providerId,
16708
+ domain: siteDomain,
16709
+ isActive: true,
16710
+ isAdmin: false
16711
+ }
16616
16712
  });
16617
- if (!workerUser) {
16618
- return c.json({ error: "Worker user not found \u2014 run: semiont useradd --email worker@semiont.local --generate-password --upsert" }, 503);
16619
- }
16713
+ const did = agentToDid({ domain: siteDomain, provider: inferenceProvider, model });
16620
16714
  const token = JWTService.generateToken({
16621
- userId: userId(workerUser.id),
16622
- email: email(workerUser.email),
16623
- name: workerUser.name ?? "Worker Pool",
16624
- domain: workerUser.domain,
16625
- provider: workerUser.provider,
16626
- isAdmin: false
16715
+ userId: userId(agentUser.id),
16716
+ email: email(agentUser.email),
16717
+ name: agentUser.name ?? agentName,
16718
+ domain: agentUser.domain,
16719
+ provider: agentUser.provider,
16720
+ isAdmin: false,
16721
+ agentDid: did
16627
16722
  }, "24h");
16628
- return c.json({ token }, 200);
16723
+ return c.json({ token, did }, 200);
16629
16724
  });
16630
16725
  authRouter.post("/api/tokens/media", authMiddleware, async (c) => {
16631
16726
  const user = c.get("user");
@@ -17209,7 +17304,8 @@ function deriveStorageUri(name, format) {
17209
17304
  function registerCreateResource(router) {
17210
17305
  router.post("/resources", async (c) => {
17211
17306
  const user = c.get("user");
17212
- if (!user) {
17307
+ const principalDid = c.get("principalDid");
17308
+ if (!user || !principalDid) {
17213
17309
  throw new HTTPException(401, { message: "Authentication required" });
17214
17310
  }
17215
17311
  const formData = await c.req.formData();
@@ -17218,7 +17314,6 @@ function registerCreateResource(router) {
17218
17314
  const formatRaw = formData.get("format");
17219
17315
  const language = formData.get("language");
17220
17316
  const entityTypesStr = formData.get("entityTypes");
17221
- const creationMethod = formData.get("creationMethod");
17222
17317
  const storageUri = formData.get("storageUri");
17223
17318
  const sourceAnnotationId = formData.get("sourceAnnotationId");
17224
17319
  const sourceResourceId = formData.get("sourceResourceId");
@@ -17266,13 +17361,12 @@ function registerCreateResource(router) {
17266
17361
  format,
17267
17362
  language: language || void 0,
17268
17363
  entityTypes,
17269
- creationMethod: creationMethod || void 0,
17270
17364
  generatedFrom,
17271
17365
  generationPrompt: generationPrompt || void 0,
17272
17366
  generator,
17273
17367
  isDraft: isDraftStr ? isDraftStr === "true" : void 0
17274
17368
  },
17275
- userId(userToDid(user)),
17369
+ userId(principalDid),
17276
17370
  eventBus2
17277
17371
  );
17278
17372
  },
@@ -17611,9 +17705,9 @@ function createBusRouter(authMiddleware2) {
17611
17705
  throw new HTTPException(400, { message: `Invalid payload for ${channel}: ${errorMessage}` });
17612
17706
  }
17613
17707
  }
17614
- const user = c.get("user");
17615
- if (user) {
17616
- payload._userId = userToDid(user);
17708
+ const principalDid = c.get("principalDid");
17709
+ if (principalDid) {
17710
+ payload._userId = principalDid;
17617
17711
  }
17618
17712
  const traceparent = c.req.header("traceparent");
17619
17713
  const tracestate = c.req.header("tracestate");