@opengeni/api-router 4.3.3-canary.39786771332001 → 4.5.0

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.
@@ -5,6 +5,7 @@ import {
5
5
  PublishWorkspaceArtifactVersionRequest,
6
6
  RollbackWorkspaceArtifactRequest,
7
7
  SetWorkspaceArtifactStatusRequest,
8
+ SiteOpenedResponse,
8
9
  UpdateWorkspaceArtifactVisibilityRequest,
9
10
  UpdateWorkspaceArtifactVisibilityResponse,
10
11
  WORKSPACE_ARTIFACT_TITLE_MAX_CHARS,
@@ -24,6 +25,7 @@ import {
24
25
  resolveSiteAudienceChange,
25
26
  SessionAuthorizationDeniedError,
26
27
  SessionAuthorizationUnavailableError,
28
+ currentSiteSessionOrigin,
27
29
  siteViewerForGrant,
28
30
  type ApiRouteDeps,
29
31
  } from "@opengeni/core";
@@ -33,6 +35,7 @@ import {
33
35
  getWorkspaceArtifact,
34
36
  getWorkspaceArtifactContentRef,
35
37
  listWorkspaceArtifacts,
38
+ recordSiteOpened,
36
39
  nestedPostgresSqlState,
37
40
  publishWorkspaceArtifactVersion,
38
41
  rollbackWorkspaceArtifact,
@@ -232,6 +235,33 @@ export function registerWorkspaceArtifactRoutes(app: Hono, deps: ApiRouteDeps):
232
235
  }
233
236
  });
234
237
 
238
+ // Opening a Site counts as this person's activity on its sidebar row. It is
239
+ // personal, needs only read access, and a Site page cannot record it.
240
+ app.post(`${base}/:artifactId/opened`, async (context) => {
241
+ const workspaceId = context.req.param("workspaceId");
242
+ const grant = await requireAccessGrant(context, deps, workspaceId, "artifacts:read");
243
+ if (currentSiteSessionOrigin())
244
+ throw new HTTPException(403, { message: "A Site page cannot record opening a Site" });
245
+ const siteId = artifactId(context);
246
+ const viewer = await siteViewerForGrant(deps.db, grant);
247
+ if (!viewer.subjectId)
248
+ throw new HTTPException(403, { message: "A signed-in person is required" });
249
+ try {
250
+ return context.json(
251
+ SiteOpenedResponse.parse(
252
+ await recordSiteOpened(deps.db, {
253
+ workspaceId,
254
+ subjectId: viewer.subjectId,
255
+ viewer,
256
+ siteId,
257
+ }),
258
+ ),
259
+ );
260
+ } catch (error) {
261
+ return workspaceArtifactErrorResponse(context, error);
262
+ }
263
+ });
264
+
235
265
  app.get(base, async (context) => {
236
266
  const workspaceId = context.req.param("workspaceId");
237
267
  const grant = await requireAccessGrant(context, deps, workspaceId, "artifacts:read");
@@ -461,7 +461,9 @@ export function sandboxArtifactRelativePath(value: string, workspaceRoot = "/wor
461
461
  }
462
462
  if (!relative) {
463
463
  throw new HTTPException(400, {
464
- message: "sandbox artifact path must name a file inside the active workspace root",
464
+ // Say which root: on a connected machine the file may exist outside it,
465
+ // and copying it in is the way forward.
466
+ message: `sandbox artifact path must name a file inside this session's workspace root ${workspaceRoot}; ${path} is outside it, so copy the file into the workspace first and use that path`,
465
467
  });
466
468
  }
467
469
  if (absolute.length > SANDBOX_ARTIFACT_ABSOLUTE_PATH_MAX_CHARS) {
@@ -1,33 +0,0 @@
1
- /** The one refusal a card can resolve: the owner just has not allowed this chat. */
2
- export declare const MACHINE_PERMISSION_REQUIRED_CODE = "personal_machine_permission_required";
3
- /**
4
- * What the agent reads after the card was posted for it. The card is posted in
5
- * this chat's Opengeni timeline; the person may instead be reading the chat in
6
- * Slack, or this may be a worker or scheduled chat, so the text names the chat
7
- * link rather than promising the card is in front of them. Choosing the
8
- * machine on the card sends a message that continues the chat, so the agent
9
- * has nothing to wait for and nothing to retry.
10
- */
11
- export declare function machinePermissionCardShownReason(chatUrl: string | null): string;
12
- type MachineRefusal = {
13
- code?: string | undefined;
14
- reason?: string | undefined;
15
- };
16
- export type MachinePermissionCardPost = () => Promise<{
17
- eventId: string | null;
18
- chatUrl: string | null;
19
- } | null>;
20
- /**
21
- * Post the Connected Machine card when a swap or one-off command was refused
22
- * only because the machine's owner has not allowed this chat yet, and tell the
23
- * agent it is posted. Every other result is returned unchanged. Posting is best
24
- * effort: if it cannot be posted, the original refusal still stands.
25
- */
26
- export declare function withMachinePermissionCard<T extends MachineRefusal>(result: T, post: MachinePermissionCardPost): Promise<T & {
27
- card?: "shown";
28
- cardEventId?: string | null;
29
- chatUrl?: string;
30
- }>;
31
- /** The Opengeni console link for a chat, or null when no safe web base is configured. */
32
- export declare function sessionChatUrl(base: string | null | undefined, workspaceId: string, sessionId: string): string | null;
33
- export {};
@@ -1,78 +0,0 @@
1
- /** The one refusal a card can resolve: the owner just has not allowed this chat. */
2
- export const MACHINE_PERMISSION_REQUIRED_CODE = "personal_machine_permission_required";
3
-
4
- /**
5
- * What the agent reads after the card was posted for it. The card is posted in
6
- * this chat's Opengeni timeline; the person may instead be reading the chat in
7
- * Slack, or this may be a worker or scheduled chat, so the text names the chat
8
- * link rather than promising the card is in front of them. Choosing the
9
- * machine on the card sends a message that continues the chat, so the agent
10
- * has nothing to wait for and nothing to retry.
11
- */
12
- export function machinePermissionCardShownReason(chatUrl: string | null): string {
13
- const elsewhere = chatUrl
14
- ? `If they are reading this chat somewhere else (for example Slack, or this is a worker or scheduled chat), give them this link to allow it: ${chatUrl}. `
15
- : "If they are reading this chat somewhere else (for example Slack, or this is a worker or scheduled chat), tell them to open this chat in Opengeni to allow it. ";
16
- return (
17
- "This is a personal (Only me) machine, and its owner has not allowed this chat to use it yet. " +
18
- "A card where the owner can allow it is now posted in this chat in Opengeni. " +
19
- "If they are reading this chat in Opengeni, tell them in one short sentence that they can allow it there; do not describe buttons or steps. " +
20
- elsewhere +
21
- "Do not wait for it and do not retry: when they allow it, their message continues this chat."
22
- );
23
- }
24
-
25
- type MachineRefusal = { code?: string | undefined; reason?: string | undefined };
26
-
27
- export type MachinePermissionCardPost = () => Promise<{
28
- eventId: string | null;
29
- chatUrl: string | null;
30
- } | null>;
31
-
32
- /**
33
- * Post the Connected Machine card when a swap or one-off command was refused
34
- * only because the machine's owner has not allowed this chat yet, and tell the
35
- * agent it is posted. Every other result is returned unchanged. Posting is best
36
- * effort: if it cannot be posted, the original refusal still stands.
37
- */
38
- export async function withMachinePermissionCard<T extends MachineRefusal>(
39
- result: T,
40
- post: MachinePermissionCardPost,
41
- ): Promise<T & { card?: "shown"; cardEventId?: string | null; chatUrl?: string }> {
42
- if (result.code !== MACHINE_PERMISSION_REQUIRED_CODE) return result;
43
- let posted: Awaited<ReturnType<MachinePermissionCardPost>>;
44
- try {
45
- posted = await post();
46
- } catch {
47
- return result;
48
- }
49
- if (!posted) return result;
50
- return {
51
- ...result,
52
- reason: machinePermissionCardShownReason(posted.chatUrl),
53
- card: "shown",
54
- cardEventId: posted.eventId,
55
- ...(posted.chatUrl ? { chatUrl: posted.chatUrl } : {}),
56
- };
57
- }
58
-
59
- /** The Opengeni console link for a chat, or null when no safe web base is configured. */
60
- export function sessionChatUrl(
61
- base: string | null | undefined,
62
- workspaceId: string,
63
- sessionId: string,
64
- ): string | null {
65
- if (!base) return null;
66
- try {
67
- const url = new URL(
68
- `/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}`,
69
- base,
70
- );
71
- if ((url.protocol !== "https:" && url.protocol !== "http:") || url.username || url.password) {
72
- return null;
73
- }
74
- return url.toString();
75
- } catch {
76
- return null;
77
- }
78
- }