@aglyn/tenant-data-admin 1.0.0-beta.229 → 1.0.0-beta.230

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/tenant-data-admin",
3
- "version": "1.0.0-beta.229",
3
+ "version": "1.0.0-beta.230",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,12 +25,12 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.229",
29
- "@aglyn/shared-util-email": "1.0.0-beta.229",
30
- "@aglyn/shared-util-fbserver": "1.0.0-beta.229",
31
- "@aglyn/shared-util-first-touch": "1.0.0-beta.229",
32
- "@aglyn/shared-util-http": "1.0.0-beta.229",
33
- "@aglyn/shared-util-tools": "1.0.0-beta.229",
28
+ "@aglyn/aglyn": "1.0.0-beta.230",
29
+ "@aglyn/shared-util-email": "1.0.0-beta.230",
30
+ "@aglyn/shared-util-fbserver": "1.0.0-beta.230",
31
+ "@aglyn/shared-util-first-touch": "1.0.0-beta.230",
32
+ "@aglyn/shared-util-http": "1.0.0-beta.230",
33
+ "@aglyn/shared-util-tools": "1.0.0-beta.230",
34
34
  "@msgpack/msgpack": "^3.1.3",
35
35
  "@swc/helpers": "0.5.23",
36
36
  "sharp": "^0.35.5",
@@ -131,7 +131,10 @@ import firebaseAdmin from "./firebase-admin.js";
131
131
  'kind'
132
132
  ],
133
133
  versionParentField: 'componentId',
134
- entitlement: 'reusableComponents'
134
+ // Counted only: a reusable component is not behind
135
+ // `reusableComponents`, the same as its create at `/api/hosts/resources`
136
+ // (AGL-3615). Free 1, every paid plan unlimited.
137
+ quotaKey: 'componentsPerHost'
135
138
  },
136
139
  layout: {
137
140
  collection: 'layouts',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/duplicate-resource.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\n/**\n * Duplicating a site resource whole, as a draft (AGL-2936).\n *\n * One module for every kind that lives under a site — screens and email\n * designs, reusable components, layouts, templates and forms, and the kinds\n * a plugin declares a copy for (a workflow) — so the console route, the AI\n * runtime's `duplicate_resource` tool and any later door make the same copy:\n * the same fields, the same uniqueness\n * rules, the same band arithmetic, the same activity rows. A caller that\n * has verified who is asking and that they may write the site hands over\n * the ids and gets back the copy's id, or a refusal it can answer with.\n *\n * WHAT MAKES THE COPY A DRAFT is different per kind, and each is the\n * omission of exactly one thing the create paths would otherwise do:\n *\n * - a screen is reachable only through the host's routing map, so the\n * copy gets no entry there — it has a slug of its own (`-copy`) but no\n * address until somebody publishes it;\n * - a collection's entry template renders only for the collection that\n * points at it, and nothing points at the copy;\n * - a layout, component or form is served only where a page places it,\n * and nothing places the copy;\n * - a declared kind drops what its plugin says a copy must not carry —\n * a workflow's trigger is cleared, so the copy runs nothing;\n * - a template is inert until it is used.\n *\n * COUNTED LIKE A CREATE. The copy is one more document in a collection the\n * plan bands, so it meets the same counter the create route meets — for a\n * screen that is not a page, the flat platform ceiling rather than a band —\n * and meets it INSIDE the transaction that writes the copy (AGL-2231): a count\n * taken before the write is a count N concurrent copies all pass. The\n * siblings are read once and answer three questions — the count, the name\n * the copy may take, and the slug — because three reads of one collection\n * is the reason a route and its readout come to disagree.\n *\n * THE VERSION TRAVELS WITH THE DOCUMENT. A besigner resource is opened by\n * its version, so the copy of a screen with no version is a screen nobody\n * can open. The source's most recently saved version is copied into the\n * copy's own `versions` as its first — no `versioning` entitlement is\n * owed for a first version — with its stored node bytes carried across\n * untouched: they are msgpack at rest, and re-encoding them is the\n * double-encode `encodeStoredNodes` exists to avoid.\n *\n * THE ONE TREE THAT IS REWRITTEN is a form's (AGL-3024). A form's design\n * names its own form inside itself, so carrying those bytes across hands the\n * copy a design that files every submission under the SOURCE — silently,\n * because the copy still renders and the row still lands. That binding is\n * moved onto the copy below. No other kind embeds its own id in its design,\n * so no other kind is decoded.\n */\n\n// The leaf module, not the server barrel: specs stage that barrel as a\n// closed world, and the list keys every create stamps (AGL-3321) are\n// nothing they have reason to name.\nimport { artifactCreateListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport {\n billableScreenIds,\n checkEntitlement,\n checkQuota,\n claimAttempt,\n createResourceUid,\n decodeStoredNodes,\n encodeStoredNodes,\n newFormListFields,\n NON_PAGE_SCREEN_MAX_PER_HOST,\n nonPageScreenIds,\n SCREEN_KIND_EMAIL,\n SCREEN_KIND_TEMPLATE,\n screenClaimsToBeAPage,\n type BillableScreenSource,\n type OrgEntitlements,\n type OrgFeatureFlags,\n} from '@aglyn/aglyn/server'\nimport {\n DUPLICABLE_RESOURCE_NOUNS,\n DUPLICATE_BUSY_MESSAGE,\n duplicateDisplayName,\n duplicateVersionNote,\n uniqueDuplicateName,\n uniqueDuplicateSlug,\n type DuplicableHostResourceKind,\n} from '@aglyn/aglyn/app-utils/duplicate-resource'\nimport { formDesignReboundTo } from '@aglyn/aglyn/app-utils/forms'\nimport { pluginHostResource } from '@aglyn/aglyn/plugin-manager/plugin-host-resources'\nimport { Timestamp } from 'firebase-admin/firestore'\nimport { logResourceDuplicated } from './duplicate-activity'\nimport firebaseAdmin from './firebase-admin'\n\n/** How the copy of each kind is made. */\ninterface KindRecipe {\n collection: string\n /** The field the name lives in — `displayName` on core's kinds; a declared kind names its own. */\n nameField: string\n /** The source fields carried onto the copy; everything else stays behind. */\n fields: readonly string[]\n /** The back-pointer a copied version carries, when the kind versions at all. */\n versionParentField?: 'screenId' | 'layoutId' | 'componentId' | 'formId'\n /** The plan counter the copy is charged to. */\n quotaKey?: keyof OrgEntitlements & string\n /** The feature the plan must include for the kind to exist at all. */\n entitlement?: keyof OrgFeatureFlags\n /** Whether a `slug` on the source becomes a unique `-copy` slug. */\n slug?: boolean\n /** What the copy carries beyond the source's fields. */\n stamps?: Readonly<Record<string, unknown>>\n}\n\n/**\n * Per kind, the same fields the create route's allow-list admits, less the\n * ones a copy must not inherit: a screen's `versionId` (the copy mints its\n * own), a template's `source` (stamped `authored` below — a copy of a\n * marketplace template is the customer's own), and anything the server\n * stamps on every create.\n *\n * The platform's own kinds only. A kind a plugin owns — a workflow, whose\n * copy clears its trigger so it runs nothing until armed — is declared by\n * that plugin beside its collection in `plugins.config.json` and read by\n * {@link recipeFor} (AGL-3080), compiled so a copy never depends on this\n * process having loaded the plugin.\n */\nconst RECIPES: Partial<Record<DuplicableHostResourceKind, KindRecipe>> = {\n screen: {\n collection: 'screens',\n nameField: 'displayName',\n fields: [\n 'description',\n 'seo',\n 'kind',\n 'visibility',\n 'locale',\n 'protection',\n // The shared-layout binding. A version inherits it from its screen, so\n // a copy without it renders with no header or footer (AGL-3120).\n 'layoutId',\n ],\n versionParentField: 'screenId',\n quotaKey: 'screensPerHost',\n slug: true,\n },\n // No `quotaKey`: an email design is not a page, so it meets the flat\n // platform ceiling on non-page screens below rather than the page band.\n emailDesign: {\n collection: 'screens',\n nameField: 'displayName',\n fields: ['description', 'kind'],\n versionParentField: 'screenId',\n },\n component: {\n collection: 'components',\n nameField: 'displayName',\n // `kind` keeps an email block an email block (AGL-3287): without it the\n // copy reads as a page component, leaves every email editor's palette,\n // and turns up in page editors where its email blocks cannot render.\n fields: ['description', 'icon', 'rootId', 'nodes', 'props', 'kind'],\n versionParentField: 'componentId',\n entitlement: 'reusableComponents',\n },\n layout: {\n collection: 'layouts',\n nameField: 'displayName',\n fields: ['description'],\n versionParentField: 'layoutId',\n quotaKey: 'sharedLayoutsPerHost',\n },\n template: {\n collection: 'templates',\n nameField: 'displayName',\n fields: [\n 'kind',\n // A component template's own kind — email block or page component —\n // so Use template on a copy still makes the right one (AGL-3287).\n 'componentKind',\n 'description',\n 'category',\n 'placeholders',\n 'nodes',\n 'rootId',\n 'props',\n 'slug',\n 'seo',\n 'theme',\n ],\n quotaKey: 'templatesPerHost',\n stamps: { source: { type: 'authored' } },\n },\n form: {\n collection: 'forms',\n nameField: 'displayName',\n fields: [\n 'fields',\n 'consentFieldName',\n 'routing',\n 'legacyMatch',\n 'rootId',\n 'nodes',\n ],\n versionParentField: 'formId',\n // Counted only: a saved form is not behind `reusableComponents`, the same\n // as its create at `/api/hosts/resources` (AGL-3597).\n quotaKey: 'formsPerHost',\n slug: true,\n },\n}\n\n/**\n * The recipe for a kind: core's own, else the one its plugin declared, else\n * `null` — which is refused, never guessed at. A declared copy is met against\n * the same plan counter and feature as the kind's create.\n */\nexport function recipeFor(kind: string): KindRecipe | null {\n if (Object.prototype.hasOwnProperty.call(RECIPES, kind)) {\n return RECIPES[kind as DuplicableHostResourceKind] ?? null\n }\n const declared = pluginHostResource(kind)\n if (!declared?.duplicate) return null\n return {\n collection: declared.collection,\n nameField: declared.duplicate.nameField,\n fields: declared.duplicate.fields,\n ...(declared.quotaKey\n ? { quotaKey: declared.quotaKey as keyof OrgEntitlements & string }\n : {}),\n ...(declared.entitlement\n ? { entitlement: declared.entitlement as keyof OrgFeatureFlags }\n : {}),\n ...(declared.duplicate.stamps ? { stamps: declared.duplicate.stamps } : {}),\n }\n}\n\nexport interface DuplicateResourceOptions {\n orgId: string\n hostId: string\n sourceId: string\n /** The name the copy takes; blank means `Copy of <source>`. */\n name?: string | null\n /** The verified person making the copy. */\n uid: string\n email?: string | null\n /**\n * The owning org's document, when the caller already holds it; read\n * here otherwise. The bands are resolved from it.\n */\n org?: Record<string, unknown> | null\n /**\n * A client-minted key for this attempt, so a double click makes one copy\n * and a retry of a finished attempt answers what the first did. A caller\n * with no client behind it — the AI runtime — sends none and is not\n * claimed.\n */\n attemptKey?: string | null\n}\n\nexport type DuplicateResourceResult =\n | {\n ok: true\n id: string\n /** The copy's first version, for the kinds that version. */\n versionId: string | null\n name: string\n }\n | { ok: false; status: number; error: string }\n\n/** What a sibling contributes to the three answers the siblings give. */\ninterface SiblingRow {\n id: string\n name: string\n slug: string\n kind: unknown\n deletedAt: unknown\n sourceType: unknown\n}\n\nconst asRecord = (value: unknown): Record<string, unknown> =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {}\n\n/**\n * Makes the copy. The caller has authenticated `uid` and verified that they\n * may write content on `hostId`; this decides everything after that.\n */\nexport async function duplicateResource(\n kind: DuplicableHostResourceKind,\n options: DuplicateResourceOptions,\n): Promise<DuplicateResourceResult> {\n const recipe = recipeFor(kind)\n const noun = DUPLICABLE_RESOURCE_NOUNS[kind]\n if (!recipe) return { ok: false, status: 400, error: 'That resource cannot be duplicated' }\n const sourceId = String(options.sourceId ?? '').trim().slice(0, 64)\n if (!sourceId) return { ok: false, status: 400, error: `Missing the ${noun} to copy` }\n const firestore = firebaseAdmin.app().firestore()\n\n const attemptKey = String(options.attemptKey ?? '').trim().slice(0, 200)\n const claimed = attemptKey\n ? await claimAttempt(firestore as never, {\n kind: `duplicate-${kind}`,\n scopeId: `${options.hostId}:${sourceId}`,\n orgId: options.orgId,\n key: attemptKey,\n busyMessage: DUPLICATE_BUSY_MESSAGE,\n })\n : null\n if (claimed && 'replay' in claimed) {\n return claimed.replay.body as DuplicateResourceResult\n }\n const claim = claimed && 'claim' in claimed ? claimed.claim : null\n\n try {\n const result = await copy(firestore, kind, recipe, { ...options, sourceId })\n if (result.ok) {\n await logResourceDuplicated(\n kind,\n { uid: options.uid, email: options.email ?? null },\n {\n orgId: options.orgId,\n hostId: options.hostId,\n source: { id: sourceId, name: result.sourceName },\n target: { id: result.id, name: result.name, versionId: result.versionId },\n },\n )\n const answer: DuplicateResourceResult = {\n ok: true,\n id: result.id,\n versionId: result.versionId,\n name: result.name,\n }\n await claim?.record(200, answer)\n return answer\n }\n // A refusal releases the key: the person fixes the cause — frees a slot,\n // picks another name — and presses the same button again.\n await claim?.release()\n return result\n } catch (error) {\n await claim?.release()\n throw error\n }\n}\n\ntype CopyOutcome =\n | { ok: true; id: string; versionId: string | null; name: string; sourceName: string }\n | { ok: false; status: number; error: string }\n\nasync function copy(\n firestore: FirebaseFirestore.Firestore,\n kind: DuplicableHostResourceKind,\n recipe: KindRecipe,\n options: DuplicateResourceOptions & { sourceId: string },\n): Promise<CopyOutcome> {\n const { hostId, sourceId, uid } = options\n const noun = DUPLICABLE_RESOURCE_NOUNS[kind]\n const hostRef = firestore.collection('hosts').doc(hostId)\n const collectionRef = hostRef.collection(recipe.collection)\n const sourceRef = collectionRef.doc(sourceId)\n const org =\n options.org ??\n (((await firestore.collection('orgs').doc(options.orgId).get()).data() ??\n null) as Record<string, unknown> | null)\n\n if (recipe.entitlement && !checkEntitlement(org as never, recipe.entitlement)) {\n return {\n ok: false,\n status: 403,\n error: 'This feature is not included in your plan — see Billing',\n }\n }\n\n const id = createResourceUid()\n const versionId = recipe.versionParentField ? createResourceUid() : null\n\n return firestore.runTransaction(async (tx): Promise<CopyOutcome> => {\n // ALL READS BEFORE ANY WRITE, which Firestore requires. The host is read\n // inside the transaction rather than passed in because its routing map\n // is an input to the screen count and is ordinary client-writable.\n const siblingFields = [\n recipe.nameField,\n 'slug',\n 'kind',\n 'deletedAt',\n 'source.type',\n ]\n const [hostSnapshot, source, siblingsSnapshot] = await Promise.all([\n tx.get(hostRef),\n tx.get(sourceRef),\n tx.get(collectionRef.select(...siblingFields)),\n ])\n if (!hostSnapshot.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (!source.exists || source.get('deletedAt') != null) {\n return { ok: false, status: 404, error: `Unknown ${noun}` }\n }\n const data = asRecord(source.data())\n\n // The two screen kinds share a collection and are told apart by `kind`:\n // an email design copied as a page would land on the Screens list, and\n // the other way round would put a page on the Emails page.\n //\n // Of the screens that are not pages, only a collection's entry template\n // may be copied as a screen (AGL-3102), and the copy stays a template:\n // `kind` rides across with the recipe's fields, so it composes nothing\n // until a collection points at it — which is how a second collection\n // starts from the first one's design. An error screen is refused:\n // `kind: 'error'` is the slot assignment's to stamp, because that write\n // is what bounds how many a site holds, and a copy would stamp one\n // outside it.\n const sourceKind = data['kind']\n const nonPageScreen =\n recipe.collection === 'screens' &&\n !screenClaimsToBeAPage({ kind: sourceKind as string })\n if (recipe.collection === 'screens') {\n if (kind === 'emailDesign' && sourceKind !== SCREEN_KIND_EMAIL) {\n return { ok: false, status: 400, error: 'That page is not an email design' }\n }\n if (kind === 'screen' && nonPageScreen && sourceKind !== SCREEN_KIND_TEMPLATE) {\n return {\n ok: false,\n status: 400,\n error: 'Only a page or a collection entry template can be duplicated as a page',\n }\n }\n }\n\n const siblings: SiblingRow[] = siblingsSnapshot.docs.map((row) => ({\n id: row.id,\n name: String(row.get(recipe.nameField) ?? ''),\n slug: String(row.get('slug') ?? ''),\n kind: row.get('kind'),\n deletedAt: row.get('deletedAt'),\n sourceType: row.get('source.type'),\n }))\n const live = siblings.filter((row) => row.deletedAt == null)\n\n // THE BAND, met inside the transaction (AGL-2231) with the create route's\n // own arithmetic: screens count what the routing map makes billable,\n // templates exclude the platform's starters, everything else counts its\n // live documents. A screen copy that is not a page — an email design or\n // an entry template — meets the flat platform ceiling on non-page screens\n // instead, the one an email design's create meets and a demoted template\n // fills, and never the page band: it spends none of it.\n const screenRows: BillableScreenSource[] = siblings.map((row) => ({\n id: row.id,\n kind: row.kind,\n deletedAt: row.deletedAt,\n }))\n const routingMap = hostSnapshot.get('screens') as never\n if (nonPageScreen) {\n if (nonPageScreenIds(screenRows, routingMap).size >= NON_PAGE_SCREEN_MAX_PER_HOST) {\n return {\n ok: false,\n status: 403,\n error:\n 'This site is at its limit of ' +\n `${NON_PAGE_SCREEN_MAX_PER_HOST} email designs and template pages — ` +\n 'delete some to make room',\n }\n }\n } else if (recipe.quotaKey) {\n const used =\n kind === 'screen'\n ? billableScreenIds(screenRows, routingMap).size\n : kind === 'template'\n ? live.filter((row) => row.sourceType !== 'starter').length\n : live.length\n const quota = checkQuota(org as never, recipe.quotaKey as never, used)\n if (!quota.allowed) {\n return {\n ok: false,\n status: 403,\n error:\n `Your plan includes ${quota.limit} ${noun}${quota.limit === 1 ? '' : 's'} — ` +\n 'upgrade in Billing for more',\n }\n }\n }\n\n // THE NAME AND THE SLUG, unique among the live siblings. A deleted\n // sibling's name is free: a person who deleted \"Home\" and copies\n // \"About\" as \"Home\" is not confused by a tombstone.\n const sourceName = String(data[recipe.nameField] ?? '').trim()\n const requested = String(options.name ?? '').trim() || duplicateDisplayName(sourceName)\n const name = uniqueDuplicateName(\n requested,\n live.map((row) => row.name),\n )\n const slug = recipe.slug\n ? uniqueDuplicateSlug(\n data['slug'] as string | undefined,\n live.map((row) => row.slug),\n )\n : undefined\n\n // THE VERSION: the most recently saved one, else the one the document\n // points at. Read inside the transaction so the copy is of what was\n // saved when the count was taken.\n let versionData: Record<string, unknown> | null = null\n let sourceVersionNumber: number | null = null\n if (recipe.versionParentField) {\n const versionsRef = sourceRef.collection('versions')\n const [newest, all] = await Promise.all([\n tx.get(versionsRef.orderBy('updatedAt', 'desc').limit(1)),\n tx.get(versionsRef.select()),\n ])\n let found: FirebaseFirestore.DocumentSnapshot | null = newest.docs[0] ?? null\n const pointer = String(data['versionId'] ?? '')\n if (!found && pointer) {\n const pointed = await tx.get(versionsRef.doc(pointer))\n if (pointed.exists) found = pointed\n }\n if (found) {\n versionData = asRecord(found.data())\n sourceVersionNumber = all.size\n }\n }\n\n // THE DOCUMENT: the recipe's fields, the name, the slug, the stamps.\n const doc: Record<string, unknown> = {}\n for (const field of recipe.fields) {\n if (data[field] !== undefined) doc[field] = data[field]\n }\n doc[recipe.nameField] = name\n if (recipe.slug) {\n if (slug) doc['slug'] = slug\n else delete doc['slug']\n }\n /*\n * THE FORM'S BINDING, moved onto the copy (AGL-3024).\n *\n * A form's design names its own form in a prop, and that name is what\n * `/api/forms/submit` files a submission under. Copied verbatim it still\n * names the SOURCE, so the copy renders, collects, and files everything\n * it collects under the form it was copied from — while its own list\n * stays empty and nothing anywhere reports an error. `checkFormContract`\n * is what the console's form page banners it with; `formDesignReboundTo`\n * is the write side of the same fact.\n *\n * This is the one place a copied tree is not carried across untouched,\n * and the exception is narrow on purpose: no other kind here embeds its\n * own id in its design. `formDesignReboundTo` answers `null` when there\n * is nothing to move, so every other kind — and a form whose design is\n * already correct — pays nothing for this.\n */\n const rebound = (raw: unknown): unknown => {\n if (kind !== 'form') return raw\n return (\n formDesignReboundTo(decodeStoredNodes(raw), {\n formId: id,\n formName: name,\n }) ?? raw\n )\n }\n // The stored tree is msgpack at rest; `encodeStoredNodes` passes encoded\n // bytes through and packs a plain map, so a legacy plain source lands\n // compressed like every create does — and so does a rebound one, which\n // comes back decoded.\n const packed = encodeStoredNodes(rebound(doc['nodes']))\n if (packed) doc['nodes'] = Buffer.from(packed)\n tx.create(collectionRef.doc(id), {\n ...doc,\n ...(recipe.stamps ?? {}),\n // The keys the copy's list finds it by (AGL-3321), from the copy's own\n // name and stamps — see `artifactCreateListKeys`.\n ...artifactCreateListKeys(recipe.collection, { ...doc, ...(recipe.stamps ?? {}) }),\n // A copied form starts as a new one does, with its own search keys and\n // nothing counted (AGL-3330); only the routing it copied is kept.\n ...(kind === 'form'\n ? newFormListFields({\n id,\n displayName: name,\n slug: doc['slug'],\n routing: doc['routing'] as Parameters<typeof newFormListFields>[0]['routing'],\n })\n : {}),\n ...(versionData && versionId ? { versionId } : {}),\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n createdBy: uid,\n })\n if (versionData && versionId && recipe.versionParentField) {\n const versionNodes = encodeStoredNodes(rebound(versionData['nodes']))\n tx.create(collectionRef.doc(id).collection('versions').doc(versionId), {\n ...versionData,\n ...(versionNodes ? { nodes: Buffer.from(versionNodes) } : {}),\n [recipe.versionParentField]: id,\n hostId,\n displayName: duplicateVersionNote(sourceName, sourceVersionNumber),\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n createdBy: uid,\n })\n }\n return {\n ok: true,\n id,\n versionId: versionData && versionId ? versionId : null,\n name,\n sourceName,\n }\n })\n}\n"],"names":["artifactCreateListKeys","billableScreenIds","checkEntitlement","checkQuota","claimAttempt","createResourceUid","decodeStoredNodes","encodeStoredNodes","newFormListFields","NON_PAGE_SCREEN_MAX_PER_HOST","nonPageScreenIds","SCREEN_KIND_EMAIL","SCREEN_KIND_TEMPLATE","screenClaimsToBeAPage","DUPLICABLE_RESOURCE_NOUNS","DUPLICATE_BUSY_MESSAGE","duplicateDisplayName","duplicateVersionNote","uniqueDuplicateName","uniqueDuplicateSlug","formDesignReboundTo","pluginHostResource","Timestamp","logResourceDuplicated","firebaseAdmin","RECIPES","screen","collection","nameField","fields","versionParentField","quotaKey","slug","emailDesign","component","entitlement","layout","template","stamps","source","type","form","recipeFor","kind","Object","prototype","hasOwnProperty","call","declared","duplicate","asRecord","value","Array","isArray","duplicateResource","options","recipe","noun","ok","status","error","sourceId","String","trim","slice","firestore","app","attemptKey","claimed","scopeId","hostId","orgId","key","busyMessage","replay","body","claim","result","copy","uid","email","id","name","sourceName","target","versionId","answer","record","release","hostRef","doc","collectionRef","sourceRef","org","get","data","runTransaction","tx","siblingFields","hostSnapshot","siblingsSnapshot","Promise","all","select","exists","sourceKind","nonPageScreen","siblings","docs","map","row","deletedAt","sourceType","live","filter","screenRows","routingMap","size","used","length","quota","allowed","limit","requested","undefined","versionData","sourceVersionNumber","newest","versionsRef","orderBy","found","pointer","pointed","field","rebound","raw","formId","formName","packed","Buffer","from","create","displayName","routing","createdAt","now","updatedAt","createdBy","versionNodes","nodes"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiDC,GAED,uEAAuE;AACvE,qEAAqE;AACrE,oCAAoC;AACpC,SAASA,sBAAsB,QAAQ,4CAA2C;AAClF,SACEC,iBAAiB,EACjBC,gBAAgB,EAChBC,UAAU,EACVC,YAAY,EACZC,iBAAiB,EACjBC,iBAAiB,EACjBC,iBAAiB,EACjBC,iBAAiB,EACjBC,4BAA4B,EAC5BC,gBAAgB,EAChBC,iBAAiB,EACjBC,oBAAoB,EACpBC,qBAAqB,QAIhB,sBAAqB;AAC5B,SACEC,yBAAyB,EACzBC,sBAAsB,EACtBC,oBAAoB,EACpBC,oBAAoB,EACpBC,mBAAmB,EACnBC,mBAAmB,QAEd,4CAA2C;AAClD,SAASC,mBAAmB,QAAQ,+BAA8B;AAClE,SAASC,kBAAkB,QAAQ,oDAAmD;AACtF,SAASC,SAAS,QAAQ,2BAA0B;AACpD,SAASC,qBAAqB,QAAQ,0BAAsB;AAC5D,OAAOC,mBAAmB,sBAAkB;AAqB5C;;;;;;;;;;;;CAYC,GACD,MAAMC,UAAmE;IACvEC,QAAQ;QACNC,YAAY;QACZC,WAAW;QACXC,QAAQ;YACN;YACA;YACA;YACA;YACA;YACA;YACA,uEAAuE;YACvE,iEAAiE;YACjE;SACD;QACDC,oBAAoB;QACpBC,UAAU;QACVC,MAAM;IACR;IACA,qEAAqE;IACrE,wEAAwE;IACxEC,aAAa;QACXN,YAAY;QACZC,WAAW;QACXC,QAAQ;YAAC;YAAe;SAAO;QAC/BC,oBAAoB;IACtB;IACAI,WAAW;QACTP,YAAY;QACZC,WAAW;QACX,wEAAwE;QACxE,uEAAuE;QACvE,qEAAqE;QACrEC,QAAQ;YAAC;YAAe;YAAQ;YAAU;YAAS;YAAS;SAAO;QACnEC,oBAAoB;QACpBK,aAAa;IACf;IACAC,QAAQ;QACNT,YAAY;QACZC,WAAW;QACXC,QAAQ;YAAC;SAAc;QACvBC,oBAAoB;QACpBC,UAAU;IACZ;IACAM,UAAU;QACRV,YAAY;QACZC,WAAW;QACXC,QAAQ;YACN;YACA,oEAAoE;YACpE,kEAAkE;YAClE;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;QACDE,UAAU;QACVO,QAAQ;YAAEC,QAAQ;gBAAEC,MAAM;YAAW;QAAE;IACzC;IACAC,MAAM;QACJd,YAAY;QACZC,WAAW;QACXC,QAAQ;YACN;YACA;YACA;YACA;YACA;YACA;SACD;QACDC,oBAAoB;QACpB,0EAA0E;QAC1E,sDAAsD;QACtDC,UAAU;QACVC,MAAM;IACR;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASU,UAAUC,IAAY;IACpC,IAAIC,OAAOC,SAAS,CAACC,cAAc,CAACC,IAAI,CAACtB,SAASkB,OAAO;YAChDlB;QAAP,QAAOA,gBAAAA,OAAO,CAACkB,KAAmC,YAA3ClB,gBAA+C;IACxD;IACA,MAAMuB,WAAW3B,mBAAmBsB;IACpC,IAAI,EAACK,4BAAAA,SAAUC,SAAS,GAAE,OAAO;IACjC,OAAO;QACLtB,YAAYqB,SAASrB,UAAU;QAC/BC,WAAWoB,SAASC,SAAS,CAACrB,SAAS;QACvCC,QAAQmB,SAASC,SAAS,CAACpB,MAAM;OAC7BmB,SAASjB,QAAQ,GACjB;QAAEA,UAAUiB,SAASjB,QAAQ;IAAmC,IAChE,CAAC,GACDiB,SAASb,WAAW,GACpB;QAAEA,aAAaa,SAASb,WAAW;IAA0B,IAC7D,CAAC,GACDa,SAASC,SAAS,CAACX,MAAM,GAAG;QAAEA,QAAQU,SAASC,SAAS,CAACX,MAAM;IAAC,IAAI,CAAC;AAE7E;AA6CA,MAAMY,WAAW,CAACC,QAChBA,SAAS,OAAOA,UAAU,YAAY,CAACC,MAAMC,OAAO,CAACF,SAChDA,QACD,CAAC;AAEP;;;CAGC,GACD,OAAO,eAAeG,kBACpBX,IAAgC,EAChCY,OAAiC;QAKTA,mBAIEA;IAP1B,MAAMC,SAASd,UAAUC;IACzB,MAAMc,OAAO3C,yBAAyB,CAAC6B,KAAK;IAC5C,IAAI,CAACa,QAAQ,OAAO;QAAEE,IAAI;QAAOC,QAAQ;QAAKC,OAAO;IAAqC;IAC1F,MAAMC,WAAWC,QAAOP,oBAAAA,QAAQM,QAAQ,YAAhBN,oBAAoB,IAAIQ,IAAI,GAAGC,KAAK,CAAC,GAAG;IAChE,IAAI,CAACH,UAAU,OAAO;QAAEH,IAAI;QAAOC,QAAQ;QAAKC,OAAO,CAAC,YAAY,EAAEH,KAAK,QAAQ,CAAC;IAAC;IACrF,MAAMQ,YAAYzC,cAAc0C,GAAG,GAAGD,SAAS;IAE/C,MAAME,aAAaL,QAAOP,sBAAAA,QAAQY,UAAU,YAAlBZ,sBAAsB,IAAIQ,IAAI,GAAGC,KAAK,CAAC,GAAG;IACpE,MAAMI,UAAUD,aACZ,MAAM/D,aAAa6D,WAAoB;QACrCtB,MAAM,CAAC,UAAU,EAAEA,MAAM;QACzB0B,SAAS,GAAGd,QAAQe,MAAM,CAAC,CAAC,EAAET,UAAU;QACxCU,OAAOhB,QAAQgB,KAAK;QACpBC,KAAKL;QACLM,aAAa1D;IACf,KACA;IACJ,IAAIqD,WAAW,YAAYA,SAAS;QAClC,OAAOA,QAAQM,MAAM,CAACC,IAAI;IAC5B;IACA,MAAMC,QAAQR,WAAW,WAAWA,UAAUA,QAAQQ,KAAK,GAAG;IAE9D,IAAI;QACF,MAAMC,SAAS,MAAMC,KAAKb,WAAWtB,MAAMa,QAAQ,aAAKD;YAASM;;QACjE,IAAIgB,OAAOnB,EAAE,EAAE;gBAGgBH;YAF7B,MAAMhC,sBACJoB,MACA;gBAAEoC,KAAKxB,QAAQwB,GAAG;gBAAEC,KAAK,GAAEzB,iBAAAA,QAAQyB,KAAK,YAAbzB,iBAAiB;YAAK,GACjD;gBACEgB,OAAOhB,QAAQgB,KAAK;gBACpBD,QAAQf,QAAQe,MAAM;gBACtB/B,QAAQ;oBAAE0C,IAAIpB;oBAAUqB,MAAML,OAAOM,UAAU;gBAAC;gBAChDC,QAAQ;oBAAEH,IAAIJ,OAAOI,EAAE;oBAAEC,MAAML,OAAOK,IAAI;oBAAEG,WAAWR,OAAOQ,SAAS;gBAAC;YAC1E;YAEF,MAAMC,SAAkC;gBACtC5B,IAAI;gBACJuB,IAAIJ,OAAOI,EAAE;gBACbI,WAAWR,OAAOQ,SAAS;gBAC3BH,MAAML,OAAOK,IAAI;YACnB;YACA,OAAMN,yBAAAA,MAAOW,MAAM,CAAC,KAAKD;YACzB,OAAOA;QACT;QACA,yEAAyE;QACzE,0DAA0D;QAC1D,OAAMV,yBAAAA,MAAOY,OAAO;QACpB,OAAOX;IACT,EAAE,OAAOjB,OAAO;QACd,OAAMgB,yBAAAA,MAAOY,OAAO;QACpB,MAAM5B;IACR;AACF;AAMA,eAAekB,KACbb,SAAsC,EACtCtB,IAAgC,EAChCa,MAAkB,EAClBD,OAAwD;QAQtDA,cACE;IAPJ,MAAM,EAAEe,MAAM,EAAET,QAAQ,EAAEkB,GAAG,EAAE,GAAGxB;IAClC,MAAME,OAAO3C,yBAAyB,CAAC6B,KAAK;IAC5C,MAAM8C,UAAUxB,UAAUtC,UAAU,CAAC,SAAS+D,GAAG,CAACpB;IAClD,MAAMqB,gBAAgBF,QAAQ9D,UAAU,CAAC6B,OAAO7B,UAAU;IAC1D,MAAMiE,YAAYD,cAAcD,GAAG,CAAC7B;IACpC,MAAMgC,OACJtC,eAAAA,QAAQsC,GAAG,YAAXtC,gBACE,QAAA,AAAC,CAAA,MAAMU,UAAUtC,UAAU,CAAC,QAAQ+D,GAAG,CAACnC,QAAQgB,KAAK,EAAEuB,GAAG,EAAC,EAAGC,IAAI,cAAlE,QACA;IAEJ,IAAIvC,OAAOrB,WAAW,IAAI,CAACjC,iBAAiB2F,KAAcrC,OAAOrB,WAAW,GAAG;QAC7E,OAAO;YACLuB,IAAI;YACJC,QAAQ;YACRC,OAAO;QACT;IACF;IAEA,MAAMqB,KAAK5E;IACX,MAAMgF,YAAY7B,OAAO1B,kBAAkB,GAAGzB,sBAAsB;IAEpE,OAAO4D,UAAU+B,cAAc,CAAC,OAAOC;YA2GXF,wBACDxC,eA+EnBC,gBAGuDA;QA7L7D,yEAAyE;QACzE,uEAAuE;QACvE,mEAAmE;QACnE,MAAM0C,gBAAgB;YACpB1C,OAAO5B,SAAS;YAChB;YACA;YACA;YACA;SACD;QACD,MAAM,CAACuE,cAAc5D,QAAQ6D,iBAAiB,GAAG,MAAMC,QAAQC,GAAG,CAAC;YACjEL,GAAGH,GAAG,CAACL;YACPQ,GAAGH,GAAG,CAACF;YACPK,GAAGH,GAAG,CAACH,cAAcY,MAAM,IAAIL;SAChC;QACD,IAAI,CAACC,aAAaK,MAAM,EAAE,OAAO;YAAE9C,IAAI;YAAOC,QAAQ;YAAKC,OAAO;QAAe;QACjF,IAAI,CAACrB,OAAOiE,MAAM,IAAIjE,OAAOuD,GAAG,CAAC,gBAAgB,MAAM;YACrD,OAAO;gBAAEpC,IAAI;gBAAOC,QAAQ;gBAAKC,OAAO,CAAC,QAAQ,EAAEH,MAAM;YAAC;QAC5D;QACA,MAAMsC,OAAO7C,SAASX,OAAOwD,IAAI;QAEjC,wEAAwE;QACxE,uEAAuE;QACvE,2DAA2D;QAC3D,EAAE;QACF,wEAAwE;QACxE,uEAAuE;QACvE,uEAAuE;QACvE,qEAAqE;QACrE,kEAAkE;QAClE,wEAAwE;QACxE,mEAAmE;QACnE,cAAc;QACd,MAAMU,aAAaV,IAAI,CAAC,OAAO;QAC/B,MAAMW,gBACJlD,OAAO7B,UAAU,KAAK,aACtB,CAACd,sBAAsB;YAAE8B,MAAM8D;QAAqB;QACtD,IAAIjD,OAAO7B,UAAU,KAAK,WAAW;YACnC,IAAIgB,SAAS,iBAAiB8D,eAAe9F,mBAAmB;gBAC9D,OAAO;oBAAE+C,IAAI;oBAAOC,QAAQ;oBAAKC,OAAO;gBAAmC;YAC7E;YACA,IAAIjB,SAAS,YAAY+D,iBAAiBD,eAAe7F,sBAAsB;gBAC7E,OAAO;oBACL8C,IAAI;oBACJC,QAAQ;oBACRC,OAAO;gBACT;YACF;QACF;QAEA,MAAM+C,WAAyBP,iBAAiBQ,IAAI,CAACC,GAAG,CAAC,CAACC;gBAE3CA,UACAA;mBAHoD;gBACjE7B,IAAI6B,IAAI7B,EAAE;gBACVC,MAAMpB,QAAOgD,WAAAA,IAAIhB,GAAG,CAACtC,OAAO5B,SAAS,aAAxBkF,WAA6B;gBAC1C9E,MAAM8B,QAAOgD,YAAAA,IAAIhB,GAAG,CAAC,mBAARgB,YAAmB;gBAChCnE,MAAMmE,IAAIhB,GAAG,CAAC;gBACdiB,WAAWD,IAAIhB,GAAG,CAAC;gBACnBkB,YAAYF,IAAIhB,GAAG,CAAC;YACtB;;QACA,MAAMmB,OAAON,SAASO,MAAM,CAAC,CAACJ,MAAQA,IAAIC,SAAS,IAAI;QAEvD,0EAA0E;QAC1E,qEAAqE;QACrE,wEAAwE;QACxE,wEAAwE;QACxE,0EAA0E;QAC1E,yEAAyE;QACzE,wDAAwD;QACxD,MAAMI,aAAqCR,SAASE,GAAG,CAAC,CAACC,MAAS,CAAA;gBAChE7B,IAAI6B,IAAI7B,EAAE;gBACVtC,MAAMmE,IAAInE,IAAI;gBACdoE,WAAWD,IAAIC,SAAS;YAC1B,CAAA;QACA,MAAMK,aAAajB,aAAaL,GAAG,CAAC;QACpC,IAAIY,eAAe;YACjB,IAAIhG,iBAAiByG,YAAYC,YAAYC,IAAI,IAAI5G,8BAA8B;gBACjF,OAAO;oBACLiD,IAAI;oBACJC,QAAQ;oBACRC,OACE,kCACA,GAAGnD,6BAA6B,oCAAoC,CAAC,GACrE;gBACJ;YACF;QACF,OAAO,IAAI+C,OAAOzB,QAAQ,EAAE;YAC1B,MAAMuF,OACJ3E,SAAS,WACL1C,kBAAkBkH,YAAYC,YAAYC,IAAI,GAC9C1E,SAAS,aACPsE,KAAKC,MAAM,CAAC,CAACJ,MAAQA,IAAIE,UAAU,KAAK,WAAWO,MAAM,GACzDN,KAAKM,MAAM;YACnB,MAAMC,QAAQrH,WAAW0F,KAAcrC,OAAOzB,QAAQ,EAAWuF;YACjE,IAAI,CAACE,MAAMC,OAAO,EAAE;gBAClB,OAAO;oBACL/D,IAAI;oBACJC,QAAQ;oBACRC,OACE,CAAC,mBAAmB,EAAE4D,MAAME,KAAK,CAAC,CAAC,EAAEjE,OAAO+D,MAAME,KAAK,KAAK,IAAI,KAAK,IAAI,GAAG,CAAC,GAC7E;gBACJ;YACF;QACF;QAEA,mEAAmE;QACnE,iEAAiE;QACjE,oDAAoD;QACpD,MAAMvC,aAAarB,QAAOiC,yBAAAA,IAAI,CAACvC,OAAO5B,SAAS,CAAC,YAAtBmE,yBAA0B,IAAIhC,IAAI;QAC5D,MAAM4D,YAAY7D,QAAOP,gBAAAA,QAAQ2B,IAAI,YAAZ3B,gBAAgB,IAAIQ,IAAI,MAAM/C,qBAAqBmE;QAC5E,MAAMD,OAAOhE,oBACXyG,WACAV,KAAKJ,GAAG,CAAC,CAACC,MAAQA,IAAI5B,IAAI;QAE5B,MAAMlD,OAAOwB,OAAOxB,IAAI,GACpBb,oBACE4E,IAAI,CAAC,OAAO,EACZkB,KAAKJ,GAAG,CAAC,CAACC,MAAQA,IAAI9E,IAAI,KAE5B4F;QAEJ,sEAAsE;QACtE,oEAAoE;QACpE,kCAAkC;QAClC,IAAIC,cAA8C;QAClD,IAAIC,sBAAqC;QACzC,IAAItE,OAAO1B,kBAAkB,EAAE;gBAM0BiG,eAChChC;YANvB,MAAMiC,cAAcpC,UAAUjE,UAAU,CAAC;YACzC,MAAM,CAACoG,QAAQzB,IAAI,GAAG,MAAMD,QAAQC,GAAG,CAAC;gBACtCL,GAAGH,GAAG,CAACkC,YAAYC,OAAO,CAAC,aAAa,QAAQP,KAAK,CAAC;gBACtDzB,GAAGH,GAAG,CAACkC,YAAYzB,MAAM;aAC1B;YACD,IAAI2B,SAAmDH,gBAAAA,OAAOnB,IAAI,CAAC,EAAE,YAAdmB,gBAAkB;YACzE,MAAMI,UAAUrE,QAAOiC,kBAAAA,IAAI,CAAC,YAAY,YAAjBA,kBAAqB;YAC5C,IAAI,CAACmC,SAASC,SAAS;gBACrB,MAAMC,UAAU,MAAMnC,GAAGH,GAAG,CAACkC,YAAYtC,GAAG,CAACyC;gBAC7C,IAAIC,QAAQ5B,MAAM,EAAE0B,QAAQE;YAC9B;YACA,IAAIF,OAAO;gBACTL,cAAc3E,SAASgF,MAAMnC,IAAI;gBACjC+B,sBAAsBxB,IAAIe,IAAI;YAChC;QACF;QAEA,qEAAqE;QACrE,MAAM3B,MAA+B,CAAC;QACtC,KAAK,MAAM2C,SAAS7E,OAAO3B,MAAM,CAAE;YACjC,IAAIkE,IAAI,CAACsC,MAAM,KAAKT,WAAWlC,GAAG,CAAC2C,MAAM,GAAGtC,IAAI,CAACsC,MAAM;QACzD;QACA3C,GAAG,CAAClC,OAAO5B,SAAS,CAAC,GAAGsD;QACxB,IAAI1B,OAAOxB,IAAI,EAAE;YACf,IAAIA,MAAM0D,GAAG,CAAC,OAAO,GAAG1D;iBACnB,OAAO0D,GAAG,CAAC,OAAO;QACzB;QACA;;;;;;;;;;;;;;;;KAgBC,GACD,MAAM4C,UAAU,CAACC;gBAGbnH;YAFF,IAAIuB,SAAS,QAAQ,OAAO4F;YAC5B,QACEnH,uBAAAA,oBAAoBd,kBAAkBiI,MAAM;gBAC1CC,QAAQvD;gBACRwD,UAAUvD;YACZ,cAHA9D,uBAGMmH;QAEV;QACA,yEAAyE;QACzE,sEAAsE;QACtE,uEAAuE;QACvE,sBAAsB;QACtB,MAAMG,SAASnI,kBAAkB+H,QAAQ5C,GAAG,CAAC,QAAQ;QACrD,IAAIgD,QAAQhD,GAAG,CAAC,QAAQ,GAAGiD,OAAOC,IAAI,CAACF;QACvCzC,GAAG4C,MAAM,CAAClD,cAAcD,GAAG,CAACT,KAAK,aAC5BS,MACClC,iBAAAA,OAAOlB,MAAM,YAAbkB,iBAAiB,CAAC,GAGnBxD,uBAAuBwD,OAAO7B,UAAU,EAAE,aAAK+D,MAASlC,kBAAAA,OAAOlB,MAAM,YAAbkB,kBAAiB,CAAC,KAGzEb,SAAS,SACTnC,kBAAkB;YAChByE;YACA6D,aAAa5D;YACblD,MAAM0D,GAAG,CAAC,OAAO;YACjBqD,SAASrD,GAAG,CAAC,UAAU;QACzB,KACA,CAAC,GACDmC,eAAexC,YAAY;YAAEA;QAAU,IAAI,CAAC;YAChD2D,WAAW1H,UAAU2H,GAAG;YACxBC,WAAW5H,UAAU2H,GAAG;YACxBE,WAAWpE;;QAEb,IAAI8C,eAAexC,aAAa7B,OAAO1B,kBAAkB,EAAE;YACzD,MAAMsH,eAAe7I,kBAAkB+H,QAAQT,WAAW,CAAC,QAAQ;YACnE5B,GAAG4C,MAAM,CAAClD,cAAcD,GAAG,CAACT,IAAItD,UAAU,CAAC,YAAY+D,GAAG,CAACL,YAAY,aAClEwC,aACCuB,eAAe;gBAAEC,OAAOV,OAAOC,IAAI,CAACQ;YAAc,IAAI,CAAC;gBAC3D,CAAC5F,OAAO1B,kBAAkB,CAAC,EAAEmD;gBAC7BX;gBACAwE,aAAa7H,qBAAqBkE,YAAY2C;gBAC9CkB,WAAW1H,UAAU2H,GAAG;gBACxBC,WAAW5H,UAAU2H,GAAG;gBACxBE,WAAWpE;;QAEf;QACA,OAAO;YACLrB,IAAI;YACJuB;YACAI,WAAWwC,eAAexC,YAAYA,YAAY;YAClDH;YACAC;QACF;IACF;AACF"}
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/duplicate-resource.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\n/**\n * Duplicating a site resource whole, as a draft (AGL-2936).\n *\n * One module for every kind that lives under a site — screens and email\n * designs, reusable components, layouts, templates and forms, and the kinds\n * a plugin declares a copy for (a workflow) — so the console route, the AI\n * runtime's `duplicate_resource` tool and any later door make the same copy:\n * the same fields, the same uniqueness\n * rules, the same band arithmetic, the same activity rows. A caller that\n * has verified who is asking and that they may write the site hands over\n * the ids and gets back the copy's id, or a refusal it can answer with.\n *\n * WHAT MAKES THE COPY A DRAFT is different per kind, and each is the\n * omission of exactly one thing the create paths would otherwise do:\n *\n * - a screen is reachable only through the host's routing map, so the\n * copy gets no entry there — it has a slug of its own (`-copy`) but no\n * address until somebody publishes it;\n * - a collection's entry template renders only for the collection that\n * points at it, and nothing points at the copy;\n * - a layout, component or form is served only where a page places it,\n * and nothing places the copy;\n * - a declared kind drops what its plugin says a copy must not carry —\n * a workflow's trigger is cleared, so the copy runs nothing;\n * - a template is inert until it is used.\n *\n * COUNTED LIKE A CREATE. The copy is one more document in a collection the\n * plan bands, so it meets the same counter the create route meets — for a\n * screen that is not a page, the flat platform ceiling rather than a band —\n * and meets it INSIDE the transaction that writes the copy (AGL-2231): a count\n * taken before the write is a count N concurrent copies all pass. The\n * siblings are read once and answer three questions — the count, the name\n * the copy may take, and the slug — because three reads of one collection\n * is the reason a route and its readout come to disagree.\n *\n * THE VERSION TRAVELS WITH THE DOCUMENT. A besigner resource is opened by\n * its version, so the copy of a screen with no version is a screen nobody\n * can open. The source's most recently saved version is copied into the\n * copy's own `versions` as its first — no `versioning` entitlement is\n * owed for a first version — with its stored node bytes carried across\n * untouched: they are msgpack at rest, and re-encoding them is the\n * double-encode `encodeStoredNodes` exists to avoid.\n *\n * THE ONE TREE THAT IS REWRITTEN is a form's (AGL-3024). A form's design\n * names its own form inside itself, so carrying those bytes across hands the\n * copy a design that files every submission under the SOURCE — silently,\n * because the copy still renders and the row still lands. That binding is\n * moved onto the copy below. No other kind embeds its own id in its design,\n * so no other kind is decoded.\n */\n\n// The leaf module, not the server barrel: specs stage that barrel as a\n// closed world, and the list keys every create stamps (AGL-3321) are\n// nothing they have reason to name.\nimport { artifactCreateListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport {\n billableScreenIds,\n checkEntitlement,\n checkQuota,\n claimAttempt,\n createResourceUid,\n decodeStoredNodes,\n encodeStoredNodes,\n newFormListFields,\n NON_PAGE_SCREEN_MAX_PER_HOST,\n nonPageScreenIds,\n SCREEN_KIND_EMAIL,\n SCREEN_KIND_TEMPLATE,\n screenClaimsToBeAPage,\n type BillableScreenSource,\n type OrgEntitlements,\n type OrgFeatureFlags,\n} from '@aglyn/aglyn/server'\nimport {\n DUPLICABLE_RESOURCE_NOUNS,\n DUPLICATE_BUSY_MESSAGE,\n duplicateDisplayName,\n duplicateVersionNote,\n uniqueDuplicateName,\n uniqueDuplicateSlug,\n type DuplicableHostResourceKind,\n} from '@aglyn/aglyn/app-utils/duplicate-resource'\nimport { formDesignReboundTo } from '@aglyn/aglyn/app-utils/forms'\nimport { pluginHostResource } from '@aglyn/aglyn/plugin-manager/plugin-host-resources'\nimport { Timestamp } from 'firebase-admin/firestore'\nimport { logResourceDuplicated } from './duplicate-activity'\nimport firebaseAdmin from './firebase-admin'\n\n/** How the copy of each kind is made. */\ninterface KindRecipe {\n collection: string\n /** The field the name lives in — `displayName` on core's kinds; a declared kind names its own. */\n nameField: string\n /** The source fields carried onto the copy; everything else stays behind. */\n fields: readonly string[]\n /** The back-pointer a copied version carries, when the kind versions at all. */\n versionParentField?: 'screenId' | 'layoutId' | 'componentId' | 'formId'\n /** The plan counter the copy is charged to. */\n quotaKey?: keyof OrgEntitlements & string\n /** The feature the plan must include for the kind to exist at all. */\n entitlement?: keyof OrgFeatureFlags\n /** Whether a `slug` on the source becomes a unique `-copy` slug. */\n slug?: boolean\n /** What the copy carries beyond the source's fields. */\n stamps?: Readonly<Record<string, unknown>>\n}\n\n/**\n * Per kind, the same fields the create route's allow-list admits, less the\n * ones a copy must not inherit: a screen's `versionId` (the copy mints its\n * own), a template's `source` (stamped `authored` below — a copy of a\n * marketplace template is the customer's own), and anything the server\n * stamps on every create.\n *\n * The platform's own kinds only. A kind a plugin owns — a workflow, whose\n * copy clears its trigger so it runs nothing until armed — is declared by\n * that plugin beside its collection in `plugins.config.json` and read by\n * {@link recipeFor} (AGL-3080), compiled so a copy never depends on this\n * process having loaded the plugin.\n */\nconst RECIPES: Partial<Record<DuplicableHostResourceKind, KindRecipe>> = {\n screen: {\n collection: 'screens',\n nameField: 'displayName',\n fields: [\n 'description',\n 'seo',\n 'kind',\n 'visibility',\n 'locale',\n 'protection',\n // The shared-layout binding. A version inherits it from its screen, so\n // a copy without it renders with no header or footer (AGL-3120).\n 'layoutId',\n ],\n versionParentField: 'screenId',\n quotaKey: 'screensPerHost',\n slug: true,\n },\n // No `quotaKey`: an email design is not a page, so it meets the flat\n // platform ceiling on non-page screens below rather than the page band.\n emailDesign: {\n collection: 'screens',\n nameField: 'displayName',\n fields: ['description', 'kind'],\n versionParentField: 'screenId',\n },\n component: {\n collection: 'components',\n nameField: 'displayName',\n // `kind` keeps an email block an email block (AGL-3287): without it the\n // copy reads as a page component, leaves every email editor's palette,\n // and turns up in page editors where its email blocks cannot render.\n fields: ['description', 'icon', 'rootId', 'nodes', 'props', 'kind'],\n versionParentField: 'componentId',\n // Counted only: a reusable component is not behind\n // `reusableComponents`, the same as its create at `/api/hosts/resources`\n // (AGL-3615). Free 1, every paid plan unlimited.\n quotaKey: 'componentsPerHost',\n },\n layout: {\n collection: 'layouts',\n nameField: 'displayName',\n fields: ['description'],\n versionParentField: 'layoutId',\n quotaKey: 'sharedLayoutsPerHost',\n },\n template: {\n collection: 'templates',\n nameField: 'displayName',\n fields: [\n 'kind',\n // A component template's own kind — email block or page component —\n // so Use template on a copy still makes the right one (AGL-3287).\n 'componentKind',\n 'description',\n 'category',\n 'placeholders',\n 'nodes',\n 'rootId',\n 'props',\n 'slug',\n 'seo',\n 'theme',\n ],\n quotaKey: 'templatesPerHost',\n stamps: { source: { type: 'authored' } },\n },\n form: {\n collection: 'forms',\n nameField: 'displayName',\n fields: [\n 'fields',\n 'consentFieldName',\n 'routing',\n 'legacyMatch',\n 'rootId',\n 'nodes',\n ],\n versionParentField: 'formId',\n // Counted only: a saved form is not behind `reusableComponents`, the same\n // as its create at `/api/hosts/resources` (AGL-3597).\n quotaKey: 'formsPerHost',\n slug: true,\n },\n}\n\n/**\n * The recipe for a kind: core's own, else the one its plugin declared, else\n * `null` — which is refused, never guessed at. A declared copy is met against\n * the same plan counter and feature as the kind's create.\n */\nexport function recipeFor(kind: string): KindRecipe | null {\n if (Object.prototype.hasOwnProperty.call(RECIPES, kind)) {\n return RECIPES[kind as DuplicableHostResourceKind] ?? null\n }\n const declared = pluginHostResource(kind)\n if (!declared?.duplicate) return null\n return {\n collection: declared.collection,\n nameField: declared.duplicate.nameField,\n fields: declared.duplicate.fields,\n ...(declared.quotaKey\n ? { quotaKey: declared.quotaKey as keyof OrgEntitlements & string }\n : {}),\n ...(declared.entitlement\n ? { entitlement: declared.entitlement as keyof OrgFeatureFlags }\n : {}),\n ...(declared.duplicate.stamps ? { stamps: declared.duplicate.stamps } : {}),\n }\n}\n\nexport interface DuplicateResourceOptions {\n orgId: string\n hostId: string\n sourceId: string\n /** The name the copy takes; blank means `Copy of <source>`. */\n name?: string | null\n /** The verified person making the copy. */\n uid: string\n email?: string | null\n /**\n * The owning org's document, when the caller already holds it; read\n * here otherwise. The bands are resolved from it.\n */\n org?: Record<string, unknown> | null\n /**\n * A client-minted key for this attempt, so a double click makes one copy\n * and a retry of a finished attempt answers what the first did. A caller\n * with no client behind it — the AI runtime — sends none and is not\n * claimed.\n */\n attemptKey?: string | null\n}\n\nexport type DuplicateResourceResult =\n | {\n ok: true\n id: string\n /** The copy's first version, for the kinds that version. */\n versionId: string | null\n name: string\n }\n | { ok: false; status: number; error: string }\n\n/** What a sibling contributes to the three answers the siblings give. */\ninterface SiblingRow {\n id: string\n name: string\n slug: string\n kind: unknown\n deletedAt: unknown\n sourceType: unknown\n}\n\nconst asRecord = (value: unknown): Record<string, unknown> =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {}\n\n/**\n * Makes the copy. The caller has authenticated `uid` and verified that they\n * may write content on `hostId`; this decides everything after that.\n */\nexport async function duplicateResource(\n kind: DuplicableHostResourceKind,\n options: DuplicateResourceOptions,\n): Promise<DuplicateResourceResult> {\n const recipe = recipeFor(kind)\n const noun = DUPLICABLE_RESOURCE_NOUNS[kind]\n if (!recipe) return { ok: false, status: 400, error: 'That resource cannot be duplicated' }\n const sourceId = String(options.sourceId ?? '').trim().slice(0, 64)\n if (!sourceId) return { ok: false, status: 400, error: `Missing the ${noun} to copy` }\n const firestore = firebaseAdmin.app().firestore()\n\n const attemptKey = String(options.attemptKey ?? '').trim().slice(0, 200)\n const claimed = attemptKey\n ? await claimAttempt(firestore as never, {\n kind: `duplicate-${kind}`,\n scopeId: `${options.hostId}:${sourceId}`,\n orgId: options.orgId,\n key: attemptKey,\n busyMessage: DUPLICATE_BUSY_MESSAGE,\n })\n : null\n if (claimed && 'replay' in claimed) {\n return claimed.replay.body as DuplicateResourceResult\n }\n const claim = claimed && 'claim' in claimed ? claimed.claim : null\n\n try {\n const result = await copy(firestore, kind, recipe, { ...options, sourceId })\n if (result.ok) {\n await logResourceDuplicated(\n kind,\n { uid: options.uid, email: options.email ?? null },\n {\n orgId: options.orgId,\n hostId: options.hostId,\n source: { id: sourceId, name: result.sourceName },\n target: { id: result.id, name: result.name, versionId: result.versionId },\n },\n )\n const answer: DuplicateResourceResult = {\n ok: true,\n id: result.id,\n versionId: result.versionId,\n name: result.name,\n }\n await claim?.record(200, answer)\n return answer\n }\n // A refusal releases the key: the person fixes the cause — frees a slot,\n // picks another name — and presses the same button again.\n await claim?.release()\n return result\n } catch (error) {\n await claim?.release()\n throw error\n }\n}\n\ntype CopyOutcome =\n | { ok: true; id: string; versionId: string | null; name: string; sourceName: string }\n | { ok: false; status: number; error: string }\n\nasync function copy(\n firestore: FirebaseFirestore.Firestore,\n kind: DuplicableHostResourceKind,\n recipe: KindRecipe,\n options: DuplicateResourceOptions & { sourceId: string },\n): Promise<CopyOutcome> {\n const { hostId, sourceId, uid } = options\n const noun = DUPLICABLE_RESOURCE_NOUNS[kind]\n const hostRef = firestore.collection('hosts').doc(hostId)\n const collectionRef = hostRef.collection(recipe.collection)\n const sourceRef = collectionRef.doc(sourceId)\n const org =\n options.org ??\n (((await firestore.collection('orgs').doc(options.orgId).get()).data() ??\n null) as Record<string, unknown> | null)\n\n if (recipe.entitlement && !checkEntitlement(org as never, recipe.entitlement)) {\n return {\n ok: false,\n status: 403,\n error: 'This feature is not included in your plan — see Billing',\n }\n }\n\n const id = createResourceUid()\n const versionId = recipe.versionParentField ? createResourceUid() : null\n\n return firestore.runTransaction(async (tx): Promise<CopyOutcome> => {\n // ALL READS BEFORE ANY WRITE, which Firestore requires. The host is read\n // inside the transaction rather than passed in because its routing map\n // is an input to the screen count and is ordinary client-writable.\n const siblingFields = [\n recipe.nameField,\n 'slug',\n 'kind',\n 'deletedAt',\n 'source.type',\n ]\n const [hostSnapshot, source, siblingsSnapshot] = await Promise.all([\n tx.get(hostRef),\n tx.get(sourceRef),\n tx.get(collectionRef.select(...siblingFields)),\n ])\n if (!hostSnapshot.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (!source.exists || source.get('deletedAt') != null) {\n return { ok: false, status: 404, error: `Unknown ${noun}` }\n }\n const data = asRecord(source.data())\n\n // The two screen kinds share a collection and are told apart by `kind`:\n // an email design copied as a page would land on the Screens list, and\n // the other way round would put a page on the Emails page.\n //\n // Of the screens that are not pages, only a collection's entry template\n // may be copied as a screen (AGL-3102), and the copy stays a template:\n // `kind` rides across with the recipe's fields, so it composes nothing\n // until a collection points at it — which is how a second collection\n // starts from the first one's design. An error screen is refused:\n // `kind: 'error'` is the slot assignment's to stamp, because that write\n // is what bounds how many a site holds, and a copy would stamp one\n // outside it.\n const sourceKind = data['kind']\n const nonPageScreen =\n recipe.collection === 'screens' &&\n !screenClaimsToBeAPage({ kind: sourceKind as string })\n if (recipe.collection === 'screens') {\n if (kind === 'emailDesign' && sourceKind !== SCREEN_KIND_EMAIL) {\n return { ok: false, status: 400, error: 'That page is not an email design' }\n }\n if (kind === 'screen' && nonPageScreen && sourceKind !== SCREEN_KIND_TEMPLATE) {\n return {\n ok: false,\n status: 400,\n error: 'Only a page or a collection entry template can be duplicated as a page',\n }\n }\n }\n\n const siblings: SiblingRow[] = siblingsSnapshot.docs.map((row) => ({\n id: row.id,\n name: String(row.get(recipe.nameField) ?? ''),\n slug: String(row.get('slug') ?? ''),\n kind: row.get('kind'),\n deletedAt: row.get('deletedAt'),\n sourceType: row.get('source.type'),\n }))\n const live = siblings.filter((row) => row.deletedAt == null)\n\n // THE BAND, met inside the transaction (AGL-2231) with the create route's\n // own arithmetic: screens count what the routing map makes billable,\n // templates exclude the platform's starters, everything else counts its\n // live documents. A screen copy that is not a page — an email design or\n // an entry template — meets the flat platform ceiling on non-page screens\n // instead, the one an email design's create meets and a demoted template\n // fills, and never the page band: it spends none of it.\n const screenRows: BillableScreenSource[] = siblings.map((row) => ({\n id: row.id,\n kind: row.kind,\n deletedAt: row.deletedAt,\n }))\n const routingMap = hostSnapshot.get('screens') as never\n if (nonPageScreen) {\n if (nonPageScreenIds(screenRows, routingMap).size >= NON_PAGE_SCREEN_MAX_PER_HOST) {\n return {\n ok: false,\n status: 403,\n error:\n 'This site is at its limit of ' +\n `${NON_PAGE_SCREEN_MAX_PER_HOST} email designs and template pages — ` +\n 'delete some to make room',\n }\n }\n } else if (recipe.quotaKey) {\n const used =\n kind === 'screen'\n ? billableScreenIds(screenRows, routingMap).size\n : kind === 'template'\n ? live.filter((row) => row.sourceType !== 'starter').length\n : live.length\n const quota = checkQuota(org as never, recipe.quotaKey as never, used)\n if (!quota.allowed) {\n return {\n ok: false,\n status: 403,\n error:\n `Your plan includes ${quota.limit} ${noun}${quota.limit === 1 ? '' : 's'} — ` +\n 'upgrade in Billing for more',\n }\n }\n }\n\n // THE NAME AND THE SLUG, unique among the live siblings. A deleted\n // sibling's name is free: a person who deleted \"Home\" and copies\n // \"About\" as \"Home\" is not confused by a tombstone.\n const sourceName = String(data[recipe.nameField] ?? '').trim()\n const requested = String(options.name ?? '').trim() || duplicateDisplayName(sourceName)\n const name = uniqueDuplicateName(\n requested,\n live.map((row) => row.name),\n )\n const slug = recipe.slug\n ? uniqueDuplicateSlug(\n data['slug'] as string | undefined,\n live.map((row) => row.slug),\n )\n : undefined\n\n // THE VERSION: the most recently saved one, else the one the document\n // points at. Read inside the transaction so the copy is of what was\n // saved when the count was taken.\n let versionData: Record<string, unknown> | null = null\n let sourceVersionNumber: number | null = null\n if (recipe.versionParentField) {\n const versionsRef = sourceRef.collection('versions')\n const [newest, all] = await Promise.all([\n tx.get(versionsRef.orderBy('updatedAt', 'desc').limit(1)),\n tx.get(versionsRef.select()),\n ])\n let found: FirebaseFirestore.DocumentSnapshot | null = newest.docs[0] ?? null\n const pointer = String(data['versionId'] ?? '')\n if (!found && pointer) {\n const pointed = await tx.get(versionsRef.doc(pointer))\n if (pointed.exists) found = pointed\n }\n if (found) {\n versionData = asRecord(found.data())\n sourceVersionNumber = all.size\n }\n }\n\n // THE DOCUMENT: the recipe's fields, the name, the slug, the stamps.\n const doc: Record<string, unknown> = {}\n for (const field of recipe.fields) {\n if (data[field] !== undefined) doc[field] = data[field]\n }\n doc[recipe.nameField] = name\n if (recipe.slug) {\n if (slug) doc['slug'] = slug\n else delete doc['slug']\n }\n /*\n * THE FORM'S BINDING, moved onto the copy (AGL-3024).\n *\n * A form's design names its own form in a prop, and that name is what\n * `/api/forms/submit` files a submission under. Copied verbatim it still\n * names the SOURCE, so the copy renders, collects, and files everything\n * it collects under the form it was copied from — while its own list\n * stays empty and nothing anywhere reports an error. `checkFormContract`\n * is what the console's form page banners it with; `formDesignReboundTo`\n * is the write side of the same fact.\n *\n * This is the one place a copied tree is not carried across untouched,\n * and the exception is narrow on purpose: no other kind here embeds its\n * own id in its design. `formDesignReboundTo` answers `null` when there\n * is nothing to move, so every other kind — and a form whose design is\n * already correct — pays nothing for this.\n */\n const rebound = (raw: unknown): unknown => {\n if (kind !== 'form') return raw\n return (\n formDesignReboundTo(decodeStoredNodes(raw), {\n formId: id,\n formName: name,\n }) ?? raw\n )\n }\n // The stored tree is msgpack at rest; `encodeStoredNodes` passes encoded\n // bytes through and packs a plain map, so a legacy plain source lands\n // compressed like every create does — and so does a rebound one, which\n // comes back decoded.\n const packed = encodeStoredNodes(rebound(doc['nodes']))\n if (packed) doc['nodes'] = Buffer.from(packed)\n tx.create(collectionRef.doc(id), {\n ...doc,\n ...(recipe.stamps ?? {}),\n // The keys the copy's list finds it by (AGL-3321), from the copy's own\n // name and stamps — see `artifactCreateListKeys`.\n ...artifactCreateListKeys(recipe.collection, { ...doc, ...(recipe.stamps ?? {}) }),\n // A copied form starts as a new one does, with its own search keys and\n // nothing counted (AGL-3330); only the routing it copied is kept.\n ...(kind === 'form'\n ? newFormListFields({\n id,\n displayName: name,\n slug: doc['slug'],\n routing: doc['routing'] as Parameters<typeof newFormListFields>[0]['routing'],\n })\n : {}),\n ...(versionData && versionId ? { versionId } : {}),\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n createdBy: uid,\n })\n if (versionData && versionId && recipe.versionParentField) {\n const versionNodes = encodeStoredNodes(rebound(versionData['nodes']))\n tx.create(collectionRef.doc(id).collection('versions').doc(versionId), {\n ...versionData,\n ...(versionNodes ? { nodes: Buffer.from(versionNodes) } : {}),\n [recipe.versionParentField]: id,\n hostId,\n displayName: duplicateVersionNote(sourceName, sourceVersionNumber),\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n createdBy: uid,\n })\n }\n return {\n ok: true,\n id,\n versionId: versionData && versionId ? versionId : null,\n name,\n sourceName,\n }\n })\n}\n"],"names":["artifactCreateListKeys","billableScreenIds","checkEntitlement","checkQuota","claimAttempt","createResourceUid","decodeStoredNodes","encodeStoredNodes","newFormListFields","NON_PAGE_SCREEN_MAX_PER_HOST","nonPageScreenIds","SCREEN_KIND_EMAIL","SCREEN_KIND_TEMPLATE","screenClaimsToBeAPage","DUPLICABLE_RESOURCE_NOUNS","DUPLICATE_BUSY_MESSAGE","duplicateDisplayName","duplicateVersionNote","uniqueDuplicateName","uniqueDuplicateSlug","formDesignReboundTo","pluginHostResource","Timestamp","logResourceDuplicated","firebaseAdmin","RECIPES","screen","collection","nameField","fields","versionParentField","quotaKey","slug","emailDesign","component","layout","template","stamps","source","type","form","recipeFor","kind","Object","prototype","hasOwnProperty","call","declared","duplicate","entitlement","asRecord","value","Array","isArray","duplicateResource","options","recipe","noun","ok","status","error","sourceId","String","trim","slice","firestore","app","attemptKey","claimed","scopeId","hostId","orgId","key","busyMessage","replay","body","claim","result","copy","uid","email","id","name","sourceName","target","versionId","answer","record","release","hostRef","doc","collectionRef","sourceRef","org","get","data","runTransaction","tx","siblingFields","hostSnapshot","siblingsSnapshot","Promise","all","select","exists","sourceKind","nonPageScreen","siblings","docs","map","row","deletedAt","sourceType","live","filter","screenRows","routingMap","size","used","length","quota","allowed","limit","requested","undefined","versionData","sourceVersionNumber","newest","versionsRef","orderBy","found","pointer","pointed","field","rebound","raw","formId","formName","packed","Buffer","from","create","displayName","routing","createdAt","now","updatedAt","createdBy","versionNodes","nodes"],"mappings":"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"}