///
/**
* TRUSTED FILE - Google Docs Comment Confirmation Handlers
*
* NOTE: This is a utility module, not a standalone pattern.
* It is imported by google-docs-comment-orchestrator.tsx.
*
* This file contains the trusted handlers and API client for Google Docs
* comment actions. The actual side effects (API calls) happen here.
*
* TRUST BOUNDARY: User clicking a button that invokes executeAction
* creates official approval for the side effect.
*
* Future trust policies can grant trust to this file independently
* to assert "this action was user-approved".
*/
import { Default, handler, Writable } from "commontools";
// =============================================================================
// Types - Exported for orchestrator
// =============================================================================
type CFC = T;
type Secret = CFC;
export type Auth = {
token: Default, "">;
tokenType: Default;
scope: Default;
expiresIn: Default;
expiresAt: Default;
refreshToken: Default, "">;
user: Default<{
email: string;
name: string;
picture: string;
}, { email: ""; name: ""; picture: "" }>;
};
export interface GoogleComment {
id: string;
author: { displayName: string; photoLink?: string; emailAddress?: string };
content: string;
htmlContent?: string;
createdTime: string;
modifiedTime?: string;
resolved: boolean;
quotedFileContent?: { value: string; mimeType?: string };
anchor?: string;
replies?: Array<{
id: string;
author: { displayName: string; photoLink?: string; emailAddress?: string };
content: string;
createdTime: string;
action?: "resolve" | "reopen";
}>;
}
export interface CommentState {
regenerateNonce: number;
status: "pending" | "generating" | "ready" | "accepted" | "skipped";
}
// =============================================================================
// Types - Pending Action
// =============================================================================
export interface PendingCommentAction {
type: "reply" | "reply-resolve";
docUrl: string;
fileId: string;
// Comment context
commentId: string;
commentAuthor: string;
commentContent: string;
quotedText?: string;
// Action content
responseText: string;
}
// =============================================================================
// API Client (TRUST BOUNDARY - API calls happen here)
// =============================================================================
class GoogleDocsClient {
private token: string;
constructor(token: string) {
this.token = token;
}
async createReply(
fileId: string,
commentId: string,
content: string,
resolve = false,
): Promise {
const url = new URL(
`https://www.googleapis.com/drive/v3/files/${fileId}/comments/${commentId}/replies`,
);
url.searchParams.set("fields", "id,content,action");
const body: { content: string; action?: string } = { content };
if (resolve) {
body.action = "resolve";
}
const res = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) {
const text = await res.text();
if (res.status === 401) {
throw new Error(
"Token expired. Please re-authenticate in your Google Auth piece.",
);
}
if (res.status === 403) {
throw new Error(
"Access denied. You may not have permission to comment on this document.",
);
}
throw new Error(`Failed to post reply: ${res.status} - ${text}`);
}
}
}
// =============================================================================
// Handlers (TRUST BOUNDARY - exported for use by orchestrator)
// =============================================================================
/**
* Execute the confirmed action - THIS IS THE TRUST BOUNDARY
*
* User clicking the "Post Reply" button invokes this handler,
* which executes the API call. This is the trusted side effect.
*/
export const executeAction = handler<
unknown,
{
action: Writable;
// deno-lint-ignore no-explicit-any
auth: any; // Accepts OpaqueCell or Cell from wish()
comments: Writable;
commentStates: Writable>;
expandedCommentId: Writable;
lastError: Writable;
isExecuting: Writable;
}
>(async (_, {
action,
auth,
comments,
commentStates,
expandedCommentId,
lastError,
isExecuting,
}) => {
const pendingAction = action.get();
if (!pendingAction) {
lastError.set("No action to execute");
return;
}
const token = auth?.token ?? auth?.get?.()?.token;
if (!token) {
lastError.set("Please authenticate with Google first");
return;
}
isExecuting.set(true);
lastError.set(null);
try {
const client = new GoogleDocsClient(token);
const resolve = pendingAction.type === "reply-resolve";
await client.createReply(
pendingAction.fileId,
pendingAction.commentId,
pendingAction.responseText,
resolve,
);
// Update local state - mark as accepted
const currentStates = commentStates.get() ?? {};
commentStates.set({
...currentStates,
[pendingAction.commentId]: {
...(currentStates[pendingAction.commentId] ?? { regenerateNonce: 0 }),
status: "accepted",
},
});
// If resolved, remove from comments list
if (resolve) {
const currentComments = comments.get() ?? [];
comments.set(
currentComments.filter((c) => c.id !== pendingAction.commentId),
);
}
// Collapse the comment and clear the action
expandedCommentId.set(null);
action.set(null);
} catch (e: unknown) {
console.error("[executeAction] Error:", e);
const errorMessage = e instanceof Error
? e.message
: "Failed to post reply";
lastError.set(errorMessage);
} finally {
isExecuting.set(false);
}
});
/**
* Cancel the pending action
*/
export const cancelAction = handler<
unknown,
{ action: Writable }
>((_, { action }) => {
action.set(null);
});