@aglyn/plugins-ai 1.0.0-beta.231 → 1.0.0-beta.232
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/package.json +17 -17
- package/src/lib/activity/ai-activity-actions.js +8 -4
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-job-follow.component.js +1 -0
- package/src/lib/components/ai-job-follow.component.js.map +1 -1
- package/src/lib/components/ai-job-links.js +13 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-jobs-list-page.component.js +1 -0
- package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
- package/src/lib/components/ai-media-create.component.d.ts +20 -7
- package/src/lib/components/ai-media-create.component.js +107 -55
- package/src/lib/components/ai-media-create.component.js.map +1 -1
- package/src/lib/components/ai-seo-fields-card.component.js +1 -0
- package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
- package/src/lib/components/ai-site-memory-card.component.d.ts +32 -0
- package/src/lib/components/ai-site-memory-card.component.js +187 -0
- package/src/lib/components/ai-site-memory-card.component.js.map +1 -0
- package/src/lib/components/ai-site-start-card.component.js +80 -77
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-theme-proposal-card.component.js +4 -0
- package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
- package/src/lib/components/ai-usage-strip.component.js +1 -0
- package/src/lib/components/ai-usage-strip.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.d.ts +10 -0
- package/src/lib/components/assist-panel.component.js +119 -9
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/assist-signals-page.component.js +2 -0
- package/src/lib/components/assist-signals-page.component.js.map +1 -1
- package/src/lib/components/billing-ai-allotments.component.js +3 -0
- package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
- package/src/lib/components/billing-ai-top-users.component.js +1 -0
- package/src/lib/components/billing-ai-top-users.component.js.map +1 -1
- package/src/lib/components/billing-assist-overage-card.component.js +2 -0
- package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
- package/src/lib/components/member-ai-usage-card.component.js +10 -0
- package/src/lib/components/member-ai-usage-card.component.js.map +1 -1
- package/src/lib/components/staff-org-ai-card.component.js +1 -0
- package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
- package/src/lib/components/staff-org-ai-conversations.component.d.ts +23 -0
- package/src/lib/components/staff-org-ai-conversations.component.js +346 -0
- package/src/lib/components/staff-org-ai-conversations.component.js.map +1 -0
- package/src/lib/jobs/ai-build-capabilities.d.ts +13 -1
- package/src/lib/jobs/ai-build-capabilities.js +101 -7
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-business-profile-prefill.d.ts +29 -0
- package/src/lib/jobs/ai-business-profile-prefill.js +85 -0
- package/src/lib/jobs/ai-business-profile-prefill.js.map +1 -0
- package/src/lib/jobs/ai-job-build-step.js +3 -1
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-edit-step.d.ts +225 -0
- package/src/lib/jobs/ai-job-edit-step.js +631 -0
- package/src/lib/jobs/ai-job-edit-step.js.map +1 -0
- package/src/lib/jobs/ai-job-insight-step.js +21 -4
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +8 -3
- package/src/lib/jobs/ai-job-layout-language.js +8 -2
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +7 -6
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-logic-step.d.ts +20 -0
- package/src/lib/jobs/ai-job-logic-step.js +87 -6
- package/src/lib/jobs/ai-job-logic-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +52 -7
- package/src/lib/jobs/ai-job-page-language.js +72 -9
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +12 -2
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +19 -5
- package/src/lib/jobs/ai-job-plan-step.js +54 -20
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +163 -0
- package/src/lib/jobs/ai-job-site-content.js +443 -0
- package/src/lib/jobs/ai-job-site-content.js.map +1 -0
- package/src/lib/jobs/ai-job-site-look.d.ts +112 -0
- package/src/lib/jobs/ai-job-site-look.js +322 -0
- package/src/lib/jobs/ai-job-site-look.js.map +1 -0
- package/src/lib/jobs/ai-job-site-step.d.ts +26 -5
- package/src/lib/jobs/ai-job-site-step.js +163 -24
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +6 -1
- package/src/lib/jobs/ai-jobs-notify.js +15 -3
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
- package/src/lib/layout-language/ai-layout-compiler.d.ts +9 -0
- package/src/lib/layout-language/ai-layout-compiler.js +120 -33
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.js +5 -4
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +12 -5
- package/src/lib/layout-language/ai-layout-language.js +92 -9
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +144 -0
- package/src/lib/layout-language/ai-layout-pictures.js +214 -0
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -0
- package/src/lib/layout-language/ai-layout-store.d.ts +2 -0
- package/src/lib/layout-language/ai-layout-store.js +7 -1
- package/src/lib/layout-language/ai-layout-store.js.map +1 -1
- package/src/lib/model/ai-build-progress.js +4 -1
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-edit-job.d.ts +109 -0
- package/src/lib/model/ai-edit-job.js +157 -0
- package/src/lib/model/ai-edit-job.js.map +1 -0
- package/src/lib/model/ai-job-activity.js +1 -0
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +3 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-logic-job.d.ts +18 -1
- package/src/lib/model/ai-logic-job.js +24 -3
- package/src/lib/model/ai-logic-job.js.map +1 -1
- package/src/lib/model/ai-media-credits.d.ts +8 -4
- package/src/lib/model/ai-media-credits.js +11 -4
- package/src/lib/model/ai-media-credits.js.map +1 -1
- package/src/lib/model/ai-media-kinds.d.ts +68 -0
- package/src/lib/model/ai-media-kinds.js +98 -0
- package/src/lib/model/ai-media-kinds.js.map +1 -0
- package/src/lib/model/ai-site-build-progress.d.ts +4 -0
- package/src/lib/model/ai-site-build-progress.js +28 -11
- package/src/lib/model/ai-site-build-progress.js.map +1 -1
- package/src/lib/model/ai-site-context.d.ts +111 -0
- package/src/lib/model/ai-site-context.js +222 -0
- package/src/lib/model/ai-site-context.js.map +1 -0
- package/src/lib/model/ai-site-job.d.ts +10 -2
- package/src/lib/model/ai-site-job.js +12 -5
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.d.ts +164 -0
- package/src/lib/model/ai-site-kinds.js +1942 -0
- package/src/lib/model/ai-site-kinds.js.map +1 -0
- package/src/lib/model/ai-site-look.d.ts +150 -0
- package/src/lib/model/ai-site-look.js +1236 -0
- package/src/lib/model/ai-site-look.js.map +1 -0
- package/src/lib/model/ai-site-memory.d.ts +80 -0
- package/src/lib/model/ai-site-memory.js +171 -0
- package/src/lib/model/ai-site-memory.js.map +1 -0
- package/src/lib/model/ai-site-start.d.ts +10 -1
- package/src/lib/model/ai-site-start.js +16 -2
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/model/assist-follow-up.d.ts +92 -0
- package/src/lib/model/assist-follow-up.js +128 -0
- package/src/lib/model/assist-follow-up.js.map +1 -0
- package/src/lib/plugin-config.js +2 -0
- package/src/lib/plugin-config.js.map +1 -1
- package/src/lib/plugin.js +20 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/providers/anthropic.d.ts +9 -1
- package/src/lib/providers/anthropic.js +143 -9
- package/src/lib/providers/anthropic.js.map +1 -1
- package/src/lib/providers/catalog.d.ts +37 -7
- package/src/lib/providers/catalog.js +103 -11
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/providers/contract.d.ts +29 -0
- package/src/lib/providers/contract.js.map +1 -1
- package/src/lib/providers/image-contract.d.ts +22 -1
- package/src/lib/providers/image-contract.js +40 -1
- package/src/lib/providers/image-contract.js.map +1 -1
- package/src/lib/providers/image-endpoints.d.ts +24 -0
- package/src/lib/providers/image-endpoints.js +28 -0
- package/src/lib/providers/image-endpoints.js.map +1 -0
- package/src/lib/providers/model-choice.js +23 -2
- package/src/lib/providers/model-choice.js.map +1 -1
- package/src/lib/providers/routing.js +18 -0
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/providers/vertex-image-endpoint.d.ts +48 -0
- package/src/lib/providers/vertex-image-endpoint.js +80 -0
- package/src/lib/providers/vertex-image-endpoint.js.map +1 -0
- package/src/lib/providers/vertex-image.d.ts +13 -27
- package/src/lib/providers/vertex-image.js +15 -42
- package/src/lib/providers/vertex-image.js.map +1 -1
- package/src/lib/runtime/ai-blog-post-generation.d.ts +62 -0
- package/src/lib/runtime/ai-blog-post-generation.js +89 -0
- package/src/lib/runtime/ai-blog-post-generation.js.map +1 -0
- package/src/lib/runtime/ai-dev-env.d.ts +53 -0
- package/src/lib/runtime/ai-dev-env.js +72 -0
- package/src/lib/runtime/ai-dev-env.js.map +1 -0
- package/src/lib/runtime/ai-dev-replay.d.ts +116 -0
- package/src/lib/runtime/ai-dev-replay.js +313 -0
- package/src/lib/runtime/ai-dev-replay.js.map +1 -0
- package/src/lib/runtime/ai-doctrine.js +5 -1
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.js +4 -0
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-eval.d.ts +13 -1
- package/src/lib/runtime/ai-eval.js +84 -1
- package/src/lib/runtime/ai-eval.js.map +1 -1
- package/src/lib/runtime/ai-live-batch.d.ts +45 -0
- package/src/lib/runtime/ai-live-batch.js +89 -0
- package/src/lib/runtime/ai-live-batch.js.map +1 -0
- package/src/lib/runtime/ai-runtime.js +46 -2
- package/src/lib/runtime/ai-runtime.js.map +1 -1
- package/src/lib/runtime/site-context.d.ts +58 -0
- package/src/lib/runtime/site-context.js +127 -0
- package/src/lib/runtime/site-context.js.map +1 -0
- package/src/lib/server/ai-admin-conversations.d.ts +22 -0
- package/src/lib/server/ai-admin-conversations.js +197 -0
- package/src/lib/server/ai-admin-conversations.js.map +1 -0
- package/src/lib/server/ai-media-image.d.ts +5 -5
- package/src/lib/server/ai-media-image.js +25 -11
- package/src/lib/server/ai-media-image.js.map +1 -1
- package/src/lib/server/ai-media-raster-prompt.d.ts +34 -0
- package/src/lib/server/ai-media-raster-prompt.js +53 -0
- package/src/lib/server/ai-media-raster-prompt.js.map +1 -0
- package/src/lib/server/assist-chat.d.ts +11 -0
- package/src/lib/server/assist-chat.js +57 -18
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-edit-applied.js +16 -3
- package/src/lib/server/assist-edit-applied.js.map +1 -1
- package/src/lib/server/assist-view-context.d.ts +19 -1
- package/src/lib/server/assist-view-context.js +38 -1
- package/src/lib/server/assist-view-context.js.map +1 -1
- package/src/lib/server.js +10 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/subprocessors.js +17 -1
- package/src/lib/subprocessors.js.map +1 -1
- package/src/lib/tools/ai-blog-post-tool.d.ts +60 -0
- package/src/lib/tools/ai-blog-post-tool.js +175 -0
- package/src/lib/tools/ai-blog-post-tool.js.map +1 -0
- package/src/lib/usage/assist-usage.d.ts +3 -0
- package/src/lib/usage/assist-usage.js +4 -2
- package/src/lib/usage/assist-usage.js.map +1 -1
- package/src/lib/usage/staff-org-ai-conversations.d.ts +103 -0
- package/src/lib/usage/staff-org-ai-conversations.js +152 -0
- package/src/lib/usage/staff-org-ai-conversations.js.map +1 -0
|
@@ -18,6 +18,8 @@ import { permissionRefusal, checkRateLimit, emailUnverifiedResponse, firebaseAdm
|
|
|
18
18
|
import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
19
19
|
import { logAiEditApplied } from "../activity/ai-activity.js";
|
|
20
20
|
import { ASSIST_EDIT_DOCUMENT_KINDS, ASSIST_EDIT_OP_COUNT_WORDS, assistEditDocumentOf } from "../model/assist-edit.js";
|
|
21
|
+
import { aiPreferencesFromEdit } from "../model/ai-site-memory.js";
|
|
22
|
+
import { rememberAiSitePreferences } from "../runtime/site-context.js";
|
|
21
23
|
import { sanitiseId } from "./assist-view-context.js";
|
|
22
24
|
/**
|
|
23
25
|
* The applied-edit record (AGL-2906, AGL-2929): the panel reports that the
|
|
@@ -25,8 +27,9 @@ import { sanitiseId } from "./assist-view-context.js";
|
|
|
25
27
|
* `ai.edit.applied` row to the site's activity log, attributed to them.
|
|
26
28
|
*
|
|
27
29
|
* The edits were applied in the author's own editor, as unsaved changes;
|
|
28
|
-
* nothing here can see them or repeat them, and nothing here writes
|
|
29
|
-
* document.
|
|
30
|
+
* nothing here can see them or repeat them, and nothing here writes the
|
|
31
|
+
* edited document. (It does remember what the edit showed about the owner's
|
|
32
|
+
* preferences, AGL-3661 — see `model/ai-site-memory.ts`.) What this door checks is that the report stands on a proposal
|
|
30
33
|
* the server issued: the exchange exists under the named org, the same
|
|
31
34
|
* member asked it about the same site, and its signal records edit
|
|
32
35
|
* operations proposed from the besigner route of the same document. A
|
|
@@ -205,7 +208,7 @@ async function handler(request) {
|
|
|
205
208
|
status: 200
|
|
206
209
|
});
|
|
207
210
|
case 'recorded':
|
|
208
|
-
var _decoded_email;
|
|
211
|
+
var _decoded_email, _asked_question;
|
|
209
212
|
await logAiEditApplied(report.hostId, {
|
|
210
213
|
uid: decoded.uid,
|
|
211
214
|
email: (_decoded_email = decoded.email) != null ? _decoded_email : null
|
|
@@ -215,6 +218,16 @@ async function handler(request) {
|
|
|
215
218
|
versionId: report.versionId,
|
|
216
219
|
opCounts: report.opCounts
|
|
217
220
|
});
|
|
221
|
+
// What the applied edit showed about the owner's preferences
|
|
222
|
+
// (AGL-3661), remembered for every later AI job on the site. Rules,
|
|
223
|
+
// no model call; a failure never fails the record it follows.
|
|
224
|
+
await rememberAiSitePreferences(firestore, report.hostId, aiPreferencesFromEdit({
|
|
225
|
+
question: String((_asked_question = asked['question']) != null ? _asked_question : ''),
|
|
226
|
+
opCounts: report.opCounts
|
|
227
|
+
})).catch((error)=>console.warn('ai site memory unwritten', {
|
|
228
|
+
hostId: report.hostId,
|
|
229
|
+
error
|
|
230
|
+
}));
|
|
218
231
|
return Response.json({
|
|
219
232
|
ok: true
|
|
220
233
|
}, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/assist-edit-applied.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n permissionRefusal,\n checkRateLimit,\n emailUnverifiedResponse,\n firebaseAdmin,\n getOrgForUser,\n isImpersonationSession,\n isServerReleaseFlagOnForOrg,\n lockdownRefusal,\n memberHasPermissionOnHost,\n rateLimitHeaders,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { logAiEditApplied } from '../activity/ai-activity'\nimport {\n ASSIST_EDIT_DOCUMENT_KINDS,\n ASSIST_EDIT_OP_COUNT_WORDS,\n assistEditDocumentOf,\n type AssistEditAppliedReport,\n type AssistEditDocumentKind,\n} from '../model/assist-edit'\nimport { sanitiseId } from './assist-view-context'\n\n/**\n * The applied-edit record (AGL-2906, AGL-2929): the panel reports that the\n * author applied a proposal in their editor, and this door writes the\n * `ai.edit.applied` row to the site's activity log, attributed to them.\n *\n * The edits were applied in the author's own editor, as unsaved changes;\n * nothing here can see them or repeat them, and nothing here writes a\n * document. What this door checks is that the report stands on a proposal\n * the server issued: the exchange exists under the named org, the same\n * member asked it about the same site, and its signal records edit\n * operations proposed from the besigner route of the same document. A\n * report that fails any of those writes nothing. The counts are held to the\n * operation words and to what that turn proposed, and a second report for\n * one exchange writes no second row.\n *\n * The ladder: 405 → 401 → 400 → 403 email unverified → 403 not a member →\n * 403 the role lacks `ai.generate` (staff pass) → 404 `release_ai_generative`\n * off (staff preview) → 423 lockdown → 429 rate → 404 no such exchange of\n * the caller's → 409 the exchange proposed no such edit. No reservation and\n * no provider: a record of an act costs no credits.\n */\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** The report, held to its own alphabets; null when anything is missing or malformed. */\nexport function parseEditAppliedReport(payload: unknown): AssistEditAppliedReport | null {\n const body = isRecord(payload) ? payload : {}\n // Ids are document path segments here, so a slash is refused rather than\n // followed into a subcollection.\n const orgId = sanitiseId(String(body['orgId'] ?? ''))\n const hostId = sanitiseId(String(body['hostId'] ?? ''))\n const exchangeId = sanitiseId(String(body['exchangeId'] ?? ''))\n const documentId = sanitiseId(String(body['documentId'] ?? ''))\n const versionId = sanitiseId(String(body['versionId'] ?? ''))\n const documentKind = body['documentKind'] as AssistEditDocumentKind\n if (!orgId || !hostId || !exchangeId || !documentId || !versionId) return null\n if (!ASSIST_EDIT_DOCUMENT_KINDS.includes(documentKind)) return null\n const opCounts: Record<string, number> = {}\n for (const [word, count] of Object.entries(isRecord(body['opCounts']) ? body['opCounts'] : {})) {\n if (!ASSIST_EDIT_OP_COUNT_WORDS.includes(word)) continue\n const parsed = Number(count)\n if (Number.isInteger(parsed) && parsed > 0) opCounts[word] = parsed\n }\n if (!Object.keys(opCounts).length) return null\n return { orgId, hostId, exchangeId, documentKind, documentId, versionId, opCounts }\n}\n\ntype RecordVerdict = 'recorded' | 'duplicate' | 'unknown' | 'no-proposal' | 'too-many'\n\nasync function handler(request: Request): Promise<Response> {\n if (request.method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = request.headers.get('authorization') ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n let payload: unknown\n try {\n payload = await request.json()\n } catch {\n payload = null\n }\n const report = parseEditAppliedReport(payload)\n if (!report) {\n return Response.json({ error: 'Missing or malformed report' }, { status: 400 })\n }\n\n try {\n const app = firebaseAdmin.app()\n const decoded = await app.auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const staff = decoded['staff'] === true\n const resolved = await getOrgForUser(decoded.uid, report.orgId)\n if (!resolved || resolved.orgId !== report.orgId) {\n return Response.json(\n { error: 'You are not a member of that organization' },\n { status: 403 },\n )\n }\n if (\n !staff &&\n !(await memberHasPermissionOnHost(report.orgId, report.hostId, resolved.member, 'ai.generate'))\n ) {\n return permissionRefusal('ai.generate')\n }\n if (!staff && !(await isServerReleaseFlagOnForOrg('release_ai_generative', report.orgId))) {\n return Response.json({ error: 'Not found' }, { status: 404 })\n }\n const locked = await lockdownRefusal({\n request,\n staff,\n uid: decoded.uid,\n org: (resolved.org ?? {}) as Record<string, unknown>,\n })\n if (locked) return locked\n const rate = checkRateLimit(`assist-edit-applied:${decoded.uid}`, {\n limit: 30,\n windowMs: 60_000,\n })\n if (!rate.allowed) {\n return Response.json(\n { error: 'Too many requests — slow down a moment', reason: 'rate' },\n { status: 429, headers: rateLimitHeaders(rate) },\n )\n }\n\n const firestore = app.firestore()\n const orgRef = firestore.collection('orgs').doc(report.orgId)\n const exchange = await orgRef.collection('assistExchanges').doc(report.exchangeId).get()\n const asked = exchange.exists ? (exchange.data() as Record<string, unknown> | undefined) : undefined\n if (!asked || asked['uid'] !== decoded.uid || (asked['hostId'] ?? null) !== report.hostId) {\n return Response.json({ error: 'Unknown exchange' }, { status: 404 })\n }\n\n const signalRef = orgRef.collection('assistSignals').doc(report.exchangeId)\n const verdict = await firestore.runTransaction(async (tx): Promise<RecordVerdict> => {\n const signal = await tx.get(signalRef)\n const data = signal.exists ? (signal.data() as Record<string, unknown> | undefined) : undefined\n if (!data) return 'unknown'\n const proposed = Number(data['editOps'] ?? 0)\n const document = assistEditDocumentOf(String(data['route'] ?? ''))\n if (\n !(proposed > 0) ||\n !document ||\n document.kind !== report.documentKind ||\n document.documentId !== report.documentId\n ) {\n return 'no-proposal'\n }\n const applied = Object.values(report.opCounts).reduce((sum, count) => sum + count, 0)\n if (applied > proposed) return 'too-many'\n if (data['editAppliedAt']) return 'duplicate'\n tx.update(signalRef, { editAppliedAt: FieldValue.serverTimestamp() })\n return 'recorded'\n })\n\n switch (verdict) {\n case 'unknown':\n return Response.json({ error: 'Unknown exchange' }, { status: 404 })\n case 'no-proposal':\n return Response.json(\n { error: 'That exchange proposed no edit to this document' },\n { status: 409 },\n )\n case 'too-many':\n return Response.json(\n { error: 'More edits reported than that exchange proposed' },\n { status: 409 },\n )\n case 'duplicate':\n return Response.json({ ok: true, duplicate: true }, { status: 200 })\n case 'recorded':\n await logAiEditApplied(\n report.hostId,\n { uid: decoded.uid, email: decoded.email ?? null },\n {\n type: report.documentKind,\n id: report.documentId,\n versionId: report.versionId,\n opCounts: report.opCounts,\n },\n )\n return Response.json({ ok: true }, { status: 200 })\n }\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993).\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error(error)\n return Response.json({ error: 'The edit could not be recorded' }, { status: 500 })\n }\n}\n\nexport const dynamic = 'force-dynamic'\nexport { handler as POST }\n"],"names":["FieldValue","permissionRefusal","checkRateLimit","emailUnverifiedResponse","firebaseAdmin","getOrgForUser","isImpersonationSession","isServerReleaseFlagOnForOrg","lockdownRefusal","memberHasPermissionOnHost","rateLimitHeaders","invalidIdTokenResponse","logAiEditApplied","ASSIST_EDIT_DOCUMENT_KINDS","ASSIST_EDIT_OP_COUNT_WORDS","assistEditDocumentOf","sanitiseId","isRecord","value","Array","isArray","parseEditAppliedReport","payload","body","orgId","String","hostId","exchangeId","documentId","versionId","documentKind","includes","opCounts","word","count","Object","entries","parsed","Number","isInteger","keys","length","handler","request","method","Response","json","error","status","authorization","headers","get","idToken","startsWith","slice","undefined","report","resolved","asked","app","decoded","auth","verifyIdToken","email_verified","staff","uid","member","locked","org","rate","limit","windowMs","allowed","reason","firestore","orgRef","collection","doc","exchange","exists","data","signalRef","verdict","runTransaction","tx","signal","proposed","document","kind","applied","values","reduce","sum","update","editAppliedAt","serverTimestamp","ok","duplicate","email","type","id","unauthenticated","console","dynamic","POST"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/assist-edit-applied.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n permissionRefusal,\n checkRateLimit,\n emailUnverifiedResponse,\n firebaseAdmin,\n getOrgForUser,\n isImpersonationSession,\n isServerReleaseFlagOnForOrg,\n lockdownRefusal,\n memberHasPermissionOnHost,\n rateLimitHeaders,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { logAiEditApplied } from '../activity/ai-activity'\nimport {\n ASSIST_EDIT_DOCUMENT_KINDS,\n ASSIST_EDIT_OP_COUNT_WORDS,\n assistEditDocumentOf,\n type AssistEditAppliedReport,\n type AssistEditDocumentKind,\n} from '../model/assist-edit'\nimport { aiPreferencesFromEdit } from '../model/ai-site-memory'\nimport { rememberAiSitePreferences } from '../runtime/site-context'\nimport { sanitiseId } from './assist-view-context'\n\n/**\n * The applied-edit record (AGL-2906, AGL-2929): the panel reports that the\n * author applied a proposal in their editor, and this door writes the\n * `ai.edit.applied` row to the site's activity log, attributed to them.\n *\n * The edits were applied in the author's own editor, as unsaved changes;\n * nothing here can see them or repeat them, and nothing here writes the\n * edited document. (It does remember what the edit showed about the owner's\n * preferences, AGL-3661 — see `model/ai-site-memory.ts`.) What this door checks is that the report stands on a proposal\n * the server issued: the exchange exists under the named org, the same\n * member asked it about the same site, and its signal records edit\n * operations proposed from the besigner route of the same document. A\n * report that fails any of those writes nothing. The counts are held to the\n * operation words and to what that turn proposed, and a second report for\n * one exchange writes no second row.\n *\n * The ladder: 405 → 401 → 400 → 403 email unverified → 403 not a member →\n * 403 the role lacks `ai.generate` (staff pass) → 404 `release_ai_generative`\n * off (staff preview) → 423 lockdown → 429 rate → 404 no such exchange of\n * the caller's → 409 the exchange proposed no such edit. No reservation and\n * no provider: a record of an act costs no credits.\n */\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** The report, held to its own alphabets; null when anything is missing or malformed. */\nexport function parseEditAppliedReport(payload: unknown): AssistEditAppliedReport | null {\n const body = isRecord(payload) ? payload : {}\n // Ids are document path segments here, so a slash is refused rather than\n // followed into a subcollection.\n const orgId = sanitiseId(String(body['orgId'] ?? ''))\n const hostId = sanitiseId(String(body['hostId'] ?? ''))\n const exchangeId = sanitiseId(String(body['exchangeId'] ?? ''))\n const documentId = sanitiseId(String(body['documentId'] ?? ''))\n const versionId = sanitiseId(String(body['versionId'] ?? ''))\n const documentKind = body['documentKind'] as AssistEditDocumentKind\n if (!orgId || !hostId || !exchangeId || !documentId || !versionId) return null\n if (!ASSIST_EDIT_DOCUMENT_KINDS.includes(documentKind)) return null\n const opCounts: Record<string, number> = {}\n for (const [word, count] of Object.entries(isRecord(body['opCounts']) ? body['opCounts'] : {})) {\n if (!ASSIST_EDIT_OP_COUNT_WORDS.includes(word)) continue\n const parsed = Number(count)\n if (Number.isInteger(parsed) && parsed > 0) opCounts[word] = parsed\n }\n if (!Object.keys(opCounts).length) return null\n return { orgId, hostId, exchangeId, documentKind, documentId, versionId, opCounts }\n}\n\ntype RecordVerdict = 'recorded' | 'duplicate' | 'unknown' | 'no-proposal' | 'too-many'\n\nasync function handler(request: Request): Promise<Response> {\n if (request.method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = request.headers.get('authorization') ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n let payload: unknown\n try {\n payload = await request.json()\n } catch {\n payload = null\n }\n const report = parseEditAppliedReport(payload)\n if (!report) {\n return Response.json({ error: 'Missing or malformed report' }, { status: 400 })\n }\n\n try {\n const app = firebaseAdmin.app()\n const decoded = await app.auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const staff = decoded['staff'] === true\n const resolved = await getOrgForUser(decoded.uid, report.orgId)\n if (!resolved || resolved.orgId !== report.orgId) {\n return Response.json(\n { error: 'You are not a member of that organization' },\n { status: 403 },\n )\n }\n if (\n !staff &&\n !(await memberHasPermissionOnHost(report.orgId, report.hostId, resolved.member, 'ai.generate'))\n ) {\n return permissionRefusal('ai.generate')\n }\n if (!staff && !(await isServerReleaseFlagOnForOrg('release_ai_generative', report.orgId))) {\n return Response.json({ error: 'Not found' }, { status: 404 })\n }\n const locked = await lockdownRefusal({\n request,\n staff,\n uid: decoded.uid,\n org: (resolved.org ?? {}) as Record<string, unknown>,\n })\n if (locked) return locked\n const rate = checkRateLimit(`assist-edit-applied:${decoded.uid}`, {\n limit: 30,\n windowMs: 60_000,\n })\n if (!rate.allowed) {\n return Response.json(\n { error: 'Too many requests — slow down a moment', reason: 'rate' },\n { status: 429, headers: rateLimitHeaders(rate) },\n )\n }\n\n const firestore = app.firestore()\n const orgRef = firestore.collection('orgs').doc(report.orgId)\n const exchange = await orgRef.collection('assistExchanges').doc(report.exchangeId).get()\n const asked = exchange.exists ? (exchange.data() as Record<string, unknown> | undefined) : undefined\n if (!asked || asked['uid'] !== decoded.uid || (asked['hostId'] ?? null) !== report.hostId) {\n return Response.json({ error: 'Unknown exchange' }, { status: 404 })\n }\n\n const signalRef = orgRef.collection('assistSignals').doc(report.exchangeId)\n const verdict = await firestore.runTransaction(async (tx): Promise<RecordVerdict> => {\n const signal = await tx.get(signalRef)\n const data = signal.exists ? (signal.data() as Record<string, unknown> | undefined) : undefined\n if (!data) return 'unknown'\n const proposed = Number(data['editOps'] ?? 0)\n const document = assistEditDocumentOf(String(data['route'] ?? ''))\n if (\n !(proposed > 0) ||\n !document ||\n document.kind !== report.documentKind ||\n document.documentId !== report.documentId\n ) {\n return 'no-proposal'\n }\n const applied = Object.values(report.opCounts).reduce((sum, count) => sum + count, 0)\n if (applied > proposed) return 'too-many'\n if (data['editAppliedAt']) return 'duplicate'\n tx.update(signalRef, { editAppliedAt: FieldValue.serverTimestamp() })\n return 'recorded'\n })\n\n switch (verdict) {\n case 'unknown':\n return Response.json({ error: 'Unknown exchange' }, { status: 404 })\n case 'no-proposal':\n return Response.json(\n { error: 'That exchange proposed no edit to this document' },\n { status: 409 },\n )\n case 'too-many':\n return Response.json(\n { error: 'More edits reported than that exchange proposed' },\n { status: 409 },\n )\n case 'duplicate':\n return Response.json({ ok: true, duplicate: true }, { status: 200 })\n case 'recorded':\n await logAiEditApplied(\n report.hostId,\n { uid: decoded.uid, email: decoded.email ?? null },\n {\n type: report.documentKind,\n id: report.documentId,\n versionId: report.versionId,\n opCounts: report.opCounts,\n },\n )\n // What the applied edit showed about the owner's preferences\n // (AGL-3661), remembered for every later AI job on the site. Rules,\n // no model call; a failure never fails the record it follows.\n await rememberAiSitePreferences(\n firestore,\n report.hostId,\n aiPreferencesFromEdit({ question: String(asked['question'] ?? ''), opCounts: report.opCounts }),\n ).catch((error) => console.warn('ai site memory unwritten', { hostId: report.hostId, error }))\n return Response.json({ ok: true }, { status: 200 })\n }\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993).\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error(error)\n return Response.json({ error: 'The edit could not be recorded' }, { status: 500 })\n }\n}\n\nexport const dynamic = 'force-dynamic'\nexport { handler as POST }\n"],"names":["FieldValue","permissionRefusal","checkRateLimit","emailUnverifiedResponse","firebaseAdmin","getOrgForUser","isImpersonationSession","isServerReleaseFlagOnForOrg","lockdownRefusal","memberHasPermissionOnHost","rateLimitHeaders","invalidIdTokenResponse","logAiEditApplied","ASSIST_EDIT_DOCUMENT_KINDS","ASSIST_EDIT_OP_COUNT_WORDS","assistEditDocumentOf","aiPreferencesFromEdit","rememberAiSitePreferences","sanitiseId","isRecord","value","Array","isArray","parseEditAppliedReport","payload","body","orgId","String","hostId","exchangeId","documentId","versionId","documentKind","includes","opCounts","word","count","Object","entries","parsed","Number","isInteger","keys","length","handler","request","method","Response","json","error","status","authorization","headers","get","idToken","startsWith","slice","undefined","report","resolved","asked","app","decoded","auth","verifyIdToken","email_verified","staff","uid","member","locked","org","rate","limit","windowMs","allowed","reason","firestore","orgRef","collection","doc","exchange","exists","data","signalRef","verdict","runTransaction","tx","signal","proposed","document","kind","applied","values","reduce","sum","update","editAppliedAt","serverTimestamp","ok","duplicate","email","type","id","question","catch","console","warn","unauthenticated","dynamic","POST"],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import { type AssistEditDocumentKind } from '../model/assist-edit';
|
|
18
|
+
import { type AssistBuildDraft } from '../model/assist-follow-up';
|
|
18
19
|
/**
|
|
19
20
|
* Aglyn Assist level 2 — the GUIDE capability (AGL-1988, under AGL-1860).
|
|
20
21
|
*
|
|
@@ -293,6 +294,17 @@ export declare function visibleAssistText(raw: string): string;
|
|
|
293
294
|
export declare function finalAssistText(raw: string): string;
|
|
294
295
|
/** The raw JSON body between the fences, if the model emitted one. */
|
|
295
296
|
export declare function extractAssistAction(raw: string): string | null;
|
|
297
|
+
/**
|
|
298
|
+
* The navigation to a draft a build in this chat made (AGL-3616), offered
|
|
299
|
+
* beside the view's own when the request listed any. Its destination is the
|
|
300
|
+
* site's Pages list, where every draft waits: the panel, which holds the
|
|
301
|
+
* draft's own address, sends the person to that draft's Besigner instead and
|
|
302
|
+
* asks the request again there. The server never builds an address from a
|
|
303
|
+
* draft, so a ref cannot steer anyone anywhere the registry does not point.
|
|
304
|
+
*/
|
|
305
|
+
export declare const ASSIST_OPEN_DRAFT_ACTION: AssistNavigateAction;
|
|
306
|
+
/** The view with the open-draft navigation added, where the request listed drafts. */
|
|
307
|
+
export declare function withAssistOpenDraftAction(view: AssistView | null, drafts: readonly AssistBuildDraft[]): AssistView | null;
|
|
296
308
|
/** A proposal that survived validation — inert, and safe to show. */
|
|
297
309
|
export interface AssistProposal {
|
|
298
310
|
id: string;
|
|
@@ -307,6 +319,12 @@ export interface AssistProposal {
|
|
|
307
319
|
}>;
|
|
308
320
|
/** Whether `href` carries the values, or the card must ask the user to type them. */
|
|
309
321
|
prefill: boolean;
|
|
322
|
+
/**
|
|
323
|
+
* For the open-draft navigation (AGL-3616): the ref of the draft to open,
|
|
324
|
+
* one the request listed. The panel resolves it to the draft's Besigner
|
|
325
|
+
* and asks the request again there.
|
|
326
|
+
*/
|
|
327
|
+
draft?: AssistBuildDraft;
|
|
310
328
|
}
|
|
311
329
|
/**
|
|
312
330
|
* Turn a model-emitted action block into a proposal, or nothing.
|
|
@@ -326,7 +344,7 @@ export interface AssistProposal {
|
|
|
326
344
|
export declare function resolveAssistProposal(rawBlock: string | null, view: AssistView | null, scope: {
|
|
327
345
|
orgSlug: string;
|
|
328
346
|
hostId: string;
|
|
329
|
-
}): AssistProposal | null;
|
|
347
|
+
}, drafts?: readonly AssistBuildDraft[]): AssistProposal | null;
|
|
330
348
|
/**
|
|
331
349
|
* The write boundary, asserted over the live table rather than described in
|
|
332
350
|
* a comment. Called by the spec; exported so the check travels with the data
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
1
2
|
/**
|
|
2
3
|
* @license
|
|
3
4
|
* Copyright 2026 Aglyn LLC
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
* limitations under the License.
|
|
16
17
|
*/ import { resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
17
18
|
import { ASSIST_EDIT_ACTION_ID, ASSIST_EDIT_DOCUMENT_KINDS, ASSIST_EDIT_TOOL_NAME } from "../model/assist-edit.js";
|
|
19
|
+
import { ASSIST_OPEN_DRAFT_ACTION_ID, ASSIST_OPEN_DRAFT_PARAM } from "../model/assist-follow-up.js";
|
|
18
20
|
/**
|
|
19
21
|
* Destinations verified to read `assist_*` search params. EMPTY on purpose:
|
|
20
22
|
* `assertInertActions()` fails any action with `prefill: true` whose route is
|
|
@@ -773,6 +775,25 @@ const isNavigateAction = (action)=>action.kind !== 'edit';
|
|
|
773
775
|
const body = (end < 0 ? raw.slice(bodyStart) : raw.slice(bodyStart, end)).trim();
|
|
774
776
|
return body || null;
|
|
775
777
|
}
|
|
778
|
+
/**
|
|
779
|
+
* The navigation to a draft a build in this chat made (AGL-3616), offered
|
|
780
|
+
* beside the view's own when the request listed any. Its destination is the
|
|
781
|
+
* site's Pages list, where every draft waits: the panel, which holds the
|
|
782
|
+
* draft's own address, sends the person to that draft's Besigner instead and
|
|
783
|
+
* asks the request again there. The server never builds an address from a
|
|
784
|
+
* draft, so a ref cannot steer anyone anywhere the registry does not point.
|
|
785
|
+
*/ export const ASSIST_OPEN_DRAFT_ACTION = orgAction(ASSIST_OPEN_DRAFT_ACTION_ID, 'Open the draft in the Besigner', 'a draft this chat built, in the Besigner, where your request is asked again', '/[orgSlug]/hosts/[host]/screens', [
|
|
786
|
+
ASSIST_OPEN_DRAFT_PARAM
|
|
787
|
+
]);
|
|
788
|
+
/** The view with the open-draft navigation added, where the request listed drafts. */ export function withAssistOpenDraftAction(view, drafts) {
|
|
789
|
+
if (!view || !drafts.length) return view;
|
|
790
|
+
return _extends({}, view, {
|
|
791
|
+
actions: [
|
|
792
|
+
...view.actions,
|
|
793
|
+
ASSIST_OPEN_DRAFT_ACTION
|
|
794
|
+
]
|
|
795
|
+
});
|
|
796
|
+
}
|
|
776
797
|
const MAX_PARAM_VALUE_CHARS = 200;
|
|
777
798
|
/**
|
|
778
799
|
* Turn a model-emitted action block into a proposal, or nothing.
|
|
@@ -788,7 +809,7 @@ const MAX_PARAM_VALUE_CHARS = 200;
|
|
|
788
809
|
*
|
|
789
810
|
* Anything unexpected returns null. A dropped proposal costs the user a
|
|
790
811
|
* button; an honoured bad one costs them trust.
|
|
791
|
-
*/ export function resolveAssistProposal(rawBlock, view, scope) {
|
|
812
|
+
*/ export function resolveAssistProposal(rawBlock, view, scope, drafts = []) {
|
|
792
813
|
var _parsed_id, _parsed_params;
|
|
793
814
|
if (!rawBlock || !view) return null;
|
|
794
815
|
const navigations = view.actions.filter(isNavigateAction);
|
|
@@ -817,6 +838,22 @@ const MAX_PARAM_VALUE_CHARS = 200;
|
|
|
817
838
|
}
|
|
818
839
|
const href = buildActionHref(action, scope, values);
|
|
819
840
|
if (!href) return null;
|
|
841
|
+
// The open-draft navigation names a draft the request listed, or nothing.
|
|
842
|
+
if (action.id === ASSIST_OPEN_DRAFT_ACTION_ID) {
|
|
843
|
+
var _values_find;
|
|
844
|
+
const ref = (_values_find = values.find((value)=>value.name === ASSIST_OPEN_DRAFT_PARAM)) == null ? void 0 : _values_find.value;
|
|
845
|
+
const draft = drafts.find((one)=>one.ref === ref);
|
|
846
|
+
if (!draft) return null;
|
|
847
|
+
return {
|
|
848
|
+
id: action.id,
|
|
849
|
+
label: `Open “${draft.label}” in the Besigner`,
|
|
850
|
+
outcome: `the ${draft.noun} “${draft.label}” in the Besigner, where your request is asked again`,
|
|
851
|
+
href,
|
|
852
|
+
values: [],
|
|
853
|
+
prefill: false,
|
|
854
|
+
draft
|
|
855
|
+
};
|
|
856
|
+
}
|
|
820
857
|
return {
|
|
821
858
|
id: action.id,
|
|
822
859
|
label: action.label,
|