@agent-native/core-corpus 0.210.2 → 0.211.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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +41 -2
- package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +4 -2
- package/corpus/templates/analytics/CHANGELOG.md +18 -0
- package/corpus/templates/analytics/actions/build-data-index.ts +11 -2
- package/corpus/templates/analytics/actions/configure-content-collection.ts +29 -4
- package/corpus/templates/analytics/actions/configure-first-party-events-model.ts +9 -7
- package/corpus/templates/analytics/actions/create-analytics-public-key.ts +4 -0
- package/corpus/templates/analytics/actions/estimate-content-signups-cost.ts +72 -0
- package/corpus/templates/analytics/actions/estimate-query-cost.ts +16 -134
- package/corpus/templates/analytics/actions/list-sigma-workbooks.ts +123 -42
- package/corpus/templates/analytics/actions/migrate-sigma-workbook.ts +20 -17
- package/corpus/templates/analytics/actions/revoke-analytics-public-key.ts +4 -0
- package/corpus/templates/analytics/app/components/dashboard/SigmaMigrationDialog.tsx +246 -680
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/analytics/app/i18n/en-US.ts +54 -55
- package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +0 -1
- package/corpus/templates/analytics/app/pages/DataSources.tsx +56 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +4 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +26 -8
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +3 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/panel-cost-notice.ts +172 -0
- package/corpus/templates/analytics/app/pages/data-sources/SearchConsoleDetail.tsx +441 -0
- package/corpus/templates/analytics/changelog/2026-10-15-a-content-dashboard-lists-every-blog-article-with-its-author.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-15-data-sources-has-a-google-search-console-card-that-shows-the.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-the-content-dashboard-can-show-signups-per-blog-article.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-19-creating-or-revoking-a-public-analytics-key-now-asks-you-to-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-19-search-sigma-workbooks-by-name-and-open-ones-a-teammate-alre.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-blog-signup-attribution-excludes-signups-before-the-first-vi.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-collapsed-analytics-navigation-starts-at-the-top.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-content-keeps-successful-coverage-dates-and-missing-day-gaps.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-private-github-repositories-can-be-indexed-with-your-connect.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-search-console-preserves-backfill-and-paused-collection.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-search-console-refreshes-historical-coverage-when-properties.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-search-console-setup-keeps-saved-properties-readable-and-sup.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/content-view.json +182 -0
- package/corpus/templates/analytics/server/db/schema.ts +1 -0
- package/corpus/templates/analytics/server/lib/bi-migration/external-dashboards-store.ts +56 -18
- package/corpus/templates/analytics/server/lib/bi-migration/unfinished-import.ts +64 -0
- package/corpus/templates/analytics/server/lib/builtin-dashboard-pass.ts +17 -2
- package/corpus/templates/analytics/server/lib/content-metrics/collect.ts +2 -0
- package/corpus/templates/analytics/server/lib/content-metrics/config.ts +92 -3
- package/corpus/templates/analytics/server/lib/content-metrics/legacy-content-view.json +172 -0
- package/corpus/templates/analytics/server/lib/content-metrics/migration.ts +2 -0
- package/corpus/templates/analytics/server/lib/content-metrics/steps/daily-metrics.ts +8 -3
- package/corpus/templates/analytics/server/lib/content-metrics/steps/search-console.ts +16 -3
- package/corpus/templates/analytics/server/lib/content-metrics/steps/signups.ts +240 -0
- package/corpus/templates/analytics/server/lib/content-metrics/store.ts +5 -0
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +21 -0
- package/corpus/templates/analytics/server/lib/dashboard-panel-verification.ts +300 -5
- package/corpus/templates/analytics/server/lib/dashboard-seeds.ts +2 -0
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +28 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +128 -24
- package/corpus/templates/analytics/server/lib/google-analytics.ts +56 -16
- package/corpus/templates/analytics/server/lib/index-build-github.ts +5 -0
- package/corpus/templates/analytics/server/lib/provider-credentials.ts +47 -2
- package/corpus/templates/analytics/server/lib/query-cost-estimate.ts +184 -5
- package/corpus/templates/analytics/server/plugins/db.ts +9 -1
- package/corpus/templates/analytics/shared/builtin-dashboard-ids.ts +2 -0
- package/corpus/templates/analytics/shared/search-console-property.ts +78 -0
- package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/calendar/actions/delete-event.ts +1 -3
- package/corpus/templates/calendar/actions/delete-events.ts +1 -2
- package/corpus/templates/calendar/actions/event-action-helpers.ts +0 -4
- package/corpus/templates/calendar/actions/update-event.ts +2 -6
- package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/clips/CHANGELOG.md +11 -24
- package/corpus/templates/clips/actions/create-recording-upload.ts +5 -1
- package/corpus/templates/clips/app/components/player/chapter-list.tsx +7 -6
- package/corpus/templates/clips/app/components/player/comment-composer.tsx +5 -1
- package/corpus/templates/clips/app/components/player/player-controls.tsx +1 -0
- package/corpus/templates/clips/app/components/player/scrubber.tsx +4 -2
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
- package/corpus/templates/clips/app/lib/extension-auth.ts +70 -0
- package/corpus/templates/clips/app/root.tsx +22 -10
- package/corpus/templates/clips/app/routes/_app.notifications.tsx +3 -0
- package/corpus/templates/clips/app/routes/import.tsx +115 -110
- package/corpus/templates/clips/changelog/2026-10-16-agent-uploads-can-use-free-clips-storage.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-agents-see-a-clip-s-frames-as-soon-as-it-finishes-processing.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-chapter-list-times-now-match-the-player-s-clock-and-each-cha.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-editing-a-comment-puts-the-cursor-after-its-text-and-reply-o.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-youtube-import-followups.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-20-locally-saved-desktop-recordings-now-appear-in-the-recovery-.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +29 -186
- package/corpus/templates/clips/desktop/src/components/RecordingRecovery.tsx +171 -16
- package/corpus/templates/clips/desktop/src/lib/record-first.ts +9 -32
- package/corpus/templates/clips/desktop/src/lib/recording-recovery.ts +30 -1
- package/corpus/templates/clips/server/lib/agent-frame-request.ts +5 -2
- package/corpus/templates/clips/server/lib/recording-upload-ticket.ts +8 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +2 -0
- package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/content/CHANGELOG.md +12 -0
- package/corpus/templates/content/actions/_content-database-personal-view.ts +5 -15
- package/corpus/templates/content/actions/_database-navigation.ts +3 -3
- package/corpus/templates/content/actions/_database-utils.ts +10 -22
- package/corpus/templates/content/actions/_database-view-setup.ts +16 -76
- package/corpus/templates/content/actions/create-comment-ai-suggestion.ts +93 -44
- package/corpus/templates/content/actions/export-document.ts +2 -14
- package/corpus/templates/content/actions/suggest-document-edit.ts +5 -7
- package/corpus/templates/content/actions/update-content-database-view.ts +2 -14
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +86 -30
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +64 -28
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -46
- package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +72 -7
- package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +292 -103
- package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +147 -40
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +108 -11
- package/corpus/templates/content/app/components/editor/database/ContentTable.tsx +2 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +150 -166
- package/corpus/templates/content/app/components/editor/database/database-cell-comments.tsx +515 -0
- package/corpus/templates/content/app/components/editor/database/database-item-comments.tsx +5 -43
- package/corpus/templates/content/app/components/editor/database/filter-sort.ts +8 -783
- package/corpus/templates/content/app/components/editor/live-editor-session.ts +5 -1
- package/corpus/templates/content/app/components/editor/page-draft-journal.ts +61 -1
- package/corpus/templates/content/app/components/editor/property-comments.tsx +35 -4
- package/corpus/templates/content/app/components/editor/property-date-format.ts +45 -0
- package/corpus/templates/content/app/components/editor/thread-suggestions.ts +30 -0
- package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +23 -4
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +36 -10
- package/corpus/templates/content/app/global.css +23 -0
- package/corpus/templates/content/app/i18n-data.ts +1 -0
- package/corpus/templates/content/changelog/2026-10-16-opening-a-page-in-a-new-tab-while-another-tab-is-still-savin.md +6 -0
- package/corpus/templates/content/changelog/2026-10-16-typing-steadily-in-one-tab-no-longer-makes-another-open-tab-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-database-views-can-filter-the-same-property-more-than-once-n.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-edits-made-from-a-block-s-menu-or-a-table-s-row-and-column-m.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-starting-a-comment-puts-the-cursor-in-the-comment-box-ready-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-table-row-and-column-options-now-appear-for-the-row-or-colum.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +5 -1
- package/corpus/templates/content/docs/product/capabilities/content.revision.suggestions.md +3 -1
- package/corpus/templates/content/e2e/helpers.ts +10 -0
- package/corpus/templates/content/shared/api.ts +18 -12
- package/corpus/templates/content/shared/database-query.ts +107 -13
- package/corpus/templates/content/shared/suggestion-diff.ts +35 -0
- package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/design/CHANGELOG.md +2 -0
- package/corpus/templates/design/actions/delete-file.ts +1 -4
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +10 -1
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +24 -4
- package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +14 -9
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +23 -2
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +29 -0
- package/corpus/templates/design/changelog/2026-10-19-reply-and-edit-on-a-comment-in-the-sidebar-or-on-a-canvas-pi.md +6 -0
- package/corpus/templates/design/changelog/2026-10-20-comments-return-to-the-mode-they-came-from-and-layers-search.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +185 -85
- package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/actions/apply-workspace-connection-setup.ts +4 -0
- package/corpus/templates/dispatch/actions/delete-workspace-connection.ts +4 -0
- package/corpus/templates/dispatch/actions/set-workspace-connection-grant.ts +4 -0
- package/corpus/templates/dispatch/actions/upsert-workspace-connection.ts +4 -0
- package/corpus/templates/dispatch/changelog/2026-10-19-vault-secrets-grants-link-tokens-app-access-and-workspace-co.md +6 -0
- package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
- package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/mail/.agents/skills/contacts-and-crm/SKILL.md +22 -17
- package/corpus/templates/mail/AGENTS.md +1 -1
- package/corpus/templates/mail/CHANGELOG.md +14 -0
- package/corpus/templates/mail/actions/provider-api-request.ts +3 -3
- package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +211 -54
- package/corpus/templates/mail/app/hooks/use-inbox-threads.ts +26 -2
- package/corpus/templates/mail/app/i18n/en-US.ts +5 -1
- package/corpus/templates/mail/app/lib/gong-connection.ts +27 -0
- package/corpus/templates/mail/app/lib/thread-cache.ts +110 -9
- package/corpus/templates/mail/app/root.tsx +10 -2
- package/corpus/templates/mail/changelog/2026-10-20-connected-gong-settings-now-open-the-shared-integration-mana.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-gong-connection-details-now-match-the-call-data-available-in.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-improve-gong-setup-with-a-click-to-connect-flow-and-permissi.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-mail-lets-you-retry-when-it-cannot-check-your-gong-access.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-mail-thread-cache-stays-isolated-when-mailbox-scope-changes.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-the-inbox-and-recently-opened-threads-now-open-instantly-fro.md +6 -0
- package/corpus/templates/mail/server/handlers/emails.ts +9 -0
- package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/plan/CHANGELOG.md +4 -0
- package/corpus/templates/plan/app/pages/PlansPage.tsx +173 -33
- package/corpus/templates/plan/changelog/2026-10-19-opening-a-comment-thread-puts-the-cursor-in-the-reply-box-an.md +6 -0
- package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -42
- package/corpus/templates/slides/app/components/editor/FloatingToolbar.tsx +3 -7
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +5 -14
- package/corpus/templates/slides/app/i18n/en-US.ts +0 -2
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +27 -33
- package/corpus/templates/slides/scripts/edit-fidelity/run.ts +1 -3
- package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +5 -3
- package/package.json +1 -1
|
@@ -66,30 +66,70 @@ export async function readServiceAccountIdentity(): Promise<ServiceAccountIdenti
|
|
|
66
66
|
throw error;
|
|
67
67
|
}
|
|
68
68
|
try {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
return typeof email === "string" && email
|
|
72
|
-
? { status: "ok", email }
|
|
73
|
-
: { status: "invalid" };
|
|
69
|
+
const creds = parseServiceAccountKey(credsJson);
|
|
70
|
+
return { status: "ok", email: creds.client_email };
|
|
74
71
|
} catch {
|
|
75
72
|
return { status: "invalid" };
|
|
76
73
|
}
|
|
77
74
|
}
|
|
78
75
|
|
|
76
|
+
/**
|
|
77
|
+
* A saved key that can't be parsed or signed with. Only the signer's error is
|
|
78
|
+
* quoted: a JSON parse error can echo part of the key.
|
|
79
|
+
*/
|
|
80
|
+
function unreadableServiceAccountKey(cause?: string): Error {
|
|
81
|
+
return Object.assign(
|
|
82
|
+
new Error(
|
|
83
|
+
`Couldn't read the saved service account key${cause ? ` (${cause})` : ""}. Re-save the key under Google Cloud in Data sources.`,
|
|
84
|
+
),
|
|
85
|
+
{ errorCode: "google_credentials_unreadable", statusCode: 422 },
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseServiceAccountKey(credsJson: string): {
|
|
90
|
+
client_email: string;
|
|
91
|
+
private_key: string;
|
|
92
|
+
} {
|
|
93
|
+
let creds: { client_email?: unknown; private_key?: unknown };
|
|
94
|
+
try {
|
|
95
|
+
creds = JSON.parse(credsJson);
|
|
96
|
+
} catch {
|
|
97
|
+
throw unreadableServiceAccountKey();
|
|
98
|
+
}
|
|
99
|
+
if (
|
|
100
|
+
!creds ||
|
|
101
|
+
typeof creds !== "object" ||
|
|
102
|
+
typeof creds.private_key !== "string" ||
|
|
103
|
+
!creds.private_key.trim() ||
|
|
104
|
+
typeof creds.client_email !== "string" ||
|
|
105
|
+
!creds.client_email.trim()
|
|
106
|
+
) {
|
|
107
|
+
throw unreadableServiceAccountKey();
|
|
108
|
+
}
|
|
109
|
+
return { client_email: creds.client_email, private_key: creds.private_key };
|
|
110
|
+
}
|
|
111
|
+
|
|
79
112
|
async function getAccessToken(scope: string): Promise<string> {
|
|
80
|
-
const creds =
|
|
113
|
+
const creds = parseServiceAccountKey(await readServiceAccountJson());
|
|
81
114
|
const now = Math.floor(Date.now() / 1000);
|
|
82
115
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
let jwt: string;
|
|
117
|
+
try {
|
|
118
|
+
jwt = await signRs256Jwt(
|
|
119
|
+
{
|
|
120
|
+
iss: creds.client_email,
|
|
121
|
+
scope,
|
|
122
|
+
aud: "https://oauth2.googleapis.com/token",
|
|
123
|
+
iat: now,
|
|
124
|
+
exp: now + 3600,
|
|
125
|
+
},
|
|
126
|
+
creds.private_key,
|
|
127
|
+
);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw unreadableServiceAccountKey(
|
|
130
|
+
error instanceof Error ? error.message : undefined,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
93
133
|
|
|
94
134
|
const res = await fetch("https://oauth2.googleapis.com/token", {
|
|
95
135
|
method: "POST",
|
|
@@ -69,6 +69,11 @@ async function readHeadCommit(
|
|
|
69
69
|
}),
|
|
70
70
|
);
|
|
71
71
|
if (!result.response.ok) {
|
|
72
|
+
if (result.response.status === 404) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
"GitHub could not find this repository or the current authorization cannot access it (HTTP 404). Verify the owner and repository name, and that the connected account has access.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
72
77
|
throw new Error(
|
|
73
78
|
`GitHub commit lookup failed with HTTP ${result.response.status}.`,
|
|
74
79
|
);
|
|
@@ -48,6 +48,7 @@ const GONG_LEGACY_API_KEY = "GONG_API_KEY";
|
|
|
48
48
|
|
|
49
49
|
export type AnalyticsProviderCredentialSource =
|
|
50
50
|
| "workspace_connection"
|
|
51
|
+
| "analytics_oauth"
|
|
51
52
|
| "analytics_local";
|
|
52
53
|
|
|
53
54
|
export interface AnalyticsProviderCredential {
|
|
@@ -241,6 +242,18 @@ function normalizeCoreCredentialResult(
|
|
|
241
242
|
};
|
|
242
243
|
}
|
|
243
244
|
|
|
245
|
+
function isAllowedForCredentialScope(
|
|
246
|
+
credential: AnalyticsProviderCredential,
|
|
247
|
+
ctx: CredentialContext,
|
|
248
|
+
): boolean {
|
|
249
|
+
if (ctx.credentialScope !== "org") return true;
|
|
250
|
+
return (
|
|
251
|
+
Boolean(ctx.orgId) &&
|
|
252
|
+
(credential.scope === "org" || credential.scope === "workspace") &&
|
|
253
|
+
credential.scopeId === ctx.orgId
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
244
257
|
function isGongAuthKey(
|
|
245
258
|
key: string,
|
|
246
259
|
): key is (typeof GONG_ANALYTICS_CREDENTIAL_KEYS)[number] {
|
|
@@ -337,6 +350,25 @@ async function resolveWorkspaceOAuthProviderCredential(
|
|
|
337
350
|
) {
|
|
338
351
|
return null;
|
|
339
352
|
}
|
|
353
|
+
const contextOrgId = options.ctx.orgId?.trim();
|
|
354
|
+
const connectionOrgId = resolvedConnection.connection.orgId?.trim();
|
|
355
|
+
const connectionScope =
|
|
356
|
+
resolvedConnection.connection.config.workspaceConnectionScope;
|
|
357
|
+
// A legacy app allowlist is ambiguous with an app-scoped OAuth grant.
|
|
358
|
+
const legacyOrganizationScope =
|
|
359
|
+
connectionScope === undefined &&
|
|
360
|
+
(resolvedConnection.connection.allowedApps?.length ?? 0) === 0 &&
|
|
361
|
+
(resolvedConnection.connection.allowedUsers?.length ?? 0) === 0 &&
|
|
362
|
+
(resolvedConnection.connection.allowedUserGroups?.length ?? 0) === 0;
|
|
363
|
+
if (
|
|
364
|
+
options.ctx.credentialScope === "org" &&
|
|
365
|
+
(!contextOrgId ||
|
|
366
|
+
!connectionOrgId ||
|
|
367
|
+
connectionOrgId !== contextOrgId ||
|
|
368
|
+
(connectionScope !== "organization" && !legacyOrganizationScope))
|
|
369
|
+
) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
340
372
|
|
|
341
373
|
const oauth = await resolveProviderApiOAuthAccessToken(
|
|
342
374
|
{
|
|
@@ -357,6 +389,12 @@ async function resolveWorkspaceOAuthProviderCredential(
|
|
|
357
389
|
connectionId: oauth.connectionId ?? resolvedConnection.connection.id,
|
|
358
390
|
connectionLabel:
|
|
359
391
|
oauth.connectionLabel ?? resolvedConnection.connection.label,
|
|
392
|
+
...(connectionOrgId
|
|
393
|
+
? {
|
|
394
|
+
scope: "org" as const,
|
|
395
|
+
scopeId: connectionOrgId,
|
|
396
|
+
}
|
|
397
|
+
: {}),
|
|
360
398
|
};
|
|
361
399
|
}
|
|
362
400
|
|
|
@@ -411,7 +449,7 @@ export async function resolveLocalAnalyticsProviderCredential(
|
|
|
411
449
|
for (const key of keys) {
|
|
412
450
|
const credential = await resolveCredentialDetailed(key, options.ctx);
|
|
413
451
|
if (credential) {
|
|
414
|
-
|
|
452
|
+
const normalized: AnalyticsProviderCredential = {
|
|
415
453
|
value: credential.value,
|
|
416
454
|
key,
|
|
417
455
|
provider: options.provider,
|
|
@@ -419,6 +457,9 @@ export async function resolveLocalAnalyticsProviderCredential(
|
|
|
419
457
|
scope: credential.scope,
|
|
420
458
|
scopeId: credential.scopeId,
|
|
421
459
|
};
|
|
460
|
+
if (isAllowedForCredentialScope(normalized, options.ctx)) {
|
|
461
|
+
return normalized;
|
|
462
|
+
}
|
|
422
463
|
}
|
|
423
464
|
}
|
|
424
465
|
return null;
|
|
@@ -464,7 +505,11 @@ export async function resolveAnalyticsProviderCredential(
|
|
|
464
505
|
|
|
465
506
|
const workspaceCredential =
|
|
466
507
|
await resolveWorkspaceConnectionProviderCredential({ ...options, keys });
|
|
467
|
-
if (workspaceCredential)
|
|
508
|
+
if (workspaceCredential) {
|
|
509
|
+
if (isAllowedForCredentialScope(workspaceCredential, options.ctx)) {
|
|
510
|
+
return workspaceCredential;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
468
513
|
|
|
469
514
|
if (!options.connectionId?.trim()) {
|
|
470
515
|
const localCredential = await resolveLocalAnalyticsProviderCredential({
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
} from "./bigquery.js";
|
|
5
5
|
import { describeError } from "./dashboard-panel-runner.js";
|
|
6
6
|
import type { ResolvedDashboardPanelQuery } from "./dashboard-panel-verification.js";
|
|
7
|
+
import { countEnabledDashboardReportSubscriptions } from "./dashboard-report-subscriptions.js";
|
|
7
8
|
import {
|
|
8
9
|
renderFirstPartyAnalyticsBigQueryEstimateSql,
|
|
9
10
|
type AnalyticsScope,
|
|
@@ -276,10 +277,181 @@ export async function estimateResolvedPanel(
|
|
|
276
277
|
};
|
|
277
278
|
}
|
|
278
279
|
|
|
280
|
+
function timedOutPanel(
|
|
281
|
+
panel: { panelId: string; title: string; source: string },
|
|
282
|
+
error: string,
|
|
283
|
+
): PanelCostEstimate {
|
|
284
|
+
return {
|
|
285
|
+
panelId: panel.panelId,
|
|
286
|
+
title: panel.title,
|
|
287
|
+
source: panel.source,
|
|
288
|
+
status: "failed",
|
|
289
|
+
failure: "timeout",
|
|
290
|
+
error,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Ends a batch early without losing it: panels still running or not yet
|
|
296
|
+
* started come back timed out with `error`, and finished panels keep their
|
|
297
|
+
* estimates.
|
|
298
|
+
*/
|
|
299
|
+
export interface EstimateStop {
|
|
300
|
+
signal: AbortSignal;
|
|
301
|
+
error: string;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Estimate one panel, but give up when the deadline passes or `stop` fires,
|
|
306
|
+
* even mid-way through credential, table, or backend lookups, so finished
|
|
307
|
+
* panels still come back before the caller's own timeout.
|
|
308
|
+
*/
|
|
309
|
+
async function estimatePanelBefore(
|
|
310
|
+
deadline: number,
|
|
311
|
+
panel: ResolvedDashboardPanelQuery,
|
|
312
|
+
scope: AnalyticsScope,
|
|
313
|
+
runsPerMonth: number | null,
|
|
314
|
+
signal?: AbortSignal,
|
|
315
|
+
stop?: EstimateStop,
|
|
316
|
+
): Promise<PanelCostEstimate> {
|
|
317
|
+
const remaining = deadline - Date.now();
|
|
318
|
+
if (remaining <= 0) {
|
|
319
|
+
return timedOutPanel(
|
|
320
|
+
panel,
|
|
321
|
+
"Skipped: this request ran out of time. Estimate this panel on its own with panelId.",
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
if (stop?.signal.aborted) return timedOutPanel(panel, stop.error);
|
|
325
|
+
const controller = new AbortController();
|
|
326
|
+
const forwardAbort = () => controller.abort(signal?.reason);
|
|
327
|
+
if (signal?.aborted) forwardAbort();
|
|
328
|
+
signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
329
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
330
|
+
let onStop: (() => void) | undefined;
|
|
331
|
+
const timedOut = new Promise<PanelCostEstimate>((resolve) => {
|
|
332
|
+
const giveUp = (error: string) => {
|
|
333
|
+
resolve(timedOutPanel(panel, error));
|
|
334
|
+
controller.abort();
|
|
335
|
+
};
|
|
336
|
+
timer = setTimeout(
|
|
337
|
+
() =>
|
|
338
|
+
giveUp(
|
|
339
|
+
"Stopped: this panel's dry run didn't finish in time. Estimate it on its own with panelId.",
|
|
340
|
+
),
|
|
341
|
+
remaining,
|
|
342
|
+
);
|
|
343
|
+
if (stop) {
|
|
344
|
+
onStop = () => giveUp(stop.error);
|
|
345
|
+
stop.signal.addEventListener("abort", onStop, { once: true });
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
try {
|
|
349
|
+
return await Promise.race([
|
|
350
|
+
estimateResolvedPanel(panel, scope, runsPerMonth, controller.signal),
|
|
351
|
+
timedOut,
|
|
352
|
+
]);
|
|
353
|
+
} finally {
|
|
354
|
+
clearTimeout(timer);
|
|
355
|
+
signal?.removeEventListener("abort", forwardAbort);
|
|
356
|
+
if (onStop) stop?.signal.removeEventListener("abort", onStop);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function mapPool<T, R>(
|
|
361
|
+
items: readonly T[],
|
|
362
|
+
concurrency: number,
|
|
363
|
+
fn: (item: T) => Promise<R>,
|
|
364
|
+
): Promise<R[]> {
|
|
365
|
+
const results: R[] = Array(items.length);
|
|
366
|
+
let next = 0;
|
|
367
|
+
const worker = async () => {
|
|
368
|
+
while (next < items.length) {
|
|
369
|
+
const index = next++;
|
|
370
|
+
results[index] = await fn(items[index]);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
await Promise.all(
|
|
374
|
+
Array.from({ length: Math.min(concurrency, items.length) }, worker),
|
|
375
|
+
);
|
|
376
|
+
return results;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const DRY_RUN_CONCURRENCY = 6;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Dry-run resolved panels a few at a time; none starts after `deadline` or
|
|
383
|
+
* once `stop` fires.
|
|
384
|
+
*/
|
|
385
|
+
export function estimateResolvedPanels(
|
|
386
|
+
panels: readonly ResolvedDashboardPanelQuery[],
|
|
387
|
+
scope: AnalyticsScope,
|
|
388
|
+
runsPerMonth: number | null,
|
|
389
|
+
deadline: number,
|
|
390
|
+
signal?: AbortSignal,
|
|
391
|
+
stop?: EstimateStop,
|
|
392
|
+
): Promise<PanelCostEstimate[]> {
|
|
393
|
+
return mapPool(panels, DRY_RUN_CONCURRENCY, (panel) =>
|
|
394
|
+
estimatePanelBefore(deadline, panel, scope, runsPerMonth, signal, stop),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* One run a day for each enabled email report on the dashboard in the
|
|
400
|
+
* caller's tenant; other tenants' reports run on their own BigQuery
|
|
401
|
+
* connection.
|
|
402
|
+
*/
|
|
403
|
+
export async function scheduledDashboardRuns(
|
|
404
|
+
dashboardId: string,
|
|
405
|
+
scope: AnalyticsScope,
|
|
406
|
+
): Promise<{ reportSchedules: number; runsPerMonth: number | null }> {
|
|
407
|
+
const reportSchedules = await countEnabledDashboardReportSubscriptions(
|
|
408
|
+
dashboardId,
|
|
409
|
+
{ email: scope.userEmail, orgId: scope.orgId },
|
|
410
|
+
);
|
|
411
|
+
return {
|
|
412
|
+
reportSchedules,
|
|
413
|
+
runsPerMonth: reportSchedules > 0 ? reportSchedules * DAYS_PER_MONTH : null,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function describeReportSchedules(
|
|
418
|
+
reportSchedules: number,
|
|
419
|
+
orgId: string | null,
|
|
420
|
+
): string {
|
|
421
|
+
return `${reportSchedules} enabled daily email report${reportSchedules === 1 ? "" : "s"} ${orgId ? "in your org" : "of yours"}`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function describeEstimate(estimate: QueryCostEstimate): string {
|
|
425
|
+
if (
|
|
426
|
+
estimate.status === "not_bigquery" ||
|
|
427
|
+
estimate.status === "not_estimated"
|
|
428
|
+
) {
|
|
429
|
+
return estimate.reason;
|
|
430
|
+
}
|
|
431
|
+
if (estimate.status === "failed") {
|
|
432
|
+
return `no estimate (${estimate.failure}): ${estimate.error}`;
|
|
433
|
+
}
|
|
434
|
+
const monthly =
|
|
435
|
+
estimate.usdPerMonth === null
|
|
436
|
+
? "not scheduled"
|
|
437
|
+
: `~${formatUsd(estimate.usdPerMonth)}/month over ${estimate.runsPerMonth} scheduled runs`;
|
|
438
|
+
const flags = [
|
|
439
|
+
estimate.overPerRunThreshold
|
|
440
|
+
? `over the ${formatUsd(QUERY_COST_THRESHOLDS.perRunUsd)}/run threshold`
|
|
441
|
+
: null,
|
|
442
|
+
estimate.overPerMonthThreshold
|
|
443
|
+
? `over the ${formatUsd(QUERY_COST_THRESHOLDS.perMonthUsd)}/month threshold`
|
|
444
|
+
: null,
|
|
445
|
+
].filter(Boolean);
|
|
446
|
+
return `~${formatBytes(estimate.estimatedBytes)}, ${formatUsd(estimate.usdPerRun)} per run, ${monthly}${flags.length ? ` (${flags.join("; ")})` : ""}`;
|
|
447
|
+
}
|
|
448
|
+
|
|
279
449
|
export function totalCost(estimates: readonly QueryCostEstimate[]): {
|
|
280
450
|
estimatedBytes: number;
|
|
281
451
|
usdPerRun: number;
|
|
282
452
|
usdPerMonth: number | null;
|
|
453
|
+
overPerRunThreshold: boolean;
|
|
454
|
+
overPerMonthThreshold: boolean;
|
|
283
455
|
} {
|
|
284
456
|
const estimated = estimates.filter(
|
|
285
457
|
(
|
|
@@ -288,13 +460,20 @@ export function totalCost(estimates: readonly QueryCostEstimate[]): {
|
|
|
288
460
|
estimate.status === "estimated",
|
|
289
461
|
);
|
|
290
462
|
const monthly = estimated.filter((estimate) => estimate.usdPerMonth !== null);
|
|
463
|
+
const usdPerRun = roundUsd(
|
|
464
|
+
estimated.reduce((sum, e) => sum + e.usdPerRun, 0),
|
|
465
|
+
);
|
|
466
|
+
const usdPerMonth =
|
|
467
|
+
monthly.length === 0
|
|
468
|
+
? null
|
|
469
|
+
: roundUsd(monthly.reduce((sum, e) => sum + (e.usdPerMonth ?? 0), 0));
|
|
291
470
|
return {
|
|
292
471
|
estimatedBytes: estimated.reduce((sum, e) => sum + e.estimatedBytes, 0),
|
|
293
|
-
usdPerRun
|
|
294
|
-
usdPerMonth
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
472
|
+
usdPerRun,
|
|
473
|
+
usdPerMonth,
|
|
474
|
+
overPerRunThreshold: usdPerRun > QUERY_COST_THRESHOLDS.perRunUsd,
|
|
475
|
+
overPerMonthThreshold:
|
|
476
|
+
usdPerMonth !== null && usdPerMonth > QUERY_COST_THRESHOLDS.perMonthUsd,
|
|
298
477
|
};
|
|
299
478
|
}
|
|
300
479
|
|
|
@@ -15,7 +15,10 @@ import "../db/index.js";
|
|
|
15
15
|
import * as schema from "../db/schema.js";
|
|
16
16
|
import { EXTERNAL_DASHBOARD_MIGRATION_SQL } from "../lib/bi-migration/external-dashboards-store.js";
|
|
17
17
|
import { BUILTIN_DASHBOARD_CHECKS_MIGRATION_SQL } from "../lib/builtin-dashboard-checks-migration.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
CONTENT_METRICS_MIGRATION_SQL,
|
|
20
|
+
CONTENT_COVERAGE_CONFIG_MIGRATION_SQL,
|
|
21
|
+
} from "../lib/content-metrics/migration.js";
|
|
19
22
|
import { DATA_NOTES_MIGRATION_SQL } from "../lib/data-notes-migration.js";
|
|
20
23
|
import { KEY_DATES_MIGRATION_SQL } from "../lib/key-dates-migration.js";
|
|
21
24
|
import { isProductionServerlessRuntime } from "../lib/production-serverless-runtime.js";
|
|
@@ -1839,6 +1842,11 @@ CREATE UNIQUE INDEX IF NOT EXISTS dashboard_views_default_per_dashboard_idx
|
|
|
1839
1842
|
name: "analytics-content-metrics-tables",
|
|
1840
1843
|
sql: CONTENT_METRICS_MIGRATION_SQL.join(";\n"),
|
|
1841
1844
|
},
|
|
1845
|
+
{
|
|
1846
|
+
version: 181,
|
|
1847
|
+
name: "analytics-content-coverage-config",
|
|
1848
|
+
sql: CONTENT_COVERAGE_CONFIG_MIGRATION_SQL,
|
|
1849
|
+
},
|
|
1842
1850
|
],
|
|
1843
1851
|
{ table: "analytics_migrations" },
|
|
1844
1852
|
);
|
|
@@ -30,6 +30,7 @@ export const SEED_BUILTIN_DASHBOARD_IDS = [
|
|
|
30
30
|
"skills-cli-funnel",
|
|
31
31
|
"google-analytics",
|
|
32
32
|
"node-exporter-full",
|
|
33
|
+
"content-view",
|
|
33
34
|
] as const;
|
|
34
35
|
|
|
35
36
|
export function nativeV2BuiltinDashboardId(templateId: string): string {
|
|
@@ -55,6 +56,7 @@ const LEGACY_NATIVE_ID = /^native-(.+)-[0-9a-f]{10}$/;
|
|
|
55
56
|
* never belonged to one. Seed ids map to themselves.
|
|
56
57
|
*/
|
|
57
58
|
export function builtinDashboardIdForLegacyId(id: string): string | null {
|
|
59
|
+
if (/^content-view-[0-9a-f]{10}$/.test(id)) return "content-view";
|
|
58
60
|
if (LEGACY_DEMO_ID.test(id)) return BUILTIN_DEMO_DASHBOARD_ID;
|
|
59
61
|
if (id === "risk-meeting") return BUILTIN_RISK_MEETING_DASHBOARD_ID;
|
|
60
62
|
const native = LEGACY_NATIVE_ID.exec(id);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Console addresses a property by the exact string it was created
|
|
3
|
+
* with: `sc-domain:example.com` for a Domain property, or the full URL with
|
|
4
|
+
* its trailing slash for a URL-prefix property. Anything else reaches the API
|
|
5
|
+
* as a property nobody has access to, so it fails as "no access" instead of
|
|
6
|
+
* as a typo.
|
|
7
|
+
*/
|
|
8
|
+
export type SearchConsolePropertyProblem =
|
|
9
|
+
| { kind: "bare_domain"; suggestion: string }
|
|
10
|
+
| { kind: "invalid_domain"; suggestion?: string }
|
|
11
|
+
| { kind: "missing_trailing_slash"; suggestion: string }
|
|
12
|
+
| { kind: "unrecognized" };
|
|
13
|
+
|
|
14
|
+
export function normalizeSearchConsoleProperty(value: string): string {
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
return /^sc-domain:/i.test(trimmed) ? trimmed.toLowerCase() : trimmed;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function searchConsolePathPrefixProblem(value?: string): boolean {
|
|
20
|
+
return Boolean(value?.trim() && !value.trim().startsWith("/"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const HOSTNAME =
|
|
24
|
+
/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
|
|
25
|
+
|
|
26
|
+
function hostOf(value: string): string | null {
|
|
27
|
+
const url = URL.canParse(value)
|
|
28
|
+
? new URL(value)
|
|
29
|
+
: URL.canParse(`https://${value}`)
|
|
30
|
+
? new URL(`https://${value}`)
|
|
31
|
+
: null;
|
|
32
|
+
return url && HOSTNAME.test(url.hostname) ? url.hostname.toLowerCase() : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function searchConsolePropertyProblem(
|
|
36
|
+
siteUrl: string,
|
|
37
|
+
): SearchConsolePropertyProblem | null {
|
|
38
|
+
const value = siteUrl.trim();
|
|
39
|
+
if (value.toLowerCase().startsWith("sc-domain:")) {
|
|
40
|
+
const domain = value.slice("sc-domain:".length);
|
|
41
|
+
if (HOSTNAME.test(domain)) return null;
|
|
42
|
+
const host = hostOf(domain);
|
|
43
|
+
return host
|
|
44
|
+
? { kind: "invalid_domain", suggestion: `sc-domain:${host}` }
|
|
45
|
+
: { kind: "invalid_domain" };
|
|
46
|
+
}
|
|
47
|
+
if (/^https?:\/\//i.test(value)) {
|
|
48
|
+
if (!URL.canParse(value) || !hostOf(value)) return { kind: "unrecognized" };
|
|
49
|
+
return value.endsWith("/")
|
|
50
|
+
? null
|
|
51
|
+
: { kind: "missing_trailing_slash", suggestion: `${value}/` };
|
|
52
|
+
}
|
|
53
|
+
const host = /^[^/\s:]+$/.test(value) ? hostOf(value) : null;
|
|
54
|
+
return host
|
|
55
|
+
? { kind: "bare_domain", suggestion: `sc-domain:${host}` }
|
|
56
|
+
: { kind: "unrecognized" };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The English explanation agents and the action's validation error show. */
|
|
60
|
+
export function searchConsolePropertyMessage(
|
|
61
|
+
siteUrl: string,
|
|
62
|
+
problem: SearchConsolePropertyProblem,
|
|
63
|
+
): string {
|
|
64
|
+
const forms =
|
|
65
|
+
"Use sc-domain:example.com for a Domain property, or the full URL-prefix property ending in /, such as https://www.example.com/.";
|
|
66
|
+
switch (problem.kind) {
|
|
67
|
+
case "bare_domain":
|
|
68
|
+
return `"${siteUrl}" isn't a Search Console property. For a Domain property, use ${problem.suggestion}.`;
|
|
69
|
+
case "invalid_domain":
|
|
70
|
+
return problem.suggestion
|
|
71
|
+
? `A Domain property is sc-domain: followed by the domain only, with no https:// or path: ${problem.suggestion}.`
|
|
72
|
+
: `"${siteUrl}" isn't a valid Domain property. ${forms}`;
|
|
73
|
+
case "missing_trailing_slash":
|
|
74
|
+
return `URL-prefix properties end in /. Use ${problem.suggestion}.`;
|
|
75
|
+
case "unrecognized":
|
|
76
|
+
return `"${siteUrl}" isn't a Search Console property. ${forms}`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -95,9 +95,9 @@ MCP clients see no `outputSchema` by default. Set `mcpOutputSchema: true` (requi
|
|
|
95
95
|
|
|
96
96
|
`authorize` decides whether the caller may run the action at all. It wraps
|
|
97
97
|
`run`, so it holds at **all six dispatch sites** — agent tool, HTTP, frontend
|
|
98
|
-
hook, MCP, A2A, CLI
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
hook, MCP, A2A, CLI. Use `authorize` for "not everyone may do this"; use
|
|
99
|
+
`needsApproval` to ask a human to bless one call a permitted caller is already
|
|
100
|
+
allowed to make.
|
|
101
101
|
|
|
102
102
|
```ts
|
|
103
103
|
import { coachAccess } from "../lib/access.js"; // defineAppRoles(...)
|
|
@@ -140,4 +140,6 @@ export default defineAction({
|
|
|
140
140
|
|
|
141
141
|
When the gate is truthy and the call isn't yet approved, the loop emits an `approval_required` event and **stops the turn — `run()` never executes**. A predicate gates conditionally (e.g. only external recipients) and **fails closed**: a throw is treated as "approval required". It receives its own copy of the prepared snapshot of the validated arguments, defaults applied, and the approval binds to the snapshot's hash, so an approved call runs only with the arguments that were approved (a Date binds by its time). The human approves via the chat UI's Approve affordance, which re-issues the turn with the call's `approvalKey`, and only then does the action run.
|
|
142
142
|
|
|
143
|
+
Approval is given in the app, so HTTP never grants it. The HTTP action route and the generated Workers entry run a gated call only for the proven app UI (a frontend call from the signed-in user, same origin, holding the short-lived UI capability; `isProvenUiRequest`, the proof `uiOnly` uses) or the app's MCP directory widget. Every other HTTP caller (bearer or API key, MCP OAuth token, org service token, A2A, extension iframe, mobile app) gets `409 approval_required`, and a service identity can never approve. The shared client presents the capability with every write from a signed-in session (minted once per user, reused for nine minutes, never for reads or public pages), so a gated button works on its first request. A call without proof gets `403 ui_capability_required`; the client then re-mints and retries once, which is only the fallback for an expired or rejected capability. Every surface evaluates the gate through `preparedCallNeedsApproval`, so a throwing predicate asks there too. A caller that must run unattended needs a different action, never a bypass of the gate.
|
|
144
|
+
|
|
143
145
|
**Keep approvals rare** — the default is off and almost every action should leave it off. The canonical example is Mail's `send-email` (`needsApproval: true`). See the `security` skill and the Human Approval doc.
|
|
@@ -95,9 +95,9 @@ MCP clients see no `outputSchema` by default. Set `mcpOutputSchema: true` (requi
|
|
|
95
95
|
|
|
96
96
|
`authorize` decides whether the caller may run the action at all. It wraps
|
|
97
97
|
`run`, so it holds at **all six dispatch sites** — agent tool, HTTP, frontend
|
|
98
|
-
hook, MCP, A2A, CLI
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
hook, MCP, A2A, CLI. Use `authorize` for "not everyone may do this"; use
|
|
99
|
+
`needsApproval` to ask a human to bless one call a permitted caller is already
|
|
100
|
+
allowed to make.
|
|
101
101
|
|
|
102
102
|
```ts
|
|
103
103
|
import { coachAccess } from "../lib/access.js"; // defineAppRoles(...)
|
|
@@ -140,4 +140,6 @@ export default defineAction({
|
|
|
140
140
|
|
|
141
141
|
When the gate is truthy and the call isn't yet approved, the loop emits an `approval_required` event and **stops the turn — `run()` never executes**. A predicate gates conditionally (e.g. only external recipients) and **fails closed**: a throw is treated as "approval required". It receives its own copy of the prepared snapshot of the validated arguments, defaults applied, and the approval binds to the snapshot's hash, so an approved call runs only with the arguments that were approved (a Date binds by its time). The human approves via the chat UI's Approve affordance, which re-issues the turn with the call's `approvalKey`, and only then does the action run.
|
|
142
142
|
|
|
143
|
+
Approval is given in the app, so HTTP never grants it. The HTTP action route and the generated Workers entry run a gated call only for the proven app UI (a frontend call from the signed-in user, same origin, holding the short-lived UI capability; `isProvenUiRequest`, the proof `uiOnly` uses) or the app's MCP directory widget. Every other HTTP caller (bearer or API key, MCP OAuth token, org service token, A2A, extension iframe, mobile app) gets `409 approval_required`, and a service identity can never approve. The shared client presents the capability with every write from a signed-in session (minted once per user, reused for nine minutes, never for reads or public pages), so a gated button works on its first request. A call without proof gets `403 ui_capability_required`; the client then re-mints and retries once, which is only the fallback for an expired or rejected capability. Every surface evaluates the gate through `preparedCallNeedsApproval`, so a throwing predicate asks there too. A caller that must run unattended needs a different action, never a bypass of the gate.
|
|
144
|
+
|
|
143
145
|
**Keep approvals rare** — the default is off and almost every action should leave it off. The canonical example is Mail's `send-email` (`needsApproval: true`). See the `security` skill and the Human Approval doc.
|
|
@@ -95,9 +95,9 @@ MCP clients see no `outputSchema` by default. Set `mcpOutputSchema: true` (requi
|
|
|
95
95
|
|
|
96
96
|
`authorize` decides whether the caller may run the action at all. It wraps
|
|
97
97
|
`run`, so it holds at **all six dispatch sites** — agent tool, HTTP, frontend
|
|
98
|
-
hook, MCP, A2A, CLI
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
hook, MCP, A2A, CLI. Use `authorize` for "not everyone may do this"; use
|
|
99
|
+
`needsApproval` to ask a human to bless one call a permitted caller is already
|
|
100
|
+
allowed to make.
|
|
101
101
|
|
|
102
102
|
```ts
|
|
103
103
|
import { coachAccess } from "../lib/access.js"; // defineAppRoles(...)
|
|
@@ -140,4 +140,6 @@ export default defineAction({
|
|
|
140
140
|
|
|
141
141
|
When the gate is truthy and the call isn't yet approved, the loop emits an `approval_required` event and **stops the turn — `run()` never executes**. A predicate gates conditionally (e.g. only external recipients) and **fails closed**: a throw is treated as "approval required". It receives its own copy of the prepared snapshot of the validated arguments, defaults applied, and the approval binds to the snapshot's hash, so an approved call runs only with the arguments that were approved (a Date binds by its time). The human approves via the chat UI's Approve affordance, which re-issues the turn with the call's `approvalKey`, and only then does the action run.
|
|
142
142
|
|
|
143
|
+
Approval is given in the app, so HTTP never grants it. The HTTP action route and the generated Workers entry run a gated call only for the proven app UI (a frontend call from the signed-in user, same origin, holding the short-lived UI capability; `isProvenUiRequest`, the proof `uiOnly` uses) or the app's MCP directory widget. Every other HTTP caller (bearer or API key, MCP OAuth token, org service token, A2A, extension iframe, mobile app) gets `409 approval_required`, and a service identity can never approve. The shared client presents the capability with every write from a signed-in session (minted once per user, reused for nine minutes, never for reads or public pages), so a gated button works on its first request. A call without proof gets `403 ui_capability_required`; the client then re-mints and retries once, which is only the fallback for an expired or rejected capability. Every surface evaluates the gate through `preparedCallNeedsApproval`, so a throwing predicate asks there too. A caller that must run unattended needs a different action, never a bypass of the gate.
|
|
144
|
+
|
|
143
145
|
**Keep approvals rare** — the default is off and almost every action should leave it off. The canonical example is Mail's `send-email` (`needsApproval: true`). See the `security` skill and the Human Approval doc.
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
cliBoolean,
|
|
12
12
|
googleEventResultId,
|
|
13
13
|
normalizeWritableGoogleEventId,
|
|
14
|
-
rawCliBoolean,
|
|
15
14
|
requireActionUserEmail,
|
|
16
15
|
resolveGoogleEventAccountEmail,
|
|
17
16
|
resolveOwnedAccountEmail,
|
|
@@ -53,8 +52,7 @@ export default defineAction({
|
|
|
53
52
|
}),
|
|
54
53
|
toolCallable: false,
|
|
55
54
|
needsApproval: ({ sendUpdates, notificationMessage, removeOnly }) =>
|
|
56
|
-
!
|
|
57
|
-
(sendUpdates === "all" || !!notificationMessage?.trim()),
|
|
55
|
+
!removeOnly && (sendUpdates === "all" || !!notificationMessage?.trim()),
|
|
58
56
|
run: async (args) => {
|
|
59
57
|
const ownerEmail = requireActionUserEmail();
|
|
60
58
|
if (!(await googleCalendar.isConnected(ownerEmail))) {
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
isBookedOnAccount,
|
|
20
20
|
mapWithConcurrency,
|
|
21
21
|
normalizeWritableGoogleEventId,
|
|
22
|
-
rawCliBoolean,
|
|
23
22
|
requireActionUserEmail,
|
|
24
23
|
requireExplicitBound,
|
|
25
24
|
resolveBulkGoogleEventAccountEmail,
|
|
@@ -121,7 +120,7 @@ export default defineAction({
|
|
|
121
120
|
.describe("Return the matched events without deleting anything"),
|
|
122
121
|
}),
|
|
123
122
|
toolCallable: false,
|
|
124
|
-
needsApproval: ({ dryRun }) => !
|
|
123
|
+
needsApproval: ({ dryRun }) => !dryRun,
|
|
125
124
|
run: async (args) => {
|
|
126
125
|
const ownerEmail = requireActionUserEmail();
|
|
127
126
|
if (!(await googleCalendar.isConnected(ownerEmail))) {
|
|
@@ -21,10 +21,6 @@ export const cliBoolean = z
|
|
|
21
21
|
.union([z.boolean(), z.enum(["true", "false"])])
|
|
22
22
|
.transform((value) => value === true || value === "true");
|
|
23
23
|
|
|
24
|
-
export function rawCliBoolean(value: unknown): boolean {
|
|
25
|
-
return value === true || value === "true";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
24
|
export const eventTypeInput = z
|
|
29
25
|
.enum(["default", "outOfOffice", "focusTime", "workingLocation"])
|
|
30
26
|
.optional();
|
|
@@ -74,12 +74,8 @@ function mergeAttendees(
|
|
|
74
74
|
return Array.from(merged.values());
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function namesGuests(value:
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
(normalizeAttendees(value as Parameters<typeof normalizeAttendees>[0])
|
|
81
|
-
?.length ?? 0) > 0
|
|
82
|
-
);
|
|
77
|
+
function namesGuests(value: Parameters<typeof normalizeAttendees>[0]): boolean {
|
|
78
|
+
return (normalizeAttendees(value)?.length ?? 0) > 0;
|
|
83
79
|
}
|
|
84
80
|
|
|
85
81
|
function workingLocationTitle(
|