@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.get("info") or {}).get("fieldName") in proposal_rules.MUTATIONS:
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 "arguments" in event and "info" in event:
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.get("info") or {}).get("fieldName")
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) => signInFailedRedirect(request, options, pendingSignIn?.returnPath ?? null, code);
173
- if (!pendingSignIn) return failed("sign_in_expired");
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
- console.error(`Prospectus sign-in failed (${error instanceof Error ? error.name : "error"}).`);
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
@@ -448,7 +448,7 @@
448
448
  "optional": true
449
449
  }
450
450
  },
451
- "version": "2.0.0-next.82",
451
+ "version": "2.0.0-next.83",
452
452
  "dependencies": {
453
453
  "@aws-amplify/adapter-nextjs": "^1.7.3",
454
454
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.0.0-next.82",
2
+ "version": "2.0.0-next.83",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",