@aglyn/aglyn 1.0.0-beta.292 → 1.0.0-beta.293

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.
Files changed (45) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/analytics-events.d.ts +12 -2
  3. package/src/lib/app-utils/analytics-events.js.map +1 -1
  4. package/src/lib/app-utils/business-profile-values.d.ts +3 -2
  5. package/src/lib/app-utils/business-profile-values.js +4 -3
  6. package/src/lib/app-utils/business-profile-values.js.map +1 -1
  7. package/src/lib/app-utils/docs-help-sections.generated.js +4 -4
  8. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  9. package/src/lib/app-utils/docs-help.generated.d.ts +8 -2
  10. package/src/lib/app-utils/docs-help.generated.js +18 -1
  11. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  12. package/src/lib/app-utils/docs-index.generated.js +101 -24
  13. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  14. package/src/lib/app-utils/notifications.d.ts +31 -2
  15. package/src/lib/app-utils/notifications.js +18 -2
  16. package/src/lib/app-utils/notifications.js.map +1 -1
  17. package/src/lib/app-utils/site-create-return.d.ts +63 -0
  18. package/src/lib/app-utils/site-create-return.js +72 -0
  19. package/src/lib/app-utils/site-create-return.js.map +1 -0
  20. package/src/lib/app-utils/site-starter-pick.d.ts +51 -0
  21. package/src/lib/app-utils/site-starter-pick.js +47 -0
  22. package/src/lib/app-utils/site-starter-pick.js.map +1 -0
  23. package/src/lib/app-utils/site-styles.d.ts +110 -0
  24. package/src/lib/app-utils/site-styles.js +126 -0
  25. package/src/lib/app-utils/site-styles.js.map +1 -0
  26. package/src/lib/app-utils/site-unavailable.d.ts +96 -0
  27. package/src/lib/app-utils/site-unavailable.js +80 -0
  28. package/src/lib/app-utils/site-unavailable.js.map +1 -0
  29. package/src/lib/app-utils/visitor-upload.d.ts +19 -0
  30. package/src/lib/app-utils/visitor-upload.js +29 -1
  31. package/src/lib/app-utils/visitor-upload.js.map +1 -1
  32. package/src/lib/foundation/definitions/platform.types.d.ts +9 -0
  33. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  34. package/src/lib/plugin-manager/feature-plugins.d.ts +68 -13
  35. package/src/lib/plugin-manager/feature-plugins.js +16 -9
  36. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  37. package/src/lib/plugin-manager/plugin-site-starters.d.ts +30 -0
  38. package/src/lib/plugin-manager/plugin-site-starters.js +19 -0
  39. package/src/lib/plugin-manager/plugin-site-starters.js.map +1 -1
  40. package/src/lib/plugin-manager/plugin-site-style-looks.d.ts +43 -0
  41. package/src/lib/plugin-manager/plugin-site-style-looks.js +30 -0
  42. package/src/lib/plugin-manager/plugin-site-style-looks.js.map +1 -0
  43. package/src/lib/plugin-manager/plugin-upload-storage-gate.d.ts +60 -0
  44. package/src/lib/plugin-manager/plugin-upload-storage-gate.js +32 -0
  45. package/src/lib/plugin-manager/plugin-upload-storage-gate.js.map +1 -0
@@ -29,7 +29,9 @@
29
29
  * labelled `text/html` is nothing;
30
30
  * - where its bytes live ({@link visitorUploadPendingPath} while it is on its
31
31
  * way, {@link visitorUploadFinalPath} once it is checked);
32
- * - the record a finished upload is ({@link VisitorUploadFile}).
32
+ * - the record a finished upload is ({@link VisitorUploadFile});
33
+ * - how a kept file counts toward the workspace's storage
34
+ * ({@link VISITOR_UPLOAD_COUNTER_BYTES}).
33
35
  *
34
36
  * The server half (`@aglyn/tenant-data-admin/server/visitor-upload`) signs the
35
37
  * upload, inspects the bytes and moves them; the browser half
@@ -341,6 +343,32 @@ export function isVisitorUploadSegment(value) {
341
343
  /** Where a checked file is kept: private to the site, with no download token. */ export function visitorUploadFinalPath(scope, uploadId) {
342
344
  return `${visitorUploadFolder(scope)}${uploadId}`;
343
345
  }
346
+ /*------------------------------------------
347
+ * Storage
348
+ *-----------------------------------------*/ /**
349
+ * A kept file counts toward its workspace's storage the way a media library
350
+ * file does. Its bytes are added to the site's storage counter,
351
+ * `hosts/{hostId}/counters/media`, whose `bytes` is the one figure the
352
+ * storage band, the Billing meter, the usage alerts and the invoice all read.
353
+ * So a proof, a customer's file on a quote and a form's file are measured,
354
+ * warned about and billed exactly as a library upload is, and no reader of
355
+ * storage can leave them out.
356
+ *
357
+ * The same counter says how much of `bytes` is these files, in the two fields
358
+ * below. That keeps `count` the number of library files and lets the media
359
+ * library show its own share ({@link mediaLibraryBytes}).
360
+ */ export const VISITOR_UPLOAD_COUNTER_BYTES = 'uploadBytes';
361
+ export const VISITOR_UPLOAD_COUNTER_FILES = 'uploadCount';
362
+ const counterBytes = (value)=>{
363
+ const bytes = Number(value);
364
+ return Number.isFinite(bytes) && bytes > 0 ? bytes : 0;
365
+ };
366
+ /** The bytes of kept files a site's storage counter holds, never more than its `bytes`. */ export function visitorUploadCounterBytes(counter) {
367
+ return Math.min(counterBytes(counter == null ? void 0 : counter['bytes']), counterBytes(counter == null ? void 0 : counter[VISITOR_UPLOAD_COUNTER_BYTES]));
368
+ }
369
+ /** The media library's own files: the counter's `bytes` less the kept files counted beside them. */ export function mediaLibraryBytes(counter) {
370
+ return counterBytes(counter == null ? void 0 : counter['bytes']) - visitorUploadCounterBytes(counter);
371
+ }
344
372
  /**
345
373
  * The files a browser picked, checked before any is sent: how many, and each
346
374
  * one's type and size. `already` is how many the submission holds.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/visitor-upload.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 * FILES A VISITOR SENDS A BUSINESS (AGL-4096).\n *\n * Artwork for a quote, a logo with a request form: files someone without an\n * account hands a business through its site. This module is the policy, pure\n * and shared by the browser and the server:\n *\n * - which files are taken ({@link VISITOR_UPLOAD_TYPES}: PNG, JPEG, WebP,\n * GIF, PDF, SVG, AI, EPS, PSD, TIFF), how big ({@link VISITOR_UPLOAD_MAX_BYTES})\n * and how many at once ({@link VISITOR_UPLOAD_MAX_FILES});\n * - what a file's type IS ({@link resolveVisitorUploadType}): read from its\n * name and checked against what the browser said, so a `.psd` a browser\n * labels `application/octet-stream` is still a Photoshop file and a `.png`\n * labelled `text/html` is nothing;\n * - where its bytes live ({@link visitorUploadPendingPath} while it is on its\n * way, {@link visitorUploadFinalPath} once it is checked);\n * - the record a finished upload is ({@link VisitorUploadFile}).\n *\n * The server half (`@aglyn/tenant-data-admin/server/visitor-upload`) signs the\n * upload, inspects the bytes and moves them; the browser half\n * (`visitor-upload-client.ts`) sends them. A plugin that takes files (commerce\n * for a quote's artwork, forms for a request) names its own `purpose` and the\n * record it is for, and decides for itself who may upload for it.\n *=========================================*/\n\n/** The most one file may weigh. */\nexport const VISITOR_UPLOAD_MAX_BYTES = 25 * 1024 * 1024\n\n/** The most files one submission carries. */\nexport const VISITOR_UPLOAD_MAX_FILES = 10\n\n/** How long the signed upload a ticket carries can be used, from its minting. */\nexport const VISITOR_UPLOAD_TICKET_TTL_MS = 15 * 60 * 1000\n\n/**\n * Where uploads wait until they are checked. Top-level, never under `hosts/`:\n * the bucket's lifecycle deletes whatever is still here a day after it\n * arrived (`cloud/media-bucket-lifecycle.json`), which no rule may do under a\n * prefix that holds live customer files.\n */\nexport const VISITOR_UPLOAD_PENDING_PREFIX = 'visitor-uploads-pending/'\n\n/** The honeypot field a form posts beside its files, as the forms plugin names it. */\nexport const VISITOR_UPLOAD_HONEYPOT_FIELD = 'website'\n\n/** One kind of file a visitor may send. */\nexport interface VisitorUploadType {\n /** The type the file is stored and served as. */\n contentType: string\n /** Lower-case, with the dot. The first is the one a file is renamed to when it has none. */\n extensions: readonly string[]\n /** What a person calls it. */\n label: string\n /**\n * Shown in the page as a picture. Only raster formats a browser draws and\n * that cannot carry script; everything else downloads.\n */\n inline: boolean\n /** Other types a browser reports for this kind of file. */\n aliases?: readonly string[]\n}\n\nexport const VISITOR_UPLOAD_TYPES: readonly VisitorUploadType[] = [\n { contentType: 'image/png', extensions: ['.png'], label: 'PNG', inline: true },\n { contentType: 'image/jpeg', extensions: ['.jpg', '.jpeg'], label: 'JPEG', inline: true, aliases: ['image/jpg', 'image/pjpeg'] },\n { contentType: 'image/webp', extensions: ['.webp'], label: 'WebP', inline: true },\n { contentType: 'image/gif', extensions: ['.gif'], label: 'GIF', inline: true },\n { contentType: 'application/pdf', extensions: ['.pdf'], label: 'PDF', inline: false, aliases: ['application/x-pdf'] },\n // Stripped of script on the way in, and still downloaded rather than drawn.\n { contentType: 'image/svg+xml', extensions: ['.svg'], label: 'SVG', inline: false },\n {\n contentType: 'application/illustrator',\n extensions: ['.ai'],\n label: 'AI',\n inline: false,\n aliases: ['application/postscript', 'application/pdf', 'application/vnd.adobe.illustrator'],\n },\n { contentType: 'application/postscript', extensions: ['.eps'], label: 'EPS', inline: false, aliases: ['application/eps', 'image/eps', 'image/x-eps', 'application/x-eps'] },\n {\n contentType: 'image/vnd.adobe.photoshop',\n extensions: ['.psd'],\n label: 'PSD',\n inline: false,\n aliases: ['application/x-photoshop', 'application/photoshop', 'application/psd', 'image/psd', 'image/x-photoshop'],\n },\n { contentType: 'image/tiff', extensions: ['.tif', '.tiff'], label: 'TIFF', inline: false, aliases: ['image/x-tiff'] },\n]\n\n/** Types a browser sends when it does not know; the name decides then. */\nconst UNKNOWN_TYPES = new Set(['', 'application/octet-stream', 'binary/octet-stream', 'application/unknown'])\n\n/**\n * The kinds of file a business can ask for, as a person picks them: photos and\n * pictures, PDFs, and design files. Each kind is a set of the types above, so\n * a field that takes only PDFs takes only `application/pdf` (AGL-4098).\n */\nexport type VisitorUploadKind = 'images' | 'pdf' | 'design'\n\nexport const VISITOR_UPLOAD_KINDS: readonly VisitorUploadKind[] = ['images', 'pdf', 'design']\n\n/** What a person calls each kind. */\nexport const VISITOR_UPLOAD_KIND_LABELS: Readonly<Record<VisitorUploadKind, string>> = {\n images: 'Images (PNG, JPEG, WebP, GIF)',\n pdf: 'PDF',\n design: 'Design files (SVG, AI, EPS, PSD, TIFF)',\n}\n\nconst KIND_TYPES: Readonly<Record<VisitorUploadKind, readonly string[]>> = {\n images: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'],\n pdf: ['application/pdf'],\n design: ['image/svg+xml', 'application/illustrator', 'application/postscript', 'image/vnd.adobe.photoshop', 'image/tiff'],\n}\n\n/** The kinds a stored list names, in order and without repeats; empty or unreadable is every kind. */\nexport function readVisitorUploadKinds(raw: unknown): VisitorUploadKind[] {\n const listed = Array.isArray(raw) ? raw : typeof raw === 'string' ? raw.split(',') : []\n const kinds = VISITOR_UPLOAD_KINDS.filter((kind) => listed.some((entry) => String(entry).trim() === kind))\n return kinds.length ? kinds : [...VISITOR_UPLOAD_KINDS]\n}\n\n/** The types the kinds take, or every type when none are named. */\nfunction typesOfKinds(kinds: readonly VisitorUploadKind[] | undefined): readonly VisitorUploadType[] {\n if (!kinds?.length) return VISITOR_UPLOAD_TYPES\n const wanted = new Set(kinds.flatMap((kind) => KIND_TYPES[kind] ?? []))\n return VISITOR_UPLOAD_TYPES.filter((type) => wanted.has(type.contentType))\n}\n\n/** Whether a stored type is one of the kinds; true for any type when no kinds are named. */\nexport function visitorUploadTypeInKinds(contentType: string, kinds?: readonly VisitorUploadKind[]): boolean {\n return typesOfKinds(kinds).some((type) => type.contentType === contentType)\n}\n\n/** `PNG, JPEG, WebP, GIF, PDF, SVG, AI, EPS, PSD or TIFF`, or the labels of the kinds named. */\nexport function describeVisitorUploadTypes(kinds?: readonly VisitorUploadKind[]): string {\n const labels = typesOfKinds(kinds).map((type) => type.label)\n if (labels.length === 1) return labels[0]\n return `${labels.slice(0, -1).join(', ')} or ${labels[labels.length - 1]}`\n}\n\n/** The file picker's `accept` for the kinds named: every extension and type they take. */\nexport function visitorUploadAccept(kinds?: readonly VisitorUploadKind[]): string {\n return typesOfKinds(kinds)\n .flatMap((type) => [...type.extensions, type.contentType])\n .join(',')\n}\n\n/** The file picker's `accept`: every extension and type taken. */\nexport const VISITOR_UPLOAD_ACCEPT = visitorUploadAccept()\n\n/** `25 MB`. */\nexport function formatVisitorUploadBytes(bytes: number): string {\n if (!(bytes > 0)) return '0 KB'\n if (bytes < 1024 * 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`\n const mb = bytes / (1024 * 1024)\n return `${mb >= 10 ? Math.round(mb) : Math.round(mb * 10) / 10} MB`\n}\n\n/** The extension of a file name, lower-case with its dot, or ''. */\nfunction extensionOf(name: string): string {\n const dot = name.lastIndexOf('.')\n return dot > 0 ? name.slice(dot).toLowerCase() : ''\n}\n\n/**\n * The type a file is taken as, or null when it is not one this takes. The\n * NAME decides, because a browser reports most design files as nothing or as\n * something generic; the reported type only has to agree with it (be this\n * type, one it is known by, or unknown). The bytes are checked against the\n * answer after they arrive.\n */\nexport function resolveVisitorUploadType(name: string, reportedType: string): VisitorUploadType | null {\n const extension = extensionOf(String(name ?? ''))\n const reported = String(reportedType ?? '').split(';')[0].trim().toLowerCase()\n const byName = VISITOR_UPLOAD_TYPES.find((type) => type.extensions.includes(extension))\n if (!byName) return null\n if (UNKNOWN_TYPES.has(reported) || reported === byName.contentType) return byName\n return byName.aliases?.includes(reported) ? byName : null\n}\n\n/** A stored type, as the list of types knows it; null for anything else. */\nexport function visitorUploadTypeOf(contentType: string): VisitorUploadType | null {\n return VISITOR_UPLOAD_TYPES.find((type) => type.contentType === contentType) ?? null\n}\n\n/** Whether a stored file may be shown as a picture rather than downloaded. */\nexport function visitorUploadIsInline(contentType: string): boolean {\n return visitorUploadTypeOf(contentType)?.inline === true\n}\n\n/**\n * A file name safe to keep and to put in a download's header: no folders, no\n * control or quoting characters, at most 120 characters, and ending in the\n * extension of the type it was taken as.\n */\nexport function safeVisitorUploadName(name: string, type: VisitorUploadType): string {\n const last = String(name ?? '').split(/[\\\\/]/).pop() ?? ''\n // Control characters, quotes and the characters a header or a path would read.\n const base = Array.from(last)\n .filter((character) => {\n const code = character.charCodeAt(0)\n return code >= 32 && code !== 127 && !'\"<>|:*?%;'.includes(character)\n })\n .join('')\n .replace(/\\s+/g, ' ')\n .trim()\n const extension = extensionOf(base)\n const stem = (type.extensions.includes(extension) ? base.slice(0, -extension.length) : base).trim()\n const kept = (stem || 'file').slice(0, 120 - 6).trim() || 'file'\n return `${kept}${type.extensions.includes(extension) ? extension : type.extensions[0]}`\n}\n\n/** What the browser asks to upload: a file's name, its reported type and its size. */\nexport interface VisitorUploadRequest {\n name: string\n contentType: string\n size: number\n}\n\n/** A request, checked: the type it is taken as and a safe name. */\nexport interface CheckedVisitorUpload {\n name: string\n type: VisitorUploadType\n size: number\n}\n\n/**\n * Checks what the browser asks to send, before anything is signed: one of\n * the types taken, not empty, not over the size. The answer's `error` names\n * the file and what to do.\n */\nexport function checkVisitorUploadRequest(\n raw: unknown,\n options: { maxBytes?: number; kinds?: readonly VisitorUploadKind[] } = {},\n): { ok: true; upload: CheckedVisitorUpload } | { ok: false; error: string; code: 'type' | 'size' | 'empty' } {\n const source = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n const name = typeof source['name'] === 'string' ? source['name'].slice(0, 400) : ''\n const size = Number(source['size'])\n const shown = name.split(/[\\\\/]/).pop()?.trim() || 'This file'\n const type = resolveVisitorUploadType(name, typeof source['contentType'] === 'string' ? source['contentType'] : '')\n if (!type || !visitorUploadTypeInKinds(type.contentType, options.kinds)) {\n return {\n ok: false,\n code: 'type',\n error: `${shown} can't be sent. Send a ${describeVisitorUploadTypes(options.kinds)} file.`,\n }\n }\n if (!Number.isSafeInteger(size) || size <= 0) {\n return { ok: false, code: 'empty', error: `${shown} is empty.` }\n }\n const maxBytes = Math.min(options.maxBytes ?? VISITOR_UPLOAD_MAX_BYTES, VISITOR_UPLOAD_MAX_BYTES)\n if (size > maxBytes) {\n return {\n ok: false,\n code: 'size',\n error: `${shown} is ${formatVisitorUploadBytes(size)}. Files can be up to ${formatVisitorUploadBytes(maxBytes)}.`,\n }\n }\n return { ok: true, upload: { name: safeVisitorUploadName(name, type), type, size } }\n}\n\n/** A file a visitor or a member sent, once it is checked and kept. */\nexport interface VisitorUploadFile {\n /** Unique within its purpose and record. */\n id: string\n /** The name it was sent with, made safe. */\n name: string\n contentType: string\n /** Bytes, as stored (an SVG may shrink when its script is stripped). */\n size: number\n /** The private Storage object. Never sent to a visitor. */\n path: string\n uploadedAtMs: number\n}\n\n/** A file as a visitor's page may see it: no storage path. */\nexport type VisitorUploadFileView = Omit<VisitorUploadFile, 'path'>\n\n/** The record without its storage path. */\nexport function visitorUploadFileView(file: VisitorUploadFile): VisitorUploadFileView {\n return { id: file.id, name: file.name, contentType: file.contentType, size: file.size, uploadedAtMs: file.uploadedAtMs }\n}\n\n/** A purpose or a record id as a path segment: letters, digits, `-` and `_`, and not too long. */\nconst SEGMENT = /^[A-Za-z0-9_-]{1,128}$/\n\nexport function isVisitorUploadSegment(value: unknown): value is string {\n return typeof value === 'string' && SEGMENT.test(value)\n}\n\n/** What an upload is for: the site, what kind of thing (`quote-artwork`), and which one. */\nexport interface VisitorUploadScope {\n hostId: string\n purpose: string\n purposeId: string\n}\n\n/** Whether every part of a scope can be a path segment. */\nexport function isVisitorUploadScope(scope: Partial<VisitorUploadScope>): scope is VisitorUploadScope {\n return isVisitorUploadSegment(scope.hostId) && isVisitorUploadSegment(scope.purpose) && isVisitorUploadSegment(scope.purposeId)\n}\n\n/** Where a file waits between the browser's upload and its check. */\nexport function visitorUploadPendingPath(scope: VisitorUploadScope, uploadId: string): string {\n return `${VISITOR_UPLOAD_PENDING_PREFIX}${scope.hostId}/${scope.purpose}/${scope.purposeId}/${uploadId}`\n}\n\n/** The folder every kept file of one purpose and record is in. */\nexport function visitorUploadFolder(scope: VisitorUploadScope): string {\n return `hosts/${scope.hostId}/private/uploads/${scope.purpose}/${scope.purposeId}/`\n}\n\n/** Where a checked file is kept: private to the site, with no download token. */\nexport function visitorUploadFinalPath(scope: VisitorUploadScope, uploadId: string): string {\n return `${visitorUploadFolder(scope)}${uploadId}`\n}\n\n/**\n * The files a browser picked, checked before any is sent: how many, and each\n * one's type and size. `already` is how many the submission holds.\n */\nexport function checkVisitorUploadSelection(\n files: ReadonlyArray<{ name: string; type: string; size: number }>,\n already = 0,\n maxFiles = VISITOR_UPLOAD_MAX_FILES,\n kinds?: readonly VisitorUploadKind[],\n): string | null {\n if (already + files.length > maxFiles) {\n return maxFiles === 1 ? 'Send one file.' : `Send up to ${maxFiles} files at a time.`\n }\n for (const file of files) {\n const checked = checkVisitorUploadRequest({ name: file.name, contentType: file.type, size: file.size }, { kinds })\n if ('error' in checked) return checked.error\n }\n return null\n}\n"],"names":["VISITOR_UPLOAD_MAX_BYTES","VISITOR_UPLOAD_MAX_FILES","VISITOR_UPLOAD_TICKET_TTL_MS","VISITOR_UPLOAD_PENDING_PREFIX","VISITOR_UPLOAD_HONEYPOT_FIELD","VISITOR_UPLOAD_TYPES","contentType","extensions","label","inline","aliases","UNKNOWN_TYPES","Set","VISITOR_UPLOAD_KINDS","VISITOR_UPLOAD_KIND_LABELS","images","pdf","design","KIND_TYPES","readVisitorUploadKinds","raw","listed","Array","isArray","split","kinds","filter","kind","some","entry","String","trim","length","typesOfKinds","wanted","flatMap","type","has","visitorUploadTypeInKinds","describeVisitorUploadTypes","labels","map","slice","join","visitorUploadAccept","VISITOR_UPLOAD_ACCEPT","formatVisitorUploadBytes","bytes","Math","max","round","mb","extensionOf","name","dot","lastIndexOf","toLowerCase","resolveVisitorUploadType","reportedType","byName","extension","reported","find","includes","visitorUploadTypeOf","visitorUploadIsInline","safeVisitorUploadName","last","pop","base","from","character","code","charCodeAt","replace","stem","kept","checkVisitorUploadRequest","options","source","size","Number","shown","ok","error","isSafeInteger","maxBytes","min","upload","visitorUploadFileView","file","id","uploadedAtMs","SEGMENT","isVisitorUploadSegment","value","test","isVisitorUploadScope","scope","hostId","purpose","purposeId","visitorUploadPendingPath","uploadId","visitorUploadFolder","visitorUploadFinalPath","checkVisitorUploadSelection","files","already","maxFiles","checked"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/visitor-upload.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 * FILES A VISITOR SENDS A BUSINESS (AGL-4096).\n *\n * Artwork for a quote, a logo with a request form: files someone without an\n * account hands a business through its site. This module is the policy, pure\n * and shared by the browser and the server:\n *\n * - which files are taken ({@link VISITOR_UPLOAD_TYPES}: PNG, JPEG, WebP,\n * GIF, PDF, SVG, AI, EPS, PSD, TIFF), how big ({@link VISITOR_UPLOAD_MAX_BYTES})\n * and how many at once ({@link VISITOR_UPLOAD_MAX_FILES});\n * - what a file's type IS ({@link resolveVisitorUploadType}): read from its\n * name and checked against what the browser said, so a `.psd` a browser\n * labels `application/octet-stream` is still a Photoshop file and a `.png`\n * labelled `text/html` is nothing;\n * - where its bytes live ({@link visitorUploadPendingPath} while it is on its\n * way, {@link visitorUploadFinalPath} once it is checked);\n * - the record a finished upload is ({@link VisitorUploadFile});\n * - how a kept file counts toward the workspace's storage\n * ({@link VISITOR_UPLOAD_COUNTER_BYTES}).\n *\n * The server half (`@aglyn/tenant-data-admin/server/visitor-upload`) signs the\n * upload, inspects the bytes and moves them; the browser half\n * (`visitor-upload-client.ts`) sends them. A plugin that takes files (commerce\n * for a quote's artwork, forms for a request) names its own `purpose` and the\n * record it is for, and decides for itself who may upload for it.\n *=========================================*/\n\n/** The most one file may weigh. */\nexport const VISITOR_UPLOAD_MAX_BYTES = 25 * 1024 * 1024\n\n/** The most files one submission carries. */\nexport const VISITOR_UPLOAD_MAX_FILES = 10\n\n/** How long the signed upload a ticket carries can be used, from its minting. */\nexport const VISITOR_UPLOAD_TICKET_TTL_MS = 15 * 60 * 1000\n\n/**\n * Where uploads wait until they are checked. Top-level, never under `hosts/`:\n * the bucket's lifecycle deletes whatever is still here a day after it\n * arrived (`cloud/media-bucket-lifecycle.json`), which no rule may do under a\n * prefix that holds live customer files.\n */\nexport const VISITOR_UPLOAD_PENDING_PREFIX = 'visitor-uploads-pending/'\n\n/** The honeypot field a form posts beside its files, as the forms plugin names it. */\nexport const VISITOR_UPLOAD_HONEYPOT_FIELD = 'website'\n\n/** One kind of file a visitor may send. */\nexport interface VisitorUploadType {\n /** The type the file is stored and served as. */\n contentType: string\n /** Lower-case, with the dot. The first is the one a file is renamed to when it has none. */\n extensions: readonly string[]\n /** What a person calls it. */\n label: string\n /**\n * Shown in the page as a picture. Only raster formats a browser draws and\n * that cannot carry script; everything else downloads.\n */\n inline: boolean\n /** Other types a browser reports for this kind of file. */\n aliases?: readonly string[]\n}\n\nexport const VISITOR_UPLOAD_TYPES: readonly VisitorUploadType[] = [\n { contentType: 'image/png', extensions: ['.png'], label: 'PNG', inline: true },\n { contentType: 'image/jpeg', extensions: ['.jpg', '.jpeg'], label: 'JPEG', inline: true, aliases: ['image/jpg', 'image/pjpeg'] },\n { contentType: 'image/webp', extensions: ['.webp'], label: 'WebP', inline: true },\n { contentType: 'image/gif', extensions: ['.gif'], label: 'GIF', inline: true },\n { contentType: 'application/pdf', extensions: ['.pdf'], label: 'PDF', inline: false, aliases: ['application/x-pdf'] },\n // Stripped of script on the way in, and still downloaded rather than drawn.\n { contentType: 'image/svg+xml', extensions: ['.svg'], label: 'SVG', inline: false },\n {\n contentType: 'application/illustrator',\n extensions: ['.ai'],\n label: 'AI',\n inline: false,\n aliases: ['application/postscript', 'application/pdf', 'application/vnd.adobe.illustrator'],\n },\n { contentType: 'application/postscript', extensions: ['.eps'], label: 'EPS', inline: false, aliases: ['application/eps', 'image/eps', 'image/x-eps', 'application/x-eps'] },\n {\n contentType: 'image/vnd.adobe.photoshop',\n extensions: ['.psd'],\n label: 'PSD',\n inline: false,\n aliases: ['application/x-photoshop', 'application/photoshop', 'application/psd', 'image/psd', 'image/x-photoshop'],\n },\n { contentType: 'image/tiff', extensions: ['.tif', '.tiff'], label: 'TIFF', inline: false, aliases: ['image/x-tiff'] },\n]\n\n/** Types a browser sends when it does not know; the name decides then. */\nconst UNKNOWN_TYPES = new Set(['', 'application/octet-stream', 'binary/octet-stream', 'application/unknown'])\n\n/**\n * The kinds of file a business can ask for, as a person picks them: photos and\n * pictures, PDFs, and design files. Each kind is a set of the types above, so\n * a field that takes only PDFs takes only `application/pdf` (AGL-4098).\n */\nexport type VisitorUploadKind = 'images' | 'pdf' | 'design'\n\nexport const VISITOR_UPLOAD_KINDS: readonly VisitorUploadKind[] = ['images', 'pdf', 'design']\n\n/** What a person calls each kind. */\nexport const VISITOR_UPLOAD_KIND_LABELS: Readonly<Record<VisitorUploadKind, string>> = {\n images: 'Images (PNG, JPEG, WebP, GIF)',\n pdf: 'PDF',\n design: 'Design files (SVG, AI, EPS, PSD, TIFF)',\n}\n\nconst KIND_TYPES: Readonly<Record<VisitorUploadKind, readonly string[]>> = {\n images: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'],\n pdf: ['application/pdf'],\n design: ['image/svg+xml', 'application/illustrator', 'application/postscript', 'image/vnd.adobe.photoshop', 'image/tiff'],\n}\n\n/** The kinds a stored list names, in order and without repeats; empty or unreadable is every kind. */\nexport function readVisitorUploadKinds(raw: unknown): VisitorUploadKind[] {\n const listed = Array.isArray(raw) ? raw : typeof raw === 'string' ? raw.split(',') : []\n const kinds = VISITOR_UPLOAD_KINDS.filter((kind) => listed.some((entry) => String(entry).trim() === kind))\n return kinds.length ? kinds : [...VISITOR_UPLOAD_KINDS]\n}\n\n/** The types the kinds take, or every type when none are named. */\nfunction typesOfKinds(kinds: readonly VisitorUploadKind[] | undefined): readonly VisitorUploadType[] {\n if (!kinds?.length) return VISITOR_UPLOAD_TYPES\n const wanted = new Set(kinds.flatMap((kind) => KIND_TYPES[kind] ?? []))\n return VISITOR_UPLOAD_TYPES.filter((type) => wanted.has(type.contentType))\n}\n\n/** Whether a stored type is one of the kinds; true for any type when no kinds are named. */\nexport function visitorUploadTypeInKinds(contentType: string, kinds?: readonly VisitorUploadKind[]): boolean {\n return typesOfKinds(kinds).some((type) => type.contentType === contentType)\n}\n\n/** `PNG, JPEG, WebP, GIF, PDF, SVG, AI, EPS, PSD or TIFF`, or the labels of the kinds named. */\nexport function describeVisitorUploadTypes(kinds?: readonly VisitorUploadKind[]): string {\n const labels = typesOfKinds(kinds).map((type) => type.label)\n if (labels.length === 1) return labels[0]\n return `${labels.slice(0, -1).join(', ')} or ${labels[labels.length - 1]}`\n}\n\n/** The file picker's `accept` for the kinds named: every extension and type they take. */\nexport function visitorUploadAccept(kinds?: readonly VisitorUploadKind[]): string {\n return typesOfKinds(kinds)\n .flatMap((type) => [...type.extensions, type.contentType])\n .join(',')\n}\n\n/** The file picker's `accept`: every extension and type taken. */\nexport const VISITOR_UPLOAD_ACCEPT = visitorUploadAccept()\n\n/** `25 MB`. */\nexport function formatVisitorUploadBytes(bytes: number): string {\n if (!(bytes > 0)) return '0 KB'\n if (bytes < 1024 * 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`\n const mb = bytes / (1024 * 1024)\n return `${mb >= 10 ? Math.round(mb) : Math.round(mb * 10) / 10} MB`\n}\n\n/** The extension of a file name, lower-case with its dot, or ''. */\nfunction extensionOf(name: string): string {\n const dot = name.lastIndexOf('.')\n return dot > 0 ? name.slice(dot).toLowerCase() : ''\n}\n\n/**\n * The type a file is taken as, or null when it is not one this takes. The\n * NAME decides, because a browser reports most design files as nothing or as\n * something generic; the reported type only has to agree with it (be this\n * type, one it is known by, or unknown). The bytes are checked against the\n * answer after they arrive.\n */\nexport function resolveVisitorUploadType(name: string, reportedType: string): VisitorUploadType | null {\n const extension = extensionOf(String(name ?? ''))\n const reported = String(reportedType ?? '').split(';')[0].trim().toLowerCase()\n const byName = VISITOR_UPLOAD_TYPES.find((type) => type.extensions.includes(extension))\n if (!byName) return null\n if (UNKNOWN_TYPES.has(reported) || reported === byName.contentType) return byName\n return byName.aliases?.includes(reported) ? byName : null\n}\n\n/** A stored type, as the list of types knows it; null for anything else. */\nexport function visitorUploadTypeOf(contentType: string): VisitorUploadType | null {\n return VISITOR_UPLOAD_TYPES.find((type) => type.contentType === contentType) ?? null\n}\n\n/** Whether a stored file may be shown as a picture rather than downloaded. */\nexport function visitorUploadIsInline(contentType: string): boolean {\n return visitorUploadTypeOf(contentType)?.inline === true\n}\n\n/**\n * A file name safe to keep and to put in a download's header: no folders, no\n * control or quoting characters, at most 120 characters, and ending in the\n * extension of the type it was taken as.\n */\nexport function safeVisitorUploadName(name: string, type: VisitorUploadType): string {\n const last = String(name ?? '').split(/[\\\\/]/).pop() ?? ''\n // Control characters, quotes and the characters a header or a path would read.\n const base = Array.from(last)\n .filter((character) => {\n const code = character.charCodeAt(0)\n return code >= 32 && code !== 127 && !'\"<>|:*?%;'.includes(character)\n })\n .join('')\n .replace(/\\s+/g, ' ')\n .trim()\n const extension = extensionOf(base)\n const stem = (type.extensions.includes(extension) ? base.slice(0, -extension.length) : base).trim()\n const kept = (stem || 'file').slice(0, 120 - 6).trim() || 'file'\n return `${kept}${type.extensions.includes(extension) ? extension : type.extensions[0]}`\n}\n\n/** What the browser asks to upload: a file's name, its reported type and its size. */\nexport interface VisitorUploadRequest {\n name: string\n contentType: string\n size: number\n}\n\n/** A request, checked: the type it is taken as and a safe name. */\nexport interface CheckedVisitorUpload {\n name: string\n type: VisitorUploadType\n size: number\n}\n\n/**\n * Checks what the browser asks to send, before anything is signed: one of\n * the types taken, not empty, not over the size. The answer's `error` names\n * the file and what to do.\n */\nexport function checkVisitorUploadRequest(\n raw: unknown,\n options: { maxBytes?: number; kinds?: readonly VisitorUploadKind[] } = {},\n): { ok: true; upload: CheckedVisitorUpload } | { ok: false; error: string; code: 'type' | 'size' | 'empty' } {\n const source = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n const name = typeof source['name'] === 'string' ? source['name'].slice(0, 400) : ''\n const size = Number(source['size'])\n const shown = name.split(/[\\\\/]/).pop()?.trim() || 'This file'\n const type = resolveVisitorUploadType(name, typeof source['contentType'] === 'string' ? source['contentType'] : '')\n if (!type || !visitorUploadTypeInKinds(type.contentType, options.kinds)) {\n return {\n ok: false,\n code: 'type',\n error: `${shown} can't be sent. Send a ${describeVisitorUploadTypes(options.kinds)} file.`,\n }\n }\n if (!Number.isSafeInteger(size) || size <= 0) {\n return { ok: false, code: 'empty', error: `${shown} is empty.` }\n }\n const maxBytes = Math.min(options.maxBytes ?? VISITOR_UPLOAD_MAX_BYTES, VISITOR_UPLOAD_MAX_BYTES)\n if (size > maxBytes) {\n return {\n ok: false,\n code: 'size',\n error: `${shown} is ${formatVisitorUploadBytes(size)}. Files can be up to ${formatVisitorUploadBytes(maxBytes)}.`,\n }\n }\n return { ok: true, upload: { name: safeVisitorUploadName(name, type), type, size } }\n}\n\n/** A file a visitor or a member sent, once it is checked and kept. */\nexport interface VisitorUploadFile {\n /** Unique within its purpose and record. */\n id: string\n /** The name it was sent with, made safe. */\n name: string\n contentType: string\n /** Bytes, as stored (an SVG may shrink when its script is stripped). */\n size: number\n /** The private Storage object. Never sent to a visitor. */\n path: string\n uploadedAtMs: number\n}\n\n/** A file as a visitor's page may see it: no storage path. */\nexport type VisitorUploadFileView = Omit<VisitorUploadFile, 'path'>\n\n/** The record without its storage path. */\nexport function visitorUploadFileView(file: VisitorUploadFile): VisitorUploadFileView {\n return { id: file.id, name: file.name, contentType: file.contentType, size: file.size, uploadedAtMs: file.uploadedAtMs }\n}\n\n/** A purpose or a record id as a path segment: letters, digits, `-` and `_`, and not too long. */\nconst SEGMENT = /^[A-Za-z0-9_-]{1,128}$/\n\nexport function isVisitorUploadSegment(value: unknown): value is string {\n return typeof value === 'string' && SEGMENT.test(value)\n}\n\n/** What an upload is for: the site, what kind of thing (`quote-artwork`), and which one. */\nexport interface VisitorUploadScope {\n hostId: string\n purpose: string\n purposeId: string\n}\n\n/** Whether every part of a scope can be a path segment. */\nexport function isVisitorUploadScope(scope: Partial<VisitorUploadScope>): scope is VisitorUploadScope {\n return isVisitorUploadSegment(scope.hostId) && isVisitorUploadSegment(scope.purpose) && isVisitorUploadSegment(scope.purposeId)\n}\n\n/** Where a file waits between the browser's upload and its check. */\nexport function visitorUploadPendingPath(scope: VisitorUploadScope, uploadId: string): string {\n return `${VISITOR_UPLOAD_PENDING_PREFIX}${scope.hostId}/${scope.purpose}/${scope.purposeId}/${uploadId}`\n}\n\n/** The folder every kept file of one purpose and record is in. */\nexport function visitorUploadFolder(scope: VisitorUploadScope): string {\n return `hosts/${scope.hostId}/private/uploads/${scope.purpose}/${scope.purposeId}/`\n}\n\n/** Where a checked file is kept: private to the site, with no download token. */\nexport function visitorUploadFinalPath(scope: VisitorUploadScope, uploadId: string): string {\n return `${visitorUploadFolder(scope)}${uploadId}`\n}\n\n/*------------------------------------------\n * Storage\n *-----------------------------------------*/\n\n/**\n * A kept file counts toward its workspace's storage the way a media library\n * file does. Its bytes are added to the site's storage counter,\n * `hosts/{hostId}/counters/media`, whose `bytes` is the one figure the\n * storage band, the Billing meter, the usage alerts and the invoice all read.\n * So a proof, a customer's file on a quote and a form's file are measured,\n * warned about and billed exactly as a library upload is, and no reader of\n * storage can leave them out.\n *\n * The same counter says how much of `bytes` is these files, in the two fields\n * below. That keeps `count` the number of library files and lets the media\n * library show its own share ({@link mediaLibraryBytes}).\n */\nexport const VISITOR_UPLOAD_COUNTER_BYTES = 'uploadBytes'\nexport const VISITOR_UPLOAD_COUNTER_FILES = 'uploadCount'\n\nconst counterBytes = (value: unknown): number => {\n const bytes = Number(value)\n return Number.isFinite(bytes) && bytes > 0 ? bytes : 0\n}\n\n/** The bytes of kept files a site's storage counter holds, never more than its `bytes`. */\nexport function visitorUploadCounterBytes(counter: Record<string, unknown> | null | undefined): number {\n return Math.min(counterBytes(counter?.['bytes']), counterBytes(counter?.[VISITOR_UPLOAD_COUNTER_BYTES]))\n}\n\n/** The media library's own files: the counter's `bytes` less the kept files counted beside them. */\nexport function mediaLibraryBytes(counter: Record<string, unknown> | null | undefined): number {\n return counterBytes(counter?.['bytes']) - visitorUploadCounterBytes(counter)\n}\n\n/**\n * The files a browser picked, checked before any is sent: how many, and each\n * one's type and size. `already` is how many the submission holds.\n */\nexport function checkVisitorUploadSelection(\n files: ReadonlyArray<{ name: string; type: string; size: number }>,\n already = 0,\n maxFiles = VISITOR_UPLOAD_MAX_FILES,\n kinds?: readonly VisitorUploadKind[],\n): string | null {\n if (already + files.length > maxFiles) {\n return maxFiles === 1 ? 'Send one file.' : `Send up to ${maxFiles} files at a time.`\n }\n for (const file of files) {\n const checked = checkVisitorUploadRequest({ name: file.name, contentType: file.type, size: file.size }, { kinds })\n if ('error' in checked) return checked.error\n }\n return null\n}\n"],"names":["VISITOR_UPLOAD_MAX_BYTES","VISITOR_UPLOAD_MAX_FILES","VISITOR_UPLOAD_TICKET_TTL_MS","VISITOR_UPLOAD_PENDING_PREFIX","VISITOR_UPLOAD_HONEYPOT_FIELD","VISITOR_UPLOAD_TYPES","contentType","extensions","label","inline","aliases","UNKNOWN_TYPES","Set","VISITOR_UPLOAD_KINDS","VISITOR_UPLOAD_KIND_LABELS","images","pdf","design","KIND_TYPES","readVisitorUploadKinds","raw","listed","Array","isArray","split","kinds","filter","kind","some","entry","String","trim","length","typesOfKinds","wanted","flatMap","type","has","visitorUploadTypeInKinds","describeVisitorUploadTypes","labels","map","slice","join","visitorUploadAccept","VISITOR_UPLOAD_ACCEPT","formatVisitorUploadBytes","bytes","Math","max","round","mb","extensionOf","name","dot","lastIndexOf","toLowerCase","resolveVisitorUploadType","reportedType","byName","extension","reported","find","includes","visitorUploadTypeOf","visitorUploadIsInline","safeVisitorUploadName","last","pop","base","from","character","code","charCodeAt","replace","stem","kept","checkVisitorUploadRequest","options","source","size","Number","shown","ok","error","isSafeInteger","maxBytes","min","upload","visitorUploadFileView","file","id","uploadedAtMs","SEGMENT","isVisitorUploadSegment","value","test","isVisitorUploadScope","scope","hostId","purpose","purposeId","visitorUploadPendingPath","uploadId","visitorUploadFolder","visitorUploadFinalPath","VISITOR_UPLOAD_COUNTER_BYTES","VISITOR_UPLOAD_COUNTER_FILES","counterBytes","isFinite","visitorUploadCounterBytes","counter","mediaLibraryBytes","checkVisitorUploadSelection","files","already","maxFiles","checked"],"mappings":"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"}
@@ -19,6 +19,7 @@ import type { HostTheme } from '@aglyn/shared-data-types';
19
19
  import type { ITimestamp } from '@aglyn/shared-util-timestamp';
20
20
  import type { AglynNodeSchema, FieldComponentType, NodeId } from './components.types';
21
21
  import type { MediaEmbeddedMetadata } from '../../app-utils/media-embedded-fields';
22
+ import type { HostStarterChoice } from '../../app-utils/site-styles';
22
23
  import type { MediaVideoMetadata } from '../../app-utils/media-metadata';
23
24
  import type { MediaVideoRendition } from '../../app-utils/media-ref';
24
25
  /**
@@ -476,6 +477,14 @@ export interface AglynHost extends AglynDocument {
476
477
  * born with the starter, which may still be offered it.
477
478
  */
478
479
  starterProvisionedAt?: unknown;
480
+ /**
481
+ * What the site was created with (AGL-4049): its style, the starter listing
482
+ * picked, and how it was started. Written by the server in the create's
483
+ * own transaction, and by the starter route when a site born for the AI
484
+ * start is given the starter in a style; never by a client. Absent on
485
+ * sites created before it.
486
+ */
487
+ starterChoice?: HostStarterChoice;
479
488
  /** Screen rendered (noindex) for unmatched paths (AGL-87). */
480
489
  notFoundScreenId?: ScreenUid;
481
490
  /**