@anthusai/papyrus 2.0.0-next.82 → 2.0.0-next.83
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.
|
@@ -1501,7 +1501,7 @@ def run_ai_worker(
|
|
|
1501
1501
|
|
|
1502
1502
|
def handle_staff_event(event: dict[str, Any], *, store: Any, settings: ReaderSettings, now_seconds: float, account_id: str | None = None) -> dict[str, Any]:
|
|
1503
1503
|
"""``moderateReader`` from AppSync: an editor or admin hides, approves, promotes, bans or undoes. Answers a receipt, never an exception."""
|
|
1504
|
-
if (event
|
|
1504
|
+
if proposal_rules.event_field_name(event) in proposal_rules.MUTATIONS:
|
|
1505
1505
|
return proposal_rules.handle_proposal_event(event, store, now_iso=iso(now_seconds), account_id=account_id)
|
|
1506
1506
|
identity = event.get("identity") or {}
|
|
1507
1507
|
claims = identity.get("claims") or {}
|
|
@@ -1539,7 +1539,7 @@ def handler(event: dict[str, Any], _context: Any = None) -> dict[str, Any]:
|
|
|
1539
1539
|
settings = settings_from_environment()
|
|
1540
1540
|
store = store_from_environment()
|
|
1541
1541
|
now_seconds = time.time()
|
|
1542
|
-
if
|
|
1542
|
+
if proposal_rules.is_appsync_event(event):
|
|
1543
1543
|
return handle_staff_event(event, store=store, settings=settings, now_seconds=now_seconds, account_id=function_account(_context))
|
|
1544
1544
|
target = snapshot_target_from_environment()
|
|
1545
1545
|
if settings.moderation_mode == "ai" and target is not None and _context is not None:
|
|
@@ -57,6 +57,22 @@ TARGET_COUNTS: dict[str, tuple[int, int]] = {
|
|
|
57
57
|
TARGET_TYPES = ("concept", "entity", "column", "story", "cluster")
|
|
58
58
|
|
|
59
59
|
MUTATIONS = ("fileProposal", "transitionProposal")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def event_field_name(event: dict[str, Any]) -> str | None:
|
|
63
|
+
"""The GraphQL field an AppSync invocation is for.
|
|
64
|
+
|
|
65
|
+
The Amplify Gen 2 Lambda data source sends ``fieldName`` at the top level with no ``info`` key; a direct resolver nests it
|
|
66
|
+
under ``info``. Accept both.
|
|
67
|
+
"""
|
|
68
|
+
info = event.get("info")
|
|
69
|
+
name = (info.get("fieldName") if isinstance(info, dict) else None) or event.get("fieldName")
|
|
70
|
+
return name if isinstance(name, str) and name else None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_appsync_event(event: dict[str, Any]) -> bool:
|
|
74
|
+
"""True for an AppSync invocation (arguments plus ``info`` or a top-level ``fieldName``); the public path sends siteSubject, action and payload."""
|
|
75
|
+
return "arguments" in event and ("info" in event or event_field_name(event) is not None)
|
|
60
76
|
PROPOSER_AI = "ai"
|
|
61
77
|
STAFF_GROUPS = frozenset({"editor", "admin"})
|
|
62
78
|
#: The only moves an operator's IAM role may make, as (from, to). Everything after the draft is an editor's decision.
|
|
@@ -282,7 +298,7 @@ def handle_proposal_event(event: dict[str, Any], store: ProposalStore, *, now_is
|
|
|
282
298
|
if actor is None:
|
|
283
299
|
return refused("not_staff", "Only editors, admins and operators of this account can file proposals; only editors and admins decide them.").to_dict()
|
|
284
300
|
arguments = event.get("arguments") or {}
|
|
285
|
-
field = (event
|
|
301
|
+
field = event_field_name(event)
|
|
286
302
|
if field == "fileProposal":
|
|
287
303
|
row, problem = file_proposal(arguments, actor_sub=actor, now_iso=now_iso)
|
|
288
304
|
if problem is not None:
|
|
@@ -72,6 +72,10 @@ export type AutomationRecord = {
|
|
|
72
72
|
enabled: boolean;
|
|
73
73
|
enabledNote: string;
|
|
74
74
|
trigger: { kind: "monitor" | "schedule" | "event" | "manual"; config: Record<string, unknown> };
|
|
75
|
+
/** Story-stage automations only: the stage that fires it, where the engine moves the story after, and the bot-hop limit. */
|
|
76
|
+
onStage?: string | null;
|
|
77
|
+
nextStage?: string | null;
|
|
78
|
+
maxHops?: number | null;
|
|
75
79
|
supported: boolean;
|
|
76
80
|
supportNote: string | null;
|
|
77
81
|
procedureKey: string;
|
|
@@ -106,6 +110,10 @@ export function triggerText(trigger: AutomationRecord["trigger"]): string {
|
|
|
106
110
|
if (trigger.kind === "monitor") {
|
|
107
111
|
return `${kind} ${String(trigger.config.monitorId ?? "?")}, delivering to ${String(trigger.config.publicationId ?? "?")}`;
|
|
108
112
|
}
|
|
113
|
+
if (trigger.kind === "event" && trigger.config.event === "story.stage_entered") {
|
|
114
|
+
const next = trigger.config.nextStage ? `, then moves it to ${String(trigger.config.nextStage)}` : "";
|
|
115
|
+
return `${kind}: a story enters ${String(trigger.config.onStage ?? "?")}${next}`;
|
|
116
|
+
}
|
|
109
117
|
return kind;
|
|
110
118
|
}
|
|
111
119
|
|
|
@@ -41,6 +41,8 @@ export interface ProspectusSignInRouteOptions {
|
|
|
41
41
|
nowInSeconds?: () => number;
|
|
42
42
|
/** Trusted configuration the public site origin is read from; defaults to process.env. */
|
|
43
43
|
environment?: Record<string, string | undefined>;
|
|
44
|
+
/** Receives each sign-in failure line; defaults to console.error so it reaches the compute log. */
|
|
45
|
+
log?: (line: string) => void;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
const NO_STORE_HEADERS = { "cache-control": "no-store" };
|
|
@@ -91,6 +93,21 @@ function signInFailedResponse(status: number, code: string, message: string): Ne
|
|
|
91
93
|
* A failed sign-in never leaves the browser on /api/auth/prospectus/callback: it answers 303 to the page the reader started from
|
|
92
94
|
* (or the home page when that is not known) with `sign_in_error=<code>`, which the reader plugin turns into a short message.
|
|
93
95
|
*/
|
|
96
|
+
/** Which callback step failed; with the code it tells an operator where a reader's sign-in stopped. */
|
|
97
|
+
type SignInFailureBranch = "pending_cookie" | "provider_error" | "missing_parameters" | "completion" | "unexpected";
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* One structured line per failed callback: `prospectus_sign_in_failed {"code":...,"branch":...}`. It carries only the
|
|
101
|
+
* code, the branch, and (for errors from completing the sign-in) the HTTP status, Prospectus error code and error name.
|
|
102
|
+
* Never tokens, authorization codes, state, emails or subjects.
|
|
103
|
+
*/
|
|
104
|
+
function logSignInFailure(
|
|
105
|
+
options: ProspectusSignInRouteOptions,
|
|
106
|
+
entry: { code: string; branch: SignInFailureBranch; httpStatus?: number; prospectusError?: string; errorName?: string },
|
|
107
|
+
): void {
|
|
108
|
+
(options.log ?? console.error)(`prospectus_sign_in_failed ${JSON.stringify(entry)}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
94
111
|
function signInFailedRedirect(request: NextRequest, options: ProspectusSignInRouteOptions, returnPath: string | null, code: string): NextResponse {
|
|
95
112
|
const target = new URL(returnPath ?? "/", siteOriginFor(request, options));
|
|
96
113
|
target.searchParams.set("sign_in_error", /^[a-z0-9_]{1,60}$/.test(code) ? code : "sign_in_failed");
|
|
@@ -169,13 +186,16 @@ async function completeCallback(
|
|
|
169
186
|
options: ProspectusSignInRouteOptions,
|
|
170
187
|
nowInSeconds: number,
|
|
171
188
|
): Promise<NextResponse> {
|
|
172
|
-
const failed = (code: string
|
|
173
|
-
|
|
189
|
+
const failed = (code: string, branch: SignInFailureBranch, detail: { httpStatus?: number; prospectusError?: string; errorName?: string } = {}) => {
|
|
190
|
+
logSignInFailure(options, { code, branch, ...detail });
|
|
191
|
+
return signInFailedRedirect(request, options, pendingSignIn?.returnPath ?? null, code);
|
|
192
|
+
};
|
|
193
|
+
if (!pendingSignIn) return failed("sign_in_expired", "pending_cookie");
|
|
174
194
|
const searchParams = request.nextUrl.searchParams;
|
|
175
|
-
if (searchParams.get("error")) return failed("sign_in_declined");
|
|
195
|
+
if (searchParams.get("error")) return failed("sign_in_declined", "provider_error");
|
|
176
196
|
const authorizationCode = searchParams.get("code");
|
|
177
197
|
const returnedState = searchParams.get("state");
|
|
178
|
-
if (!authorizationCode || !returnedState) return failed("sign_in_incomplete");
|
|
198
|
+
if (!authorizationCode || !returnedState) return failed("sign_in_incomplete", "missing_parameters");
|
|
179
199
|
try {
|
|
180
200
|
const signedIn = await completeProspectusGoogleSignIn({
|
|
181
201
|
accounts: runtime.accounts,
|
|
@@ -200,9 +220,8 @@ async function completeCallback(
|
|
|
200
220
|
);
|
|
201
221
|
return response;
|
|
202
222
|
} catch (error) {
|
|
203
|
-
if (error instanceof ProspectusAccountsError) return failed(error.code);
|
|
204
|
-
|
|
205
|
-
return failed("sign_in_failed");
|
|
223
|
+
if (error instanceof ProspectusAccountsError) return failed(error.code, "completion", { httpStatus: error.status, prospectusError: error.code });
|
|
224
|
+
return failed("sign_in_failed", "unexpected", { errorName: error instanceof Error ? error.name : "error" });
|
|
206
225
|
}
|
|
207
226
|
}
|
|
208
227
|
|
package/package.json
CHANGED
package/routes.manifest.json
CHANGED