@aglyn/aglyn 1.0.0-beta.199 → 1.0.0-beta.200
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 +11 -11
- package/src/lib/app-utils/activity-search.d.ts +39 -0
- package/src/lib/app-utils/activity-search.js +85 -0
- package/src/lib/app-utils/activity-search.js.map +1 -0
- package/src/lib/app-utils/admin-audit-index.d.ts +104 -0
- package/src/lib/app-utils/admin-audit-index.js +177 -0
- package/src/lib/app-utils/admin-audit-index.js.map +1 -0
- package/src/lib/app-utils/artifact-list-keys.d.ts +51 -0
- package/src/lib/app-utils/artifact-list-keys.js +144 -0
- package/src/lib/app-utils/artifact-list-keys.js.map +1 -0
- package/src/lib/app-utils/content-query-fields.d.ts +69 -0
- package/src/lib/app-utils/content-query-fields.js +79 -0
- package/src/lib/app-utils/content-query-fields.js.map +1 -0
- package/src/lib/app-utils/crm.d.ts +151 -1
- package/src/lib/app-utils/crm.js +388 -1
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/dataset-models.d.ts +75 -24
- package/src/lib/app-utils/dataset-models.js +107 -40
- package/src/lib/app-utils/dataset-models.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +40 -40
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/email-search.d.ts +36 -0
- package/src/lib/app-utils/email-search.js +53 -0
- package/src/lib/app-utils/email-search.js.map +1 -0
- package/src/lib/app-utils/forms.d.ts +83 -3
- package/src/lib/app-utils/forms.js +67 -0
- package/src/lib/app-utils/forms.js.map +1 -1
- package/src/lib/app-utils/marketplace-listing-visibility.d.ts +7 -6
- package/src/lib/app-utils/marketplace-listing-visibility.js +12 -10
- package/src/lib/app-utils/marketplace-listing-visibility.js.map +1 -1
- package/src/lib/app-utils/media-metadata.d.ts +79 -0
- package/src/lib/app-utils/media-metadata.js +74 -0
- package/src/lib/app-utils/media-metadata.js.map +1 -1
- package/src/lib/app-utils/message-search.d.ts +40 -0
- package/src/lib/app-utils/message-search.js +168 -0
- package/src/lib/app-utils/message-search.js.map +1 -0
- package/src/lib/app-utils/name-search.d.ts +83 -0
- package/src/lib/app-utils/name-search.js +97 -0
- package/src/lib/app-utils/name-search.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +2 -0
- package/src/lib/app-utils/server.js +2 -0
- package/src/lib/app-utils/server.js.map +1 -1
|
@@ -101,12 +101,13 @@ export declare function isListingBrowsable(listing: {
|
|
|
101
101
|
* `aglyn:addons`. `marketplace.ts` re-exports them so publishing code keeps
|
|
102
102
|
* one import site.
|
|
103
103
|
*
|
|
104
|
-
* The list is short on purpose.
|
|
105
|
-
* document
|
|
106
|
-
* seller panel,
|
|
107
|
-
*
|
|
108
|
-
* classified writable because a publisher OWNS them,
|
|
109
|
-
* would break: denying the rest cost nothing, which is
|
|
104
|
+
* The list is short on purpose. Nothing in the repo writes a listing
|
|
105
|
+
* document client-side any more: the unpublish/republish toggle in the org
|
|
106
|
+
* seller panel was the last, and it goes through the publish API since
|
|
107
|
+
* AGL-3321 — as publish, content edits, review and install always did. So
|
|
108
|
+
* the fields below are classified writable because a publisher OWNS them,
|
|
109
|
+
* not because anything would break: denying the rest cost nothing, which is
|
|
110
|
+
* how AGL-1364 closed.
|
|
110
111
|
*/
|
|
111
112
|
export declare const LISTING_CLIENT_WRITABLE_FIELDS: Readonly<Record<string, string>>;
|
|
112
113
|
/**
|
|
@@ -99,15 +99,19 @@
|
|
|
99
99
|
* `aglyn:addons`. `marketplace.ts` re-exports them so publishing code keeps
|
|
100
100
|
* one import site.
|
|
101
101
|
*
|
|
102
|
-
* The list is short on purpose.
|
|
103
|
-
* document
|
|
104
|
-
* seller panel,
|
|
105
|
-
*
|
|
106
|
-
* classified writable because a publisher OWNS them,
|
|
107
|
-
* would break: denying the rest cost nothing, which is
|
|
102
|
+
* The list is short on purpose. Nothing in the repo writes a listing
|
|
103
|
+
* document client-side any more: the unpublish/republish toggle in the org
|
|
104
|
+
* seller panel was the last, and it goes through the publish API since
|
|
105
|
+
* AGL-3321 — as publish, content edits, review and install always did. So
|
|
106
|
+
* the fields below are classified writable because a publisher OWNS them,
|
|
107
|
+
* not because anything would break: denying the rest cost nothing, which is
|
|
108
|
+
* how AGL-1364 closed.
|
|
108
109
|
*/ export const LISTING_CLIENT_WRITABLE_FIELDS = {
|
|
109
|
-
displayName
|
|
110
|
-
|
|
110
|
+
// `displayName` was here until AGL-3321: the marketplace lists search it
|
|
111
|
+
// through keys the server derives, so it is written through the API
|
|
112
|
+
// (`update-listing`) alone and denied here with `deletedAt` and
|
|
113
|
+
// `visibility`, the two other fields a listing's browse audience follows.
|
|
114
|
+
description: 'Publisher-authored storefront copy, validated by ' + '`validateListingContent` on the API path and sanitized at render — ' + 'renderers never trust this document.',
|
|
111
115
|
category: 'The single legacy category. Publisher-chosen shelf placement; it ' + 'orders a browse page and gates nothing.',
|
|
112
116
|
categories: 'The AGL-430 multi-category list. Same reasoning as `category` — ' + 'placement, not permission.',
|
|
113
117
|
readme: 'Markdown documentation for the listing page (no raw HTML). ' + 'Publisher-authored and sanitized at render.',
|
|
@@ -123,8 +127,6 @@
|
|
|
123
127
|
homepageUrl: 'Publisher-authored outbound link, sanitized at render. It points ' + 'away from the platform and decides nothing here.',
|
|
124
128
|
repositoryUrl: 'Publisher-authored outbound link. Same as `homepageUrl`.',
|
|
125
129
|
license: 'An SPDX-ish label the publisher asserts about their own code. A claim ' + 'shown to buyers, not a permission the platform grants.',
|
|
126
|
-
deletedAt: 'The unpublish/republish toggle — the ONE field the console writes ' + 'client-side, and the publisher\'s own decision to withdraw their ' + 'listing. Staff takedown is `hiddenAt`, which is denied precisely so ' + 'an owner cannot un-hide themselves.',
|
|
127
|
-
visibility: 'Private vs public (AGL-968). Genuinely the publisher\'s choice, and ' + 'it only ever NARROWS reach: a private plugin faces the identical ' + 'review bar, and the review gate a publisher might try to escape by ' + 'relabelling is closed by denying `artifactType`/`type`/`kind`.',
|
|
128
130
|
pluginId: 'The manifest id a plugin listing declares (AGL-45). Rewriting it ' + 'breaks the publisher\'s own install resolution; the artifact that ' + 'actually runs is pinned by sha256 in the staff-only `pluginVersions`.'
|
|
129
131
|
};
|
|
130
132
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/marketplace-listing-visibility.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 * Who is allowed to see a marketplace listing (AGL-432/658/968/1196).\n *\n * These four predicates live in core for the same reason\n * `MarketplaceArtifactType` (AGL-1016) and the verification policy (AGL-1217)\n * do: a `scope:app` project may not depend on an `aglyn:addons` lib, and the\n * console now has to ask the question outside the marketplace plugin. The\n * listing route's `generateMetadata` (AGL-876) builds a social card from the\n * listing document, and that card is emitted to an UNAUTHENTICATED fetcher —\n * so it has to apply the same visibility rule the browse grid applies, or a\n * rejected, private or taken-down listing gets its name and description\n * unfurled into a chat message by anyone holding the URL.\n *\n * They were moved rather than copied. A second copy of \"is this listing\n * public\" is the failure mode this file exists to prevent: the rule already\n * gates browse AND install (AGL-965 added the second call site after the\n * first proved insufficient on its own), and a third reading that drifts\n * would be a disclosure bug, not a cosmetic one.\n *\n * `libs/plugins/marketplace/src/lib/model/marketplace.ts` re-exports all four,\n * so publishing and install code keeps importing them from one place.\n */\n\nimport type { MarketplaceArtifactType } from './marketplace-provenance'\n\n/**\n * The listing's artifact type, tolerating the pre-AGL-654 shape.\n *\n * Legacy listings carry `type`/`kind` instead; a component was the absence\n * of both, which is why this defaults there rather than throwing. Keeping\n * the fallback means old docs keep resolving correctly instead of silently\n * becoming un-installable.\n */\nexport function listingArtifactType(listing: {\n artifactType?: string\n type?: string\n kind?: string\n}): MarketplaceArtifactType {\n if (listing.artifactType) return listing.artifactType as MarketplaceArtifactType\n if (listing.kind === 'template') return 'template'\n if (listing.type === 'plugin') return 'plugin'\n return 'component'\n}\n\n/**\n * Private plugins (AGL-968): an org builds for its own sites without\n * publishing to anyone. Never browsable — not even for the owner, who\n * reaches it through their own plugins area — and installable only by the\n * owning org (enforced in `install-plugin`, not in the UI). The review bar\n * is identical: private code still runs on our infrastructure.\n */\nexport function isPrivateListing(listing: { visibility?: string }): boolean {\n return listing.visibility === 'private'\n}\n\n/**\n * Whether a listing has been soft-deleted (AGL-1196).\n *\n * Browse used to express this as `where('deletedAt','==',null)` in the query\n * itself. That put a MUTABLE field in a predicate: `deletedAt` flips on every\n * unpublish/republish, and a document that stops matching a live query can\n * leave a `noDocument` tombstone at its own path — which the detail page then\n * reads BY ID and 404s on (AGL-827, AGL-929). Filtering in memory removes the\n * mechanism instead of healing it.\n *\n * FALSY, not `=== null`. Firestore's `== null` matches only an explicit null\n * and cannot express \"field is absent\", so a listing written without the field\n * was invisible to the old query. Absent means live.\n *\n * Deliberately NOT folded into `isListingBrowsable`: that check carries an\n * owner exemption, so a publisher can watch their own submission move through\n * review. Deletion has no such exemption — a deleted listing is gone for\n * everyone, including the org that owns it.\n */\nexport function isListingDeleted(listing: { deletedAt?: unknown }): boolean {\n return Boolean(listing.deletedAt)\n}\n\n/** Whether a plugin listing is publicly browsable (AGL-432). */\nexport function isListingBrowsable(listing: {\n artifactType?: string\n type?: string\n kind?: string\n reviewStatus?: string\n hiddenAt?: unknown\n visibility?: string\n}): boolean {\n // Staff takedown applies to EVERY artifact type (AGL-658). Pre-publication\n // review is plugin-only — plugins execute code, so they earn the wait —\n // but a component or template that turns out to be abusive must be\n // removable too, and before this it simply was not: the early return\n // below meant anything non-plugin was permanently browsable.\n if (listing.hiddenAt) return false\n // Private listings never reach the marketplace (AGL-968).\n if (isPrivateListing(listing)) return false\n if (listingArtifactType(listing) !== 'plugin') return true\n return (\n listing.reviewStatus === undefined ||\n listing.reviewStatus === 'listed' ||\n listing.reviewStatus === 'verified'\n )\n}\n\n/**\n * Listing fields a publisher may legitimately write, each with the reason\n * (AGL-1361). Everything else must be denied in the rules' listing key diff.\n *\n * They live here rather than beside `MarketplaceListing` for the same reason\n * the visibility policy does: this module is what READS the fields that decide\n * whether a listing is browsable, and `scope:app` may not depend on\n * `aglyn:addons`. `marketplace.ts` re-exports them so publishing code keeps\n * one import site.\n *\n * The list is short on purpose. The only client-side write to a listing\n * document anywhere in the repo is the unpublish/republish toggle in the org\n * seller panel, which writes `deletedAt` alone — publish, content edits,\n * review and install all go through Admin-SDK routes. So the fields below are\n * classified writable because a publisher OWNS them, not because anything\n * would break: denying the rest cost nothing, which is how AGL-1364 closed.\n */\nexport const LISTING_CLIENT_WRITABLE_FIELDS: Readonly<Record<string, string>> =\n {\n displayName:\n 'The listing title. Publisher-authored storefront copy, validated by ' +\n '`validateListingContent` on the API path and sanitized at render — ' +\n 'renderers never trust this document.',\n description: 'Publisher-authored storefront copy. Same handling as `displayName`.',\n category:\n 'The single legacy category. Publisher-chosen shelf placement; it ' +\n 'orders a browse page and gates nothing.',\n categories:\n 'The AGL-430 multi-category list. Same reasoning as `category` — ' +\n 'placement, not permission.',\n readme:\n 'Markdown documentation for the listing page (no raw HTML). ' +\n 'Publisher-authored and sanitized at render.',\n // `logoUrl` and `screenshots` were here, justified as \"sanitized at\n // render\" (AGL-1701). They were not: listing-content.component.tsx hands\n // both straight to an `<img src>`, and `logoUrl` additionally becomes an\n // `og:image` served to unauthenticated unfurlers. Publisher-authored\n // ARTWORK is not like publisher-authored COPY — copy is read by the\n // viewer, artwork is FETCHED by the viewer's browser from whatever host\n // the string names, disclosing their IP and a `Referer` naming the\n // console and their org to a recipient on no subprocessor register.\n // Denied in the rules and held to first-party media on the API path.\n homepageUrl:\n 'Publisher-authored outbound link, sanitized at render. It points ' +\n 'away from the platform and decides nothing here.',\n repositoryUrl: 'Publisher-authored outbound link. Same as `homepageUrl`.',\n license:\n 'An SPDX-ish label the publisher asserts about their own code. A claim ' +\n 'shown to buyers, not a permission the platform grants.',\n deletedAt:\n 'The unpublish/republish toggle — the ONE field the console writes ' +\n 'client-side, and the publisher\\'s own decision to withdraw their ' +\n 'listing. Staff takedown is `hiddenAt`, which is denied precisely so ' +\n 'an owner cannot un-hide themselves.',\n visibility:\n 'Private vs public (AGL-968). Genuinely the publisher\\'s choice, and ' +\n 'it only ever NARROWS reach: a private plugin faces the identical ' +\n 'review bar, and the review gate a publisher might try to escape by ' +\n 'relabelling is closed by denying `artifactType`/`type`/`kind`.',\n pluginId:\n 'The manifest id a plugin listing declares (AGL-45). Rewriting it ' +\n 'breaks the publisher\\'s own install resolution; the artifact that ' +\n 'actually runs is pinned by sha256 in the staff-only `pluginVersions`.',\n }\n\n/**\n * Listing keys that never reach Firestore, so the rules have no opinion.\n *\n * Empty today — `MarketplaceListing` declares no `$id`, because listing code\n * carries the id alongside the data rather than folding it in. Kept as the\n * partition's third arm so a future projection field has a home that is not\n * \"client-writable\".\n */\nexport const LISTING_UNPERSISTED_FIELDS: Readonly<Record<string, string>> = {}\n"],"names":["listingArtifactType","listing","artifactType","kind","type","isPrivateListing","visibility","isListingDeleted","Boolean","deletedAt","isListingBrowsable","hiddenAt","reviewStatus","undefined","LISTING_CLIENT_WRITABLE_FIELDS","displayName","description","category","categories","readme","homepageUrl","repositoryUrl","license","pluginId","LISTING_UNPERSISTED_FIELDS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;CAqBC,GAID;;;;;;;CAOC,GACD,OAAO,SAASA,oBAAoBC,OAInC;IACC,IAAIA,QAAQC,YAAY,EAAE,OAAOD,QAAQC,YAAY;IACrD,IAAID,QAAQE,IAAI,KAAK,YAAY,OAAO;IACxC,IAAIF,QAAQG,IAAI,KAAK,UAAU,OAAO;IACtC,OAAO;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,iBAAiBJ,OAAgC;IAC/D,OAAOA,QAAQK,UAAU,KAAK;AAChC;AAEA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,iBAAiBN,OAAgC;IAC/D,OAAOO,QAAQP,QAAQQ,SAAS;AAClC;AAEA,8DAA8D,GAC9D,OAAO,SAASC,mBAAmBT,OAOlC;IACC,2EAA2E;IAC3E,wEAAwE;IACxE,mEAAmE;IACnE,qEAAqE;IACrE,6DAA6D;IAC7D,IAAIA,QAAQU,QAAQ,EAAE,OAAO;IAC7B,0DAA0D;IAC1D,IAAIN,iBAAiBJ,UAAU,OAAO;IACtC,IAAID,oBAAoBC,aAAa,UAAU,OAAO;IACtD,OACEA,QAAQW,YAAY,KAAKC,aACzBZ,QAAQW,YAAY,KAAK,YACzBX,QAAQW,YAAY,KAAK;AAE7B;AAEA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAME,iCACX;IACEC,aACE,yEACA,wEACA;IACFC,aAAa;IACbC,UACE,sEACA;IACFC,YACE,qEACA;IACFC,QACE,gEACA;IACF,oEAAoE;IACpE,yEAAyE;IACzE,yEAAyE;IACzE,qEAAqE;IACrE,oEAAoE;IACpE,wEAAwE;IACxE,mEAAmE;IACnE,oEAAoE;IACpE,qEAAqE;IACrEC,aACE,sEACA;IACFC,eAAe;IACfC,SACE,2EACA;IACFb,WACE,uEACA,sEACA,yEACA;IACFH,YACE,yEACA,sEACA,wEACA;IACFiB,UACE,sEACA,uEACA;AACJ,EAAC;AAEH;;;;;;;CAOC,GACD,OAAO,MAAMC,6BAA+D,CAAC,EAAC"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/marketplace-listing-visibility.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 * Who is allowed to see a marketplace listing (AGL-432/658/968/1196).\n *\n * These four predicates live in core for the same reason\n * `MarketplaceArtifactType` (AGL-1016) and the verification policy (AGL-1217)\n * do: a `scope:app` project may not depend on an `aglyn:addons` lib, and the\n * console now has to ask the question outside the marketplace plugin. The\n * listing route's `generateMetadata` (AGL-876) builds a social card from the\n * listing document, and that card is emitted to an UNAUTHENTICATED fetcher —\n * so it has to apply the same visibility rule the browse grid applies, or a\n * rejected, private or taken-down listing gets its name and description\n * unfurled into a chat message by anyone holding the URL.\n *\n * They were moved rather than copied. A second copy of \"is this listing\n * public\" is the failure mode this file exists to prevent: the rule already\n * gates browse AND install (AGL-965 added the second call site after the\n * first proved insufficient on its own), and a third reading that drifts\n * would be a disclosure bug, not a cosmetic one.\n *\n * `libs/plugins/marketplace/src/lib/model/marketplace.ts` re-exports all four,\n * so publishing and install code keeps importing them from one place.\n */\n\nimport type { MarketplaceArtifactType } from './marketplace-provenance'\n\n/**\n * The listing's artifact type, tolerating the pre-AGL-654 shape.\n *\n * Legacy listings carry `type`/`kind` instead; a component was the absence\n * of both, which is why this defaults there rather than throwing. Keeping\n * the fallback means old docs keep resolving correctly instead of silently\n * becoming un-installable.\n */\nexport function listingArtifactType(listing: {\n artifactType?: string\n type?: string\n kind?: string\n}): MarketplaceArtifactType {\n if (listing.artifactType) return listing.artifactType as MarketplaceArtifactType\n if (listing.kind === 'template') return 'template'\n if (listing.type === 'plugin') return 'plugin'\n return 'component'\n}\n\n/**\n * Private plugins (AGL-968): an org builds for its own sites without\n * publishing to anyone. Never browsable — not even for the owner, who\n * reaches it through their own plugins area — and installable only by the\n * owning org (enforced in `install-plugin`, not in the UI). The review bar\n * is identical: private code still runs on our infrastructure.\n */\nexport function isPrivateListing(listing: { visibility?: string }): boolean {\n return listing.visibility === 'private'\n}\n\n/**\n * Whether a listing has been soft-deleted (AGL-1196).\n *\n * Browse used to express this as `where('deletedAt','==',null)` in the query\n * itself. That put a MUTABLE field in a predicate: `deletedAt` flips on every\n * unpublish/republish, and a document that stops matching a live query can\n * leave a `noDocument` tombstone at its own path — which the detail page then\n * reads BY ID and 404s on (AGL-827, AGL-929). Filtering in memory removes the\n * mechanism instead of healing it.\n *\n * FALSY, not `=== null`. Firestore's `== null` matches only an explicit null\n * and cannot express \"field is absent\", so a listing written without the field\n * was invisible to the old query. Absent means live.\n *\n * Deliberately NOT folded into `isListingBrowsable`: that check carries an\n * owner exemption, so a publisher can watch their own submission move through\n * review. Deletion has no such exemption — a deleted listing is gone for\n * everyone, including the org that owns it.\n */\nexport function isListingDeleted(listing: { deletedAt?: unknown }): boolean {\n return Boolean(listing.deletedAt)\n}\n\n/** Whether a plugin listing is publicly browsable (AGL-432). */\nexport function isListingBrowsable(listing: {\n artifactType?: string\n type?: string\n kind?: string\n reviewStatus?: string\n hiddenAt?: unknown\n visibility?: string\n}): boolean {\n // Staff takedown applies to EVERY artifact type (AGL-658). Pre-publication\n // review is plugin-only — plugins execute code, so they earn the wait —\n // but a component or template that turns out to be abusive must be\n // removable too, and before this it simply was not: the early return\n // below meant anything non-plugin was permanently browsable.\n if (listing.hiddenAt) return false\n // Private listings never reach the marketplace (AGL-968).\n if (isPrivateListing(listing)) return false\n if (listingArtifactType(listing) !== 'plugin') return true\n return (\n listing.reviewStatus === undefined ||\n listing.reviewStatus === 'listed' ||\n listing.reviewStatus === 'verified'\n )\n}\n\n/**\n * Listing fields a publisher may legitimately write, each with the reason\n * (AGL-1361). Everything else must be denied in the rules' listing key diff.\n *\n * They live here rather than beside `MarketplaceListing` for the same reason\n * the visibility policy does: this module is what READS the fields that decide\n * whether a listing is browsable, and `scope:app` may not depend on\n * `aglyn:addons`. `marketplace.ts` re-exports them so publishing code keeps\n * one import site.\n *\n * The list is short on purpose. Nothing in the repo writes a listing\n * document client-side any more: the unpublish/republish toggle in the org\n * seller panel was the last, and it goes through the publish API since\n * AGL-3321 — as publish, content edits, review and install always did. So\n * the fields below are classified writable because a publisher OWNS them,\n * not because anything would break: denying the rest cost nothing, which is\n * how AGL-1364 closed.\n */\nexport const LISTING_CLIENT_WRITABLE_FIELDS: Readonly<Record<string, string>> =\n {\n // `displayName` was here until AGL-3321: the marketplace lists search it\n // through keys the server derives, so it is written through the API\n // (`update-listing`) alone and denied here with `deletedAt` and\n // `visibility`, the two other fields a listing's browse audience follows.\n description:\n 'Publisher-authored storefront copy, validated by ' +\n '`validateListingContent` on the API path and sanitized at render — ' +\n 'renderers never trust this document.',\n category:\n 'The single legacy category. Publisher-chosen shelf placement; it ' +\n 'orders a browse page and gates nothing.',\n categories:\n 'The AGL-430 multi-category list. Same reasoning as `category` — ' +\n 'placement, not permission.',\n readme:\n 'Markdown documentation for the listing page (no raw HTML). ' +\n 'Publisher-authored and sanitized at render.',\n // `logoUrl` and `screenshots` were here, justified as \"sanitized at\n // render\" (AGL-1701). They were not: listing-content.component.tsx hands\n // both straight to an `<img src>`, and `logoUrl` additionally becomes an\n // `og:image` served to unauthenticated unfurlers. Publisher-authored\n // ARTWORK is not like publisher-authored COPY — copy is read by the\n // viewer, artwork is FETCHED by the viewer's browser from whatever host\n // the string names, disclosing their IP and a `Referer` naming the\n // console and their org to a recipient on no subprocessor register.\n // Denied in the rules and held to first-party media on the API path.\n homepageUrl:\n 'Publisher-authored outbound link, sanitized at render. It points ' +\n 'away from the platform and decides nothing here.',\n repositoryUrl: 'Publisher-authored outbound link. Same as `homepageUrl`.',\n license:\n 'An SPDX-ish label the publisher asserts about their own code. A claim ' +\n 'shown to buyers, not a permission the platform grants.',\n pluginId:\n 'The manifest id a plugin listing declares (AGL-45). Rewriting it ' +\n 'breaks the publisher\\'s own install resolution; the artifact that ' +\n 'actually runs is pinned by sha256 in the staff-only `pluginVersions`.',\n }\n\n/**\n * Listing keys that never reach Firestore, so the rules have no opinion.\n *\n * Empty today — `MarketplaceListing` declares no `$id`, because listing code\n * carries the id alongside the data rather than folding it in. Kept as the\n * partition's third arm so a future projection field has a home that is not\n * \"client-writable\".\n */\nexport const LISTING_UNPERSISTED_FIELDS: Readonly<Record<string, string>> = {}\n"],"names":["listingArtifactType","listing","artifactType","kind","type","isPrivateListing","visibility","isListingDeleted","Boolean","deletedAt","isListingBrowsable","hiddenAt","reviewStatus","undefined","LISTING_CLIENT_WRITABLE_FIELDS","description","category","categories","readme","homepageUrl","repositoryUrl","license","pluginId","LISTING_UNPERSISTED_FIELDS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;CAqBC,GAID;;;;;;;CAOC,GACD,OAAO,SAASA,oBAAoBC,OAInC;IACC,IAAIA,QAAQC,YAAY,EAAE,OAAOD,QAAQC,YAAY;IACrD,IAAID,QAAQE,IAAI,KAAK,YAAY,OAAO;IACxC,IAAIF,QAAQG,IAAI,KAAK,UAAU,OAAO;IACtC,OAAO;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,iBAAiBJ,OAAgC;IAC/D,OAAOA,QAAQK,UAAU,KAAK;AAChC;AAEA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,iBAAiBN,OAAgC;IAC/D,OAAOO,QAAQP,QAAQQ,SAAS;AAClC;AAEA,8DAA8D,GAC9D,OAAO,SAASC,mBAAmBT,OAOlC;IACC,2EAA2E;IAC3E,wEAAwE;IACxE,mEAAmE;IACnE,qEAAqE;IACrE,6DAA6D;IAC7D,IAAIA,QAAQU,QAAQ,EAAE,OAAO;IAC7B,0DAA0D;IAC1D,IAAIN,iBAAiBJ,UAAU,OAAO;IACtC,IAAID,oBAAoBC,aAAa,UAAU,OAAO;IACtD,OACEA,QAAQW,YAAY,KAAKC,aACzBZ,QAAQW,YAAY,KAAK,YACzBX,QAAQW,YAAY,KAAK;AAE7B;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,MAAME,iCACX;IACE,yEAAyE;IACzE,oEAAoE;IACpE,gEAAgE;IAChE,0EAA0E;IAC1EC,aACE,sDACA,wEACA;IACFC,UACE,sEACA;IACFC,YACE,qEACA;IACFC,QACE,gEACA;IACF,oEAAoE;IACpE,yEAAyE;IACzE,yEAAyE;IACzE,qEAAqE;IACrE,oEAAoE;IACpE,wEAAwE;IACxE,mEAAmE;IACnE,oEAAoE;IACpE,qEAAqE;IACrEC,aACE,sEACA;IACFC,eAAe;IACfC,SACE,2EACA;IACFC,UACE,sEACA,uEACA;AACJ,EAAC;AAEH;;;;;;;CAOC,GACD,OAAO,MAAMC,6BAA+D,CAAC,EAAC"}
|
|
@@ -15,6 +15,85 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
export * from './media-alt';
|
|
18
|
+
/**
|
|
19
|
+
* The family a stored file belongs to, as the media library's Type filter
|
|
20
|
+
* names it (AGL-3327). Stored on the document as `kind`, because a Firestore
|
|
21
|
+
* query can ask for a value and cannot ask for a prefix beside a list's own
|
|
22
|
+
* sort: `kind == 'image'` sits on any query the library builds, where a
|
|
23
|
+
* `contentType` range would take the one range field the query has.
|
|
24
|
+
*
|
|
25
|
+
* Every type media ingress accepts that is not a picture, a film or a PDF is
|
|
26
|
+
* a document — ZIP, Word, Excel, PowerPoint, CSV, text, Markdown, JSON — and
|
|
27
|
+
* so is anything older than that allowlist.
|
|
28
|
+
*/
|
|
29
|
+
export type MediaKind = 'image' | 'video' | 'pdf' | 'document';
|
|
30
|
+
export declare const MEDIA_KINDS: readonly MediaKind[];
|
|
31
|
+
export declare function mediaKindOf(contentType: unknown): MediaKind;
|
|
32
|
+
/** Which way a picture or a film is longer, as the Orientation filter asks. */
|
|
33
|
+
export type MediaOrientation = 'landscape' | 'portrait' | 'square';
|
|
34
|
+
export declare const MEDIA_ORIENTATIONS: readonly MediaOrientation[];
|
|
35
|
+
/**
|
|
36
|
+
* The orientation of a stored file (AGL-3327), from the pixel size it
|
|
37
|
+
* carries: a picture's `width` and `height`, or a film's `video.width` and
|
|
38
|
+
* `video.height` (a video's own `width`/`height` are never written — see
|
|
39
|
+
* `videoMediaProps`). Null when neither pair is whole, which a query for any
|
|
40
|
+
* orientation then leaves out rather than guesses at.
|
|
41
|
+
*/
|
|
42
|
+
export declare function mediaOrientationOf(media: {
|
|
43
|
+
width?: unknown;
|
|
44
|
+
height?: unknown;
|
|
45
|
+
video?: unknown;
|
|
46
|
+
}): MediaOrientation | null;
|
|
47
|
+
/**
|
|
48
|
+
* A file name as words (AGL-3327): every run of characters that is not a
|
|
49
|
+
* letter or a digit becomes a space, so `hero-banner_2x.png` is the four
|
|
50
|
+
* words a person would search it by. The shared name search splits on
|
|
51
|
+
* spaces only, which suits a person's name and misses nearly every file's.
|
|
52
|
+
*/
|
|
53
|
+
export declare function mediaNameWords(fileName: unknown): string;
|
|
54
|
+
/**
|
|
55
|
+
* The one token a library search asks for (AGL-3327): the first word of what
|
|
56
|
+
* was typed, folded the way `mediaNameWords` folds a stored name, lower-cased
|
|
57
|
+
* and capped as `nameSearchToken` caps it. A query holds one array filter, so
|
|
58
|
+
* a search is one word.
|
|
59
|
+
*/
|
|
60
|
+
export declare function mediaSearchToken(query: unknown): string;
|
|
61
|
+
/** The derived fields every writer of a media document stores beside it. */
|
|
62
|
+
export interface MediaFilterKeys {
|
|
63
|
+
kind: MediaKind;
|
|
64
|
+
/**
|
|
65
|
+
* `nameSearchKey` of the file name: the Name sort, and the "starts with"
|
|
66
|
+
* range a search is served by where the scope clause holds the query's one
|
|
67
|
+
* array filter.
|
|
68
|
+
*/
|
|
69
|
+
nameLower: string;
|
|
70
|
+
/** Word-prefix tokens of the file name's words, for search. */
|
|
71
|
+
nameTokens: string[];
|
|
72
|
+
/** Whether alt text is written — "missing" is a value a query can find. */
|
|
73
|
+
hasAlt: boolean;
|
|
74
|
+
/** Which way the file is longer, or null when its size is not known. */
|
|
75
|
+
orientation: MediaOrientation | null;
|
|
76
|
+
}
|
|
77
|
+
/** What `mediaFilterKeys` reads off a media document. */
|
|
78
|
+
export interface MediaFilterSource {
|
|
79
|
+
fileName?: unknown;
|
|
80
|
+
contentType?: unknown;
|
|
81
|
+
alt?: unknown;
|
|
82
|
+
width?: unknown;
|
|
83
|
+
height?: unknown;
|
|
84
|
+
video?: unknown;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* What the media library filters, sorts and searches by, derived from the
|
|
88
|
+
* fields a document already carries (AGL-3327). Every writer — the upload
|
|
89
|
+
* routes, replace, the REST API, restore, the Details drawer — spreads this
|
|
90
|
+
* beside the fields it writes, derived from the document as it will stand
|
|
91
|
+
* after the write, so a query finds every file by it. The backfill
|
|
92
|
+
* (`tools/scripts/backfill-media-filter-keys.mjs`) stamps the documents
|
|
93
|
+
* written before it existed, through `tools/scripts/lib/media-filter-keys.mjs`;
|
|
94
|
+
* both are held to `media-filter-keys.fixtures.json` beside it.
|
|
95
|
+
*/
|
|
96
|
+
export declare function mediaFilterKeys(media: MediaFilterSource): MediaFilterKeys;
|
|
18
97
|
export declare const MEDIA_TAG_MAX_COUNT = 20;
|
|
19
98
|
export declare const MEDIA_TAG_MAX_LENGTH = 40;
|
|
20
99
|
/**
|
|
@@ -19,7 +19,81 @@
|
|
|
19
19
|
* image header parser so the upload route can stamp dimensions without
|
|
20
20
|
* pulling in an image library.
|
|
21
21
|
*/ import { MEDIA_ALT_MAX_LENGTH } from "./media-alt.js";
|
|
22
|
+
import { nameSearchKey, nameSearchToken, nameSearchTokens } from "./name-search.js";
|
|
22
23
|
export * from "./media-alt.js";
|
|
24
|
+
export const MEDIA_KINDS = [
|
|
25
|
+
'image',
|
|
26
|
+
'video',
|
|
27
|
+
'pdf',
|
|
28
|
+
'document'
|
|
29
|
+
];
|
|
30
|
+
export function mediaKindOf(contentType) {
|
|
31
|
+
const type = String(contentType != null ? contentType : '').trim().toLowerCase();
|
|
32
|
+
if (type.startsWith('image/')) return 'image';
|
|
33
|
+
if (type.startsWith('video/')) return 'video';
|
|
34
|
+
if (type === 'application/pdf') return 'pdf';
|
|
35
|
+
return 'document';
|
|
36
|
+
}
|
|
37
|
+
export const MEDIA_ORIENTATIONS = [
|
|
38
|
+
'landscape',
|
|
39
|
+
'portrait',
|
|
40
|
+
'square'
|
|
41
|
+
];
|
|
42
|
+
const positive = (value)=>typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null;
|
|
43
|
+
/**
|
|
44
|
+
* The orientation of a stored file (AGL-3327), from the pixel size it
|
|
45
|
+
* carries: a picture's `width` and `height`, or a film's `video.width` and
|
|
46
|
+
* `video.height` (a video's own `width`/`height` are never written — see
|
|
47
|
+
* `videoMediaProps`). Null when neither pair is whole, which a query for any
|
|
48
|
+
* orientation then leaves out rather than guesses at.
|
|
49
|
+
*/ export function mediaOrientationOf(media) {
|
|
50
|
+
const video = media.video && typeof media.video === 'object' ? media.video : null;
|
|
51
|
+
let width = positive(media.width);
|
|
52
|
+
let height = positive(media.height);
|
|
53
|
+
if (width === null || height === null) {
|
|
54
|
+
width = positive(video == null ? void 0 : video.width);
|
|
55
|
+
height = positive(video == null ? void 0 : video.height);
|
|
56
|
+
}
|
|
57
|
+
if (width === null || height === null) return null;
|
|
58
|
+
if (width === height) return 'square';
|
|
59
|
+
return width > height ? 'landscape' : 'portrait';
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A file name as words (AGL-3327): every run of characters that is not a
|
|
63
|
+
* letter or a digit becomes a space, so `hero-banner_2x.png` is the four
|
|
64
|
+
* words a person would search it by. The shared name search splits on
|
|
65
|
+
* spaces only, which suits a person's name and misses nearly every file's.
|
|
66
|
+
*/ export function mediaNameWords(fileName) {
|
|
67
|
+
return String(fileName != null ? fileName : '').replace(RegExp("[^\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376-\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0560-\\u0588\\u05D0-\\u05EA\\u05EF-\\u05F2\\u0620-\\u064A\\u066E-\\u066F\\u0671-\\u06D3\\u06D5\\u06E5-\\u06E6\\u06EE-\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4-\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u0860-\\u086A\\u0870-\\u0887\\u0889-\\u088F\\u08A0-\\u08C9\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F-\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC-\\u09DD\\u09DF-\\u09E1\\u09F0-\\u09F1\\u09FC\\u0A05-\\u0A0A\\u0A0F-\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32-\\u0A33\\u0A35-\\u0A36\\u0A38-\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2-\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0-\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F-\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32-\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C-\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99-\\u0B9A\\u0B9C\\u0B9E-\\u0B9F\\u0BA3-\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C5C-\\u0C5D\\u0C60-\\u0C61\\u0C80\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDC-\\u0CDE\\u0CE0-\\u0CE1\\u0CF1-\\u0CF2\\u0D04-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D54-\\u0D56\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32-\\u0E33\\u0E40-\\u0E46\\u0E81-\\u0E82\\u0E84\\u0E86-\\u0E8A\\u0E8C-\\u0EA3\\u0EA5\\u0EA7-\\u0EB0\\u0EB2-\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065-\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16F1-\\u16F8\\u1700-\\u1711\\u171F-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1878\\u1880-\\u1884\\u1887-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4C\\u1B83-\\u1BA0\\u1BAE-\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1C80-\\u1C8A\\u1C90-\\u1CBA\\u1CBD-\\u1CBF\\u1CE9-\\u1CEC\\u1CEE-\\u1CF3\\u1CF5-\\u1CF6\\u1CFA\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183-\\u2184\\u2C00-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2-\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3006\\u3031-\\u3035\\u303B-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312F\\u3131-\\u318E\\u31A0-\\u31BF\\u31F0-\\u31FF\\u3400-\\u4DBF\\u4E00-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A-\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7DC\\uA7F1-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD-\\uA8FE\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5-\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB69\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40-\\uFB41\\uFB43-\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC\\u{10000}-\\u{1000B}\\u{1000D}-\\u{10026}\\u{10028}-\\u{1003A}\\u{1003C}-\\u{1003D}\\u{1003F}-\\u{1004D}\\u{10050}-\\u{1005D}\\u{10080}-\\u{100FA}\\u{10280}-\\u{1029C}\\u{102A0}-\\u{102D0}\\u{10300}-\\u{1031F}\\u{1032D}-\\u{10340}\\u{10342}-\\u{10349}\\u{10350}-\\u{10375}\\u{10380}-\\u{1039D}\\u{103A0}-\\u{103C3}\\u{103C8}-\\u{103CF}\\u{10400}-\\u{1049D}\\u{104B0}-\\u{104D3}\\u{104D8}-\\u{104FB}\\u{10500}-\\u{10527}\\u{10530}-\\u{10563}\\u{10570}-\\u{1057A}\\u{1057C}-\\u{1058A}\\u{1058C}-\\u{10592}\\u{10594}-\\u{10595}\\u{10597}-\\u{105A1}\\u{105A3}-\\u{105B1}\\u{105B3}-\\u{105B9}\\u{105BB}-\\u{105BC}\\u{105C0}-\\u{105F3}\\u{10600}-\\u{10736}\\u{10740}-\\u{10755}\\u{10760}-\\u{10767}\\u{10780}-\\u{10785}\\u{10787}-\\u{107B0}\\u{107B2}-\\u{107BA}\\u{10800}-\\u{10805}\\u{10808}\\u{1080A}-\\u{10835}\\u{10837}-\\u{10838}\\u{1083C}\\u{1083F}-\\u{10855}\\u{10860}-\\u{10876}\\u{10880}-\\u{1089E}\\u{108E0}-\\u{108F2}\\u{108F4}-\\u{108F5}\\u{10900}-\\u{10915}\\u{10920}-\\u{10939}\\u{10940}-\\u{10959}\\u{10980}-\\u{109B7}\\u{109BE}-\\u{109BF}\\u{10A00}\\u{10A10}-\\u{10A13}\\u{10A15}-\\u{10A17}\\u{10A19}-\\u{10A35}\\u{10A60}-\\u{10A7C}\\u{10A80}-\\u{10A9C}\\u{10AC0}-\\u{10AC7}\\u{10AC9}-\\u{10AE4}\\u{10B00}-\\u{10B35}\\u{10B40}-\\u{10B55}\\u{10B60}-\\u{10B72}\\u{10B80}-\\u{10B91}\\u{10C00}-\\u{10C48}\\u{10C80}-\\u{10CB2}\\u{10CC0}-\\u{10CF2}\\u{10D00}-\\u{10D23}\\u{10D4A}-\\u{10D65}\\u{10D6F}-\\u{10D85}\\u{10E80}-\\u{10EA9}\\u{10EB0}-\\u{10EB1}\\u{10EC2}-\\u{10EC7}\\u{10F00}-\\u{10F1C}\\u{10F27}\\u{10F30}-\\u{10F45}\\u{10F70}-\\u{10F81}\\u{10FB0}-\\u{10FC4}\\u{10FE0}-\\u{10FF6}\\u{11003}-\\u{11037}\\u{11071}-\\u{11072}\\u{11075}\\u{11083}-\\u{110AF}\\u{110D0}-\\u{110E8}\\u{11103}-\\u{11126}\\u{11144}\\u{11147}\\u{11150}-\\u{11172}\\u{11176}\\u{11183}-\\u{111B2}\\u{111C1}-\\u{111C4}\\u{111DA}\\u{111DC}\\u{11200}-\\u{11211}\\u{11213}-\\u{1122B}\\u{1123F}-\\u{11240}\\u{11280}-\\u{11286}\\u{11288}\\u{1128A}-\\u{1128D}\\u{1128F}-\\u{1129D}\\u{1129F}-\\u{112A8}\\u{112B0}-\\u{112DE}\\u{11305}-\\u{1130C}\\u{1130F}-\\u{11310}\\u{11313}-\\u{11328}\\u{1132A}-\\u{11330}\\u{11332}-\\u{11333}\\u{11335}-\\u{11339}\\u{1133D}\\u{11350}\\u{1135D}-\\u{11361}\\u{11380}-\\u{11389}\\u{1138B}\\u{1138E}\\u{11390}-\\u{113B5}\\u{113B7}\\u{113D1}\\u{113D3}\\u{11400}-\\u{11434}\\u{11447}-\\u{1144A}\\u{1145F}-\\u{11461}\\u{11480}-\\u{114AF}\\u{114C4}-\\u{114C5}\\u{114C7}\\u{11580}-\\u{115AE}\\u{115D8}-\\u{115DB}\\u{11600}-\\u{1162F}\\u{11644}\\u{11680}-\\u{116AA}\\u{116B8}\\u{11700}-\\u{1171A}\\u{11740}-\\u{11746}\\u{11800}-\\u{1182B}\\u{118A0}-\\u{118DF}\\u{118FF}-\\u{11906}\\u{11909}\\u{1190C}-\\u{11913}\\u{11915}-\\u{11916}\\u{11918}-\\u{1192F}\\u{1193F}\\u{11941}\\u{119A0}-\\u{119A7}\\u{119AA}-\\u{119D0}\\u{119E1}\\u{119E3}\\u{11A00}\\u{11A0B}-\\u{11A32}\\u{11A3A}\\u{11A50}\\u{11A5C}-\\u{11A89}\\u{11A9D}\\u{11AB0}-\\u{11AF8}\\u{11BC0}-\\u{11BE0}\\u{11C00}-\\u{11C08}\\u{11C0A}-\\u{11C2E}\\u{11C40}\\u{11C72}-\\u{11C8F}\\u{11D00}-\\u{11D06}\\u{11D08}-\\u{11D09}\\u{11D0B}-\\u{11D30}\\u{11D46}\\u{11D60}-\\u{11D65}\\u{11D67}-\\u{11D68}\\u{11D6A}-\\u{11D89}\\u{11D98}\\u{11DB0}-\\u{11DDB}\\u{11EE0}-\\u{11EF2}\\u{11F02}\\u{11F04}-\\u{11F10}\\u{11F12}-\\u{11F33}\\u{11FB0}\\u{12000}-\\u{12399}\\u{12480}-\\u{12543}\\u{12F90}-\\u{12FF0}\\u{13000}-\\u{1342F}\\u{13441}-\\u{13446}\\u{13460}-\\u{143FA}\\u{14400}-\\u{14646}\\u{16100}-\\u{1611D}\\u{16800}-\\u{16A38}\\u{16A40}-\\u{16A5E}\\u{16A70}-\\u{16ABE}\\u{16AD0}-\\u{16AED}\\u{16B00}-\\u{16B2F}\\u{16B40}-\\u{16B43}\\u{16B63}-\\u{16B77}\\u{16B7D}-\\u{16B8F}\\u{16D40}-\\u{16D6C}\\u{16E40}-\\u{16E7F}\\u{16EA0}-\\u{16EB8}\\u{16EBB}-\\u{16ED3}\\u{16F00}-\\u{16F4A}\\u{16F50}\\u{16F93}-\\u{16F9F}\\u{16FE0}-\\u{16FE1}\\u{16FE3}\\u{16FF2}-\\u{16FF3}\\u{17000}-\\u{18CD5}\\u{18CFF}-\\u{18D1E}\\u{18D80}-\\u{18DF2}\\u{1AFF0}-\\u{1AFF3}\\u{1AFF5}-\\u{1AFFB}\\u{1AFFD}-\\u{1AFFE}\\u{1B000}-\\u{1B122}\\u{1B132}\\u{1B150}-\\u{1B152}\\u{1B155}\\u{1B164}-\\u{1B167}\\u{1B170}-\\u{1B2FB}\\u{1BC00}-\\u{1BC6A}\\u{1BC70}-\\u{1BC7C}\\u{1BC80}-\\u{1BC88}\\u{1BC90}-\\u{1BC99}\\u{1D400}-\\u{1D454}\\u{1D456}-\\u{1D49C}\\u{1D49E}-\\u{1D49F}\\u{1D4A2}\\u{1D4A5}-\\u{1D4A6}\\u{1D4A9}-\\u{1D4AC}\\u{1D4AE}-\\u{1D4B9}\\u{1D4BB}\\u{1D4BD}-\\u{1D4C3}\\u{1D4C5}-\\u{1D505}\\u{1D507}-\\u{1D50A}\\u{1D50D}-\\u{1D514}\\u{1D516}-\\u{1D51C}\\u{1D51E}-\\u{1D539}\\u{1D53B}-\\u{1D53E}\\u{1D540}-\\u{1D544}\\u{1D546}\\u{1D54A}-\\u{1D550}\\u{1D552}-\\u{1D6A5}\\u{1D6A8}-\\u{1D6C0}\\u{1D6C2}-\\u{1D6DA}\\u{1D6DC}-\\u{1D6FA}\\u{1D6FC}-\\u{1D714}\\u{1D716}-\\u{1D734}\\u{1D736}-\\u{1D74E}\\u{1D750}-\\u{1D76E}\\u{1D770}-\\u{1D788}\\u{1D78A}-\\u{1D7A8}\\u{1D7AA}-\\u{1D7C2}\\u{1D7C4}-\\u{1D7CB}\\u{1DF00}-\\u{1DF1E}\\u{1DF25}-\\u{1DF2A}\\u{1E030}-\\u{1E06D}\\u{1E100}-\\u{1E12C}\\u{1E137}-\\u{1E13D}\\u{1E14E}\\u{1E290}-\\u{1E2AD}\\u{1E2C0}-\\u{1E2EB}\\u{1E4D0}-\\u{1E4EB}\\u{1E5D0}-\\u{1E5ED}\\u{1E5F0}\\u{1E6C0}-\\u{1E6DE}\\u{1E6E0}-\\u{1E6E2}\\u{1E6E4}-\\u{1E6E5}\\u{1E6E7}-\\u{1E6ED}\\u{1E6F0}-\\u{1E6F4}\\u{1E6FE}-\\u{1E6FF}\\u{1E7E0}-\\u{1E7E6}\\u{1E7E8}-\\u{1E7EB}\\u{1E7ED}-\\u{1E7EE}\\u{1E7F0}-\\u{1E7FE}\\u{1E800}-\\u{1E8C4}\\u{1E900}-\\u{1E943}\\u{1E94B}\\u{1EE00}-\\u{1EE03}\\u{1EE05}-\\u{1EE1F}\\u{1EE21}-\\u{1EE22}\\u{1EE24}\\u{1EE27}\\u{1EE29}-\\u{1EE32}\\u{1EE34}-\\u{1EE37}\\u{1EE39}\\u{1EE3B}\\u{1EE42}\\u{1EE47}\\u{1EE49}\\u{1EE4B}\\u{1EE4D}-\\u{1EE4F}\\u{1EE51}-\\u{1EE52}\\u{1EE54}\\u{1EE57}\\u{1EE59}\\u{1EE5B}\\u{1EE5D}\\u{1EE5F}\\u{1EE61}-\\u{1EE62}\\u{1EE64}\\u{1EE67}-\\u{1EE6A}\\u{1EE6C}-\\u{1EE72}\\u{1EE74}-\\u{1EE77}\\u{1EE79}-\\u{1EE7C}\\u{1EE7E}\\u{1EE80}-\\u{1EE89}\\u{1EE8B}-\\u{1EE9B}\\u{1EEA1}-\\u{1EEA3}\\u{1EEA5}-\\u{1EEA9}\\u{1EEAB}-\\u{1EEBB}\\u{20000}-\\u{2A6DF}\\u{2A700}-\\u{2B81D}\\u{2B820}-\\u{2CEAD}\\u{2CEB0}-\\u{2EBE0}\\u{2EBF0}-\\u{2EE5D}\\u{2F800}-\\u{2FA1D}\\u{30000}-\\u{3134A}\\u{31350}-\\u{33479}\\u0030-\\u0039\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE\\u0660-\\u0669\\u06F0-\\u06F9\\u07C0-\\u07C9\\u0966-\\u096F\\u09E6-\\u09EF\\u09F4-\\u09F9\\u0A66-\\u0A6F\\u0AE6-\\u0AEF\\u0B66-\\u0B6F\\u0B72-\\u0B77\\u0BE6-\\u0BF2\\u0C66-\\u0C6F\\u0C78-\\u0C7E\\u0CE6-\\u0CEF\\u0D58-\\u0D5E\\u0D66-\\u0D78\\u0DE6-\\u0DEF\\u0E50-\\u0E59\\u0ED0-\\u0ED9\\u0F20-\\u0F33\\u1040-\\u1049\\u1090-\\u1099\\u1369-\\u137C\\u16EE-\\u16F0\\u17E0-\\u17E9\\u17F0-\\u17F9\\u1810-\\u1819\\u1946-\\u194F\\u19D0-\\u19DA\\u1A80-\\u1A89\\u1A90-\\u1A99\\u1B50-\\u1B59\\u1BB0-\\u1BB9\\u1C40-\\u1C49\\u1C50-\\u1C59\\u2070\\u2074-\\u2079\\u2080-\\u2089\\u2150-\\u2182\\u2185-\\u2189\\u2460-\\u249B\\u24EA-\\u24FF\\u2776-\\u2793\\u2CFD\\u3007\\u3021-\\u3029\\u3038-\\u303A\\u3192-\\u3195\\u3220-\\u3229\\u3248-\\u324F\\u3251-\\u325F\\u3280-\\u3289\\u32B1-\\u32BF\\uA620-\\uA629\\uA6E6-\\uA6EF\\uA830-\\uA835\\uA8D0-\\uA8D9\\uA900-\\uA909\\uA9D0-\\uA9D9\\uA9F0-\\uA9F9\\uAA50-\\uAA59\\uABF0-\\uABF9\\uFF10-\\uFF19\\u{10107}-\\u{10133}\\u{10140}-\\u{10178}\\u{1018A}-\\u{1018B}\\u{102E1}-\\u{102FB}\\u{10320}-\\u{10323}\\u{10341}\\u{1034A}\\u{103D1}-\\u{103D5}\\u{104A0}-\\u{104A9}\\u{10858}-\\u{1085F}\\u{10879}-\\u{1087F}\\u{108A7}-\\u{108AF}\\u{108FB}-\\u{108FF}\\u{10916}-\\u{1091B}\\u{109BC}-\\u{109BD}\\u{109C0}-\\u{109CF}\\u{109D2}-\\u{109FF}\\u{10A40}-\\u{10A48}\\u{10A7D}-\\u{10A7E}\\u{10A9D}-\\u{10A9F}\\u{10AEB}-\\u{10AEF}\\u{10B58}-\\u{10B5F}\\u{10B78}-\\u{10B7F}\\u{10BA9}-\\u{10BAF}\\u{10CFA}-\\u{10CFF}\\u{10D30}-\\u{10D39}\\u{10D40}-\\u{10D49}\\u{10E60}-\\u{10E7E}\\u{10F1D}-\\u{10F26}\\u{10F51}-\\u{10F54}\\u{10FC5}-\\u{10FCB}\\u{11052}-\\u{1106F}\\u{110F0}-\\u{110F9}\\u{11136}-\\u{1113F}\\u{111D0}-\\u{111D9}\\u{111E1}-\\u{111F4}\\u{112F0}-\\u{112F9}\\u{11450}-\\u{11459}\\u{114D0}-\\u{114D9}\\u{11650}-\\u{11659}\\u{116C0}-\\u{116C9}\\u{116D0}-\\u{116E3}\\u{11730}-\\u{1173B}\\u{118E0}-\\u{118F2}\\u{11950}-\\u{11959}\\u{11BF0}-\\u{11BF9}\\u{11C50}-\\u{11C6C}\\u{11D50}-\\u{11D59}\\u{11DA0}-\\u{11DA9}\\u{11DE0}-\\u{11DE9}\\u{11F50}-\\u{11F59}\\u{11FC0}-\\u{11FD4}\\u{12400}-\\u{1246E}\\u{16130}-\\u{16139}\\u{16A60}-\\u{16A69}\\u{16AC0}-\\u{16AC9}\\u{16B50}-\\u{16B59}\\u{16B5B}-\\u{16B61}\\u{16D70}-\\u{16D79}\\u{16E80}-\\u{16E96}\\u{16FF4}-\\u{16FF6}\\u{1CCF0}-\\u{1CCF9}\\u{1D2C0}-\\u{1D2D3}\\u{1D2E0}-\\u{1D2F3}\\u{1D360}-\\u{1D378}\\u{1D7CE}-\\u{1D7FF}\\u{1E140}-\\u{1E149}\\u{1E2F0}-\\u{1E2F9}\\u{1E4F0}-\\u{1E4F9}\\u{1E5F1}-\\u{1E5FA}\\u{1E8C7}-\\u{1E8CF}\\u{1E950}-\\u{1E959}\\u{1EC71}-\\u{1ECAB}\\u{1ECAD}-\\u{1ECAF}\\u{1ECB1}-\\u{1ECB4}\\u{1ED01}-\\u{1ED2D}\\u{1ED2F}-\\u{1ED3D}\\u{1F100}-\\u{1F10C}\\u{1FBF0}-\\u{1FBF9}]+", "gu"), ' ').trim();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The one token a library search asks for (AGL-3327): the first word of what
|
|
71
|
+
* was typed, folded the way `mediaNameWords` folds a stored name, lower-cased
|
|
72
|
+
* and capped as `nameSearchToken` caps it. A query holds one array filter, so
|
|
73
|
+
* a search is one word.
|
|
74
|
+
*/ export function mediaSearchToken(query) {
|
|
75
|
+
return nameSearchToken(mediaNameWords(query));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* What the media library filters, sorts and searches by, derived from the
|
|
79
|
+
* fields a document already carries (AGL-3327). Every writer — the upload
|
|
80
|
+
* routes, replace, the REST API, restore, the Details drawer — spreads this
|
|
81
|
+
* beside the fields it writes, derived from the document as it will stand
|
|
82
|
+
* after the write, so a query finds every file by it. The backfill
|
|
83
|
+
* (`tools/scripts/backfill-media-filter-keys.mjs`) stamps the documents
|
|
84
|
+
* written before it existed, through `tools/scripts/lib/media-filter-keys.mjs`;
|
|
85
|
+
* both are held to `media-filter-keys.fixtures.json` beside it.
|
|
86
|
+
*/ export function mediaFilterKeys(media) {
|
|
87
|
+
var _media_fileName, _media_alt;
|
|
88
|
+
const fileName = String((_media_fileName = media.fileName) != null ? _media_fileName : '');
|
|
89
|
+
return {
|
|
90
|
+
kind: mediaKindOf(media.contentType),
|
|
91
|
+
nameLower: nameSearchKey(fileName),
|
|
92
|
+
nameTokens: nameSearchTokens(mediaNameWords(fileName)),
|
|
93
|
+
hasAlt: String((_media_alt = media.alt) != null ? _media_alt : '').trim().length > 0,
|
|
94
|
+
orientation: mediaOrientationOf(media)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
23
97
|
export const MEDIA_TAG_MAX_COUNT = 20;
|
|
24
98
|
export const MEDIA_TAG_MAX_LENGTH = 40;
|
|
25
99
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/media-metadata.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 * Asset metadata helpers (AGL-173): tag normalization shared by the\n * console editor and any future API validation, plus a dependency-free\n * image header parser so the upload route can stamp dimensions without\n * pulling in an image library.\n */\n\nimport { MEDIA_ALT_MAX_LENGTH } from './media-alt'\n\nexport * from './media-alt'\n\nexport const MEDIA_TAG_MAX_COUNT = 20\nexport const MEDIA_TAG_MAX_LENGTH = 40\n\n/**\n * Trim, lowercase, dedupe, and cap tags. Accepts a comma-separated string\n * or an array; empty and oversized entries are dropped.\n */\nexport function normalizeMediaTags(input: string | string[]): string[] {\n const raw = Array.isArray(input) ? input : String(input ?? '').split(',')\n const seen = new Set<string>()\n const tags: string[] = []\n for (const entry of raw) {\n const tag = String(entry ?? '').trim().toLowerCase()\n if (!tag || tag.length > MEDIA_TAG_MAX_LENGTH || seen.has(tag)) continue\n seen.add(tag)\n tags.push(tag)\n if (tags.length >= MEDIA_TAG_MAX_COUNT) break\n }\n return tags\n}\n\n/**\n * What a placement should store for `alt` once the asset's own alt text is\n * taken into account (AGL-1896) — the DAM asset is the DEFAULT, the\n * placement keeps the override.\n *\n * `AglynHostMedia.alt` has existed since AGL-173 and the library drawer has\n * always been able to set it; what never existed is anybody READING it. Every\n * placement surface asked the author to type alt again from scratch, so the\n * same logo on eight pages needed its alt typed eight times and in practice\n * shipped blank — on a customer's published site.\n *\n * ONE function, called at pick time by every surface, rather than a rule\n * re-derived per surface. The three refusals are the whole contract:\n *\n * * **`decorative` wins outright.** It is the field AGL-1305 added to record\n * \"screen readers should skip this\", and `image.tsx` already forces\n * `alt=\"\"` over any alt text when it is on. Inheriting into a node that has\n * declared itself decorative would put text on a node whose renderer\n * discards it — invisible, and misleading to the next author who opens the\n * panel.\n * * **A non-blank placement alt wins.** That is the per-placement override,\n * and clobbering it is the one failure that would make this feature worse\n * than not having it: the author's sentence about THIS placement is better\n * than the asset's generic one by construction.\n * * **A blank asset alt yields nothing.** Never a fabricated default. The\n * file name is not alt text (\"IMG_4021.jpg\" announced to a screen reader is\n * worse than silence), and nothing here has seen the image. Returning\n * `undefined` is what lets callers omit the key entirely rather than\n * writing `alt: ''`, which on a besigner node is itself an authored value.\n *\n * A blank placement alt DOES inherit, deliberately. Presets ship `alt: ''`\n * (see `card.tsx`), so requiring an absent key would have skipped the single\n * commonest authoring path — dropping a preset and pointing its image at a\n * library asset — and left the issue open for the case it was filed about.\n *\n * @returns the alt to store, or `undefined` when the caller should write\n * nothing at all.\n */\nexport function inheritedMediaAlt(options: {\n /** The alt already on the placement — a node prop, a config field. */\n placementAlt?: unknown\n /** The placement's explicit \"skip me\" intent, when it has one. */\n decorative?: unknown\n /** The chosen DAM asset's stored alt text. */\n assetAlt?: unknown\n}): string | undefined {\n const { placementAlt, decorative, assetAlt } = options ?? {}\n if (decorative === true) return undefined\n if (typeof placementAlt === 'string' && placementAlt.trim()) return undefined\n const inherited = typeof assetAlt === 'string' ? assetAlt.trim() : ''\n if (!inherited) return undefined\n // Capped at the same length the library drawer saves through, so an alt\n // that reaches a placement is one the DAM would also have stored.\n return inherited.slice(0, MEDIA_ALT_MAX_LENGTH)\n}\n\n/**\n * Component ids whose renderer reads intrinsic pixel dimensions off the node.\n *\n * Component ids are persisted in screen documents and never renamed, which is\n * what makes matching on them safe. The list is the gate rather than a\n * decoration: a prop written onto an element that does not destructure it\n * reaches `...rest` and is spread onto the DOM, so an unlisted element would\n * gain an invalid `intrinsicwidth` attribute in its published HTML.\n *\n * ⛔ `video` is NOT here, and its absence is a decision rather than an\n * oversight (AGL-2749). A video needs the pair at least as badly as an image\n * does — `preload=\"none\"` means its metadata never arrives until someone\n * presses play, so the element is zero-height for the whole life of the page\n * without it — but it cannot get it from `width`/`height`. Those are read\n * from the bytes by the server and are absent on every video, deliberately:\n * AGL-2742 kept the client-measured triple in its own `video` record so a\n * reader can tell a server measurement from a browser's report.\n * {@link videoMediaProps} is where a video gets its pair.\n */\nconst INTRINSIC_SIZE_COMPONENT_IDS = new Set(['image'])\n\n/**\n * The intrinsic `width`/`height` to copy onto a node when an author picks a\n * library asset for it (AGL-2486).\n *\n * ## Why the copy happens at pick time, and why it is not the last word\n *\n * The pair rides on the node like every other prop, so the editor canvas, and\n * any page whose asset cannot be read, still reserve a box. It is not the last\n * word. A replace rewrites the asset's `width`/`height` and cannot reach the\n * nodes that copied them, so the tenant composition reads every placed\n * image's document, in one projected batch per page, and lays its current\n * pair over this one (`media-asset-facts.ts`, AGL-2833). What is written here\n * is what a page shows when that read cannot answer.\n *\n * ## Why it matters\n *\n * `image.tsx` lays images out with `width: 100%; height: auto`, under which an\n * `<img>` has NO height until its bytes decode. Without an intrinsic pair the\n * browser has no ratio to reserve a box from, so every image on the page\n * shifts the content below it as it lands.\n *\n * ## The rules\n *\n * * **Both or neither.** A browser derives an aspect-ratio only from the\n * pair; a lone `width` is read as a real dimension and reserves a box of\n * the wrong shape, which is worse than reserving none.\n * * **Only for renderers that read them**, see\n * `INTRINSIC_SIZE_COMPONENT_IDS` — otherwise the prop lands on the DOM.\n * * **Finite and positive.** Upload capture is best-effort, so a media\n * document may carry `0`, a partial capture, or nothing at all; `width=\"0\"`\n * collapses the element.\n * * **`{}` when anything is unknown**, never `{ intrinsicWidth: undefined }`.\n * Callers spread the result into a props object that `updateNodeProps`\n * REPLACES wholesale, so a key present with an undefined value would strip\n * a pair a previous pick had correctly stored.\n *\n * @returns the props to spread, or `{}` when the caller should write nothing.\n */\nexport function intrinsicMediaSize(options: {\n /** The element being written to — its persisted component id. */\n componentId?: unknown\n /** The attribute the picker was opened for; only `src` carries an image. */\n propName?: unknown\n /** The chosen asset's stored pixel width. */\n assetWidth?: unknown\n /** The chosen asset's stored pixel height. */\n assetHeight?: unknown\n}): { intrinsicWidth?: number; intrinsicHeight?: number } {\n const { componentId, propName, assetWidth, assetHeight } = options ?? {}\n if (propName !== 'src') return {}\n if (!INTRINSIC_SIZE_COMPONENT_IDS.has(String(componentId ?? ''))) return {}\n const usable = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0\n if (!usable(assetWidth) || !usable(assetHeight)) return {}\n return { intrinsicWidth: assetWidth, intrinsicHeight: assetHeight }\n}\n\n/** The one element that reads the props below off its node. */\nconst VIDEO_COMPONENT_ID = 'video'\n\n/**\n * The video-only companions to {@link intrinsicMediaSize}, copied onto the\n * node when an author picks a video asset (AGL-2741, rewritten against the\n * real document shape in AGL-2749).\n *\n * Same route as the image dimensions, and like theirs not the last word. A\n * replace rewrites the asset's `video` and `poster` records and cannot reach\n * the nodes that copied them, so the tenant composition reads each placed\n * film's document, in the same batch as the images', and lays its current\n * records over these (`media-asset-facts.ts`, AGL-2807). What is written here\n * is what a page shows when that read cannot answer.\n *\n * ## Why a video does not simply use {@link intrinsicMediaSize}\n *\n * A video's pixel dimensions are NOT in `media.width`/`media.height`. Those\n * are read from the bytes by the server and carry a stronger claim than a\n * browser's report, so AGL-2742 kept the client-measured triple in its own\n * `video` record rather than widening them — a reader that folded the two\n * together would lose the ability to tell which it had. A video therefore\n * reaches its intrinsic pair through here, and `intrinsicMediaSize` finds\n * nothing to copy for one.\n *\n * ## The three rules\n *\n * * **Only the `video` element, only its `src`.** A `durationSeconds` spread\n * onto an element that does not destructure it becomes an invalid attribute\n * in the published HTML.\n * * **The duration is stored in SECONDS**, because the author-facing field is\n * in seconds and a person types 63, not 63000. `videoDurationIso8601` takes\n * milliseconds, and the structured-data builder is the one place that\n * converts back.\n * * **`{}` when unknown**, never a key with an `undefined` value: callers\n * spread this into a props object `updateNodeProps` REPLACES wholesale, so\n * a present-but-undefined key strips what an earlier pick stored.\n *\n * `posterFromSource` is a FLAG rather than a url, deliberately. The generated\n * poster is `?poster=1` on the video's own reference, which\n * {@link mediaPosterSrc} builds without reading anything — and that url is\n * explicitly not a promise the poster exists. This flag IS the promise: it is\n * written only when the document records one, which is what lets\n * `videoPosterSrc` offer the derived url to an `<img>` and to a\n * `thumbnailUrl`, where a 404 would be a broken image and a rich result\n * pointing at nothing.\n *\n * An author's own poster is never touched here. It wins at render time\n * instead (`videoPosterSrc`), so re-picking the SOURCE cannot quietly replace\n * a frame somebody chose.\n *\n * @returns the props to spread, or `{}` when the caller should write nothing.\n */\nexport function videoMediaProps(options: {\n /** The element being written to — its persisted component id. */\n componentId?: unknown\n /** The attribute the picker was opened for; only `src` carries a video. */\n propName?: unknown\n /**\n * The chosen asset's `video` record, as {@link normalizeVideoMetadata}\n * bounds it — `durationMs`, `width` and `height`, all three or none.\n */\n assetVideo?: unknown\n /** The chosen asset's generated `poster` record, when it has one. */\n assetPoster?: unknown\n}): {\n durationSeconds?: number\n intrinsicWidth?: number\n intrinsicHeight?: number\n posterFromSource?: boolean\n} {\n const { componentId, propName, assetVideo, assetPoster } = options ?? {}\n if (propName !== 'src') return {}\n if (String(componentId ?? '') !== VIDEO_COMPONENT_ID) return {}\n const patch: {\n durationSeconds?: number\n intrinsicWidth?: number\n intrinsicHeight?: number\n posterFromSource?: boolean\n } = {}\n // Through the DAM's own validator rather than a second reading of the same\n // three numbers: it is all-or-nothing by design, and re-deriving that rule\n // here is how two files come to disagree about what a partial record means.\n const video = normalizeVideoMetadata(assetVideo)\n if (video) {\n // Never rounds to zero: a sub-second clip is still a clip, and a stored\n // `0` reads as \"no duration\" to everything downstream.\n patch.durationSeconds = Math.max(1, Math.round(video.durationMs / 1000))\n patch.intrinsicWidth = video.width\n patch.intrinsicHeight = video.height\n }\n // A poster RECORD, not a poster url — see the note above. Its mere presence\n // on the document is the whole fact being carried.\n if (assetPoster && typeof assetPoster === 'object') {\n patch.posterFromSource = true\n }\n return patch\n}\n\nexport interface ImageDimensions {\n width: number\n height: number\n}\n\nconst readU32BE = (bytes: Uint8Array, offset: number) =>\n (bytes[offset] << 24) |\n (bytes[offset + 1] << 16) |\n (bytes[offset + 2] << 8) |\n bytes[offset + 3]\n\nconst readU16BE = (bytes: Uint8Array, offset: number) =>\n (bytes[offset] << 8) | bytes[offset + 1]\n\nconst readU16LE = (bytes: Uint8Array, offset: number) =>\n bytes[offset] | (bytes[offset + 1] << 8)\n\n/**\n * Reads pixel dimensions from PNG, JPEG, GIF, and WebP headers. Returns\n * null for anything unrecognized or truncated — callers treat dimensions\n * as best-effort metadata, never a gate.\n */\nexport function readImageDimensions(\n bytes: Uint8Array,\n): ImageDimensions | null {\n if (bytes.length < 24) return null\n\n // PNG: 8-byte signature, IHDR width/height at offsets 16/20.\n if (\n bytes[0] === 0x89 &&\n bytes[1] === 0x50 &&\n bytes[2] === 0x4e &&\n bytes[3] === 0x47\n ) {\n const width = readU32BE(bytes, 16)\n const height = readU32BE(bytes, 20)\n return width > 0 && height > 0 ? { width, height } : null\n }\n\n // GIF87a/GIF89a: little-endian dimensions at offsets 6/8.\n if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {\n const width = readU16LE(bytes, 6)\n const height = readU16LE(bytes, 8)\n return width > 0 && height > 0 ? { width, height } : null\n }\n\n // JPEG: scan segments for a SOFn marker (C0–CF except C4/C8/CC).\n if (bytes[0] === 0xff && bytes[1] === 0xd8) {\n let offset = 2\n while (offset + 9 < bytes.length) {\n if (bytes[offset] !== 0xff) {\n offset += 1\n continue\n }\n const marker = bytes[offset + 1]\n if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd9)) {\n offset += 2\n continue\n }\n const length = readU16BE(bytes, offset + 2)\n if (\n marker >= 0xc0 &&\n marker <= 0xcf &&\n marker !== 0xc4 &&\n marker !== 0xc8 &&\n marker !== 0xcc\n ) {\n const height = readU16BE(bytes, offset + 5)\n const width = readU16BE(bytes, offset + 7)\n return width > 0 && height > 0 ? { width, height } : null\n }\n if (length < 2) return null\n offset += 2 + length\n }\n return null\n }\n\n // WebP: RIFF....WEBP then VP8/VP8L/VP8X chunk.\n if (\n bytes[0] === 0x52 &&\n bytes[1] === 0x49 &&\n bytes[2] === 0x46 &&\n bytes[3] === 0x46 &&\n bytes[8] === 0x57 &&\n bytes[9] === 0x45 &&\n bytes[10] === 0x42 &&\n bytes[11] === 0x50\n ) {\n const chunk = String.fromCharCode(\n bytes[12],\n bytes[13],\n bytes[14],\n bytes[15],\n )\n if (chunk === 'VP8X' && bytes.length >= 30) {\n const width = 1 + (bytes[24] | (bytes[25] << 8) | (bytes[26] << 16))\n const height = 1 + (bytes[27] | (bytes[28] << 8) | (bytes[29] << 16))\n return { width, height }\n }\n if (chunk === 'VP8 ' && bytes.length >= 30) {\n const width = readU16LE(bytes, 26) & 0x3fff\n const height = readU16LE(bytes, 28) & 0x3fff\n return width > 0 && height > 0 ? { width, height } : null\n }\n if (chunk === 'VP8L' && bytes.length >= 25) {\n const bits =\n bytes[21] | (bytes[22] << 8) | (bytes[23] << 16) | (bytes[24] << 24)\n const width = (bits & 0x3fff) + 1\n const height = ((bits >> 14) & 0x3fff) + 1\n return { width, height }\n }\n }\n\n return null\n}\n\n/**\n * What the platform knows about a VIDEO asset (AGL-2742).\n *\n * ## Why this is not `width`/`height` on the document beside an image's\n *\n * An image's dimensions are read from its own header by\n * {@link readImageDimensions}, server-side, from bytes the platform holds.\n * None of that is available for video: the dimensions live in a `moov` atom\n * that an MP4 is free to place at the END of the file, so reading them\n * server-side means fetching up to 200 MB back out of Storage to answer a\n * question worth twelve bytes — the exact download `generateStoredMediaVariants`\n * is written to avoid. So these numbers arrive from the BROWSER, which had\n * the file in hand and a decoder already loaded, and they are consequently\n * client data: every field is bounded here before it reaches a document.\n *\n * They live under their own key rather than widening `width`/`height`\n * because the provenance differs and a reader should be able to tell. A\n * `width` on a media document means \"measured from the bytes\"; a\n * `video.width` means \"reported by the uploader's browser\". Folding the two\n * together would make the weaker claim indistinguishable from the stronger\n * one on every asset in the library.\n */\nexport interface MediaVideoMetadata {\n /** Duration in whole milliseconds. Always finite and positive. */\n durationMs: number\n /** Coded frame width in pixels, after any display-aspect correction. */\n width: number\n height: number\n /**\n * A short label for what produced the file, when the browser offered one\n * (`video/mp4; codecs=\"avc1.640028\"` collapses to `avc1.640028`). Absent\n * far more often than present — no browser API reports the codec of a\n * local file, so this is only ever filled from a `MediaCapabilities` probe\n * or a container sniff, and nothing depends on it.\n */\n codec?: string\n}\n\n/**\n * The upper bound on a stored duration: 24 hours.\n *\n * Not a policy about what may be uploaded — the DAM's ceiling is 200 MB of\n * bytes and says nothing about running time. This is the bound past which a\n * number stops being a duration and starts being a bug, and the specific\n * bug it exists for is real: `HTMLMediaElement.duration` is `Infinity` for a\n * stream and for some WebM files until the element has been seeked to the\n * end. `Infinity` fails the finite test below before it reaches this\n * constant, but a browser that reports a plausible-looking 10^12 instead\n * would otherwise write a document claiming a 31-year film.\n */\nexport const MEDIA_VIDEO_MAX_DURATION_MS = 24 * 60 * 60 * 1000\n\n/** The largest coded dimension accepted, matching the 8K ceiling encoders use. */\nexport const MEDIA_VIDEO_MAX_DIMENSION = 16384\n\n/**\n * Bound a browser's report into something safe to store, or refuse it whole.\n *\n * All-or-nothing on purpose. A partial record — a duration with no\n * dimensions — is worse than none: `VideoObject` JSON-LD would emit a\n * `duration` and omit `width`, and a renderer sizing its container from\n * `video.height` would find the key present and the value absent. The three\n * numbers are produced by one `loadedmetadata` event and are meaningful only\n * together, so they are accepted or rejected together.\n *\n * `codec` is the exception and is dropped rather than refused, for the same\n * reason `formatMediaRef` drops a malformed content pin: it is a label\n * nothing branches on, and losing the whole record over it would trade a\n * working poster and a correct aspect ratio for a cosmetic string.\n */\nexport function normalizeVideoMetadata(\n input: unknown,\n): MediaVideoMetadata | null {\n if (!input || typeof input !== 'object') return null\n const source = input as Record<string, unknown>\n const durationMs = Math.round(Number(source['durationMs']))\n const width = Math.round(Number(source['width']))\n const height = Math.round(Number(source['height']))\n if (!Number.isFinite(durationMs) || durationMs <= 0) return null\n if (durationMs > MEDIA_VIDEO_MAX_DURATION_MS) return null\n if (!Number.isFinite(width) || width <= 0 || width > MEDIA_VIDEO_MAX_DIMENSION)\n return null\n if (\n !Number.isFinite(height) ||\n height <= 0 ||\n height > MEDIA_VIDEO_MAX_DIMENSION\n )\n return null\n const rawCodec = source['codec']\n const codec =\n typeof rawCodec === 'string' && /^[\\w.,\\- ]{1,64}$/.test(rawCodec.trim())\n ? rawCodec.trim()\n : undefined\n return codec ? { durationMs, width, height, codec } : { durationMs, width, height }\n}\n\n/**\n * The `duration` a schema.org `VideoObject` wants: an ISO 8601 duration.\n *\n * Whole seconds, and never a fractional component. Google's structured-data\n * documentation accepts `PT1M30S` and treats sub-second precision as noise,\n * and a `PT1M30.437S` in a rich result is a number nobody asked for. A\n * duration under one second rounds UP to `PT1S` rather than to `PT0S`, which\n * would read as \"no duration\" to a consumer that tests for truthiness.\n */\nexport function videoDurationIso8601(\n durationMs: number | undefined | null,\n): string | undefined {\n const total = Math.max(1, Math.round(Number(durationMs) / 1000))\n if (!Number.isFinite(total)) return undefined\n if (!durationMs || Number(durationMs) <= 0) return undefined\n const hours = Math.floor(total / 3600)\n const minutes = Math.floor((total % 3600) / 60)\n const seconds = total % 60\n const parts = [\n hours ? `${hours}H` : '',\n minutes ? `${minutes}M` : '',\n seconds ? `${seconds}S` : '',\n ].join('')\n return `PT${parts || '0S'}`\n}\n"],"names":["MEDIA_ALT_MAX_LENGTH","MEDIA_TAG_MAX_COUNT","MEDIA_TAG_MAX_LENGTH","normalizeMediaTags","input","raw","Array","isArray","String","split","seen","Set","tags","entry","tag","trim","toLowerCase","length","has","add","push","inheritedMediaAlt","options","placementAlt","decorative","assetAlt","undefined","inherited","slice","INTRINSIC_SIZE_COMPONENT_IDS","intrinsicMediaSize","componentId","propName","assetWidth","assetHeight","usable","value","Number","isFinite","intrinsicWidth","intrinsicHeight","VIDEO_COMPONENT_ID","videoMediaProps","assetVideo","assetPoster","patch","video","normalizeVideoMetadata","durationSeconds","Math","max","round","durationMs","width","height","posterFromSource","readU32BE","bytes","offset","readU16BE","readU16LE","readImageDimensions","marker","chunk","fromCharCode","bits","MEDIA_VIDEO_MAX_DURATION_MS","MEDIA_VIDEO_MAX_DIMENSION","source","rawCodec","codec","test","videoDurationIso8601","total","hours","floor","minutes","seconds","parts","join"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/media-metadata.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 * Asset metadata helpers (AGL-173): tag normalization shared by the\n * console editor and any future API validation, plus a dependency-free\n * image header parser so the upload route can stamp dimensions without\n * pulling in an image library.\n */\n\nimport { MEDIA_ALT_MAX_LENGTH } from './media-alt'\nimport { nameSearchKey, nameSearchToken, nameSearchTokens } from './name-search'\n\nexport * from './media-alt'\n\n/**\n * The family a stored file belongs to, as the media library's Type filter\n * names it (AGL-3327). Stored on the document as `kind`, because a Firestore\n * query can ask for a value and cannot ask for a prefix beside a list's own\n * sort: `kind == 'image'` sits on any query the library builds, where a\n * `contentType` range would take the one range field the query has.\n *\n * Every type media ingress accepts that is not a picture, a film or a PDF is\n * a document — ZIP, Word, Excel, PowerPoint, CSV, text, Markdown, JSON — and\n * so is anything older than that allowlist.\n */\nexport type MediaKind = 'image' | 'video' | 'pdf' | 'document'\n\nexport const MEDIA_KINDS: readonly MediaKind[] = ['image', 'video', 'pdf', 'document']\n\nexport function mediaKindOf(contentType: unknown): MediaKind {\n const type = String(contentType ?? '').trim().toLowerCase()\n if (type.startsWith('image/')) return 'image'\n if (type.startsWith('video/')) return 'video'\n if (type === 'application/pdf') return 'pdf'\n return 'document'\n}\n\n/** Which way a picture or a film is longer, as the Orientation filter asks. */\nexport type MediaOrientation = 'landscape' | 'portrait' | 'square'\n\nexport const MEDIA_ORIENTATIONS: readonly MediaOrientation[] = [\n 'landscape',\n 'portrait',\n 'square',\n]\n\nconst positive = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null\n\n/**\n * The orientation of a stored file (AGL-3327), from the pixel size it\n * carries: a picture's `width` and `height`, or a film's `video.width` and\n * `video.height` (a video's own `width`/`height` are never written — see\n * `videoMediaProps`). Null when neither pair is whole, which a query for any\n * orientation then leaves out rather than guesses at.\n */\nexport function mediaOrientationOf(media: {\n width?: unknown\n height?: unknown\n video?: unknown\n}): MediaOrientation | null {\n const video =\n media.video && typeof media.video === 'object'\n ? (media.video as { width?: unknown; height?: unknown })\n : null\n let width = positive(media.width)\n let height = positive(media.height)\n if (width === null || height === null) {\n width = positive(video?.width)\n height = positive(video?.height)\n }\n if (width === null || height === null) return null\n if (width === height) return 'square'\n return width > height ? 'landscape' : 'portrait'\n}\n\n/**\n * A file name as words (AGL-3327): every run of characters that is not a\n * letter or a digit becomes a space, so `hero-banner_2x.png` is the four\n * words a person would search it by. The shared name search splits on\n * spaces only, which suits a person's name and misses nearly every file's.\n */\nexport function mediaNameWords(fileName: unknown): string {\n return String(fileName ?? '')\n .replace(/[^\\p{L}\\p{N}]+/gu, ' ')\n .trim()\n}\n\n/**\n * The one token a library search asks for (AGL-3327): the first word of what\n * was typed, folded the way `mediaNameWords` folds a stored name, lower-cased\n * and capped as `nameSearchToken` caps it. A query holds one array filter, so\n * a search is one word.\n */\nexport function mediaSearchToken(query: unknown): string {\n return nameSearchToken(mediaNameWords(query))\n}\n\n/** The derived fields every writer of a media document stores beside it. */\nexport interface MediaFilterKeys {\n kind: MediaKind\n /**\n * `nameSearchKey` of the file name: the Name sort, and the \"starts with\"\n * range a search is served by where the scope clause holds the query's one\n * array filter.\n */\n nameLower: string\n /** Word-prefix tokens of the file name's words, for search. */\n nameTokens: string[]\n /** Whether alt text is written — \"missing\" is a value a query can find. */\n hasAlt: boolean\n /** Which way the file is longer, or null when its size is not known. */\n orientation: MediaOrientation | null\n}\n\n/** What `mediaFilterKeys` reads off a media document. */\nexport interface MediaFilterSource {\n fileName?: unknown\n contentType?: unknown\n alt?: unknown\n width?: unknown\n height?: unknown\n video?: unknown\n}\n\n/**\n * What the media library filters, sorts and searches by, derived from the\n * fields a document already carries (AGL-3327). Every writer — the upload\n * routes, replace, the REST API, restore, the Details drawer — spreads this\n * beside the fields it writes, derived from the document as it will stand\n * after the write, so a query finds every file by it. The backfill\n * (`tools/scripts/backfill-media-filter-keys.mjs`) stamps the documents\n * written before it existed, through `tools/scripts/lib/media-filter-keys.mjs`;\n * both are held to `media-filter-keys.fixtures.json` beside it.\n */\nexport function mediaFilterKeys(media: MediaFilterSource): MediaFilterKeys {\n const fileName = String(media.fileName ?? '')\n return {\n kind: mediaKindOf(media.contentType),\n nameLower: nameSearchKey(fileName),\n nameTokens: nameSearchTokens(mediaNameWords(fileName)),\n hasAlt: String(media.alt ?? '').trim().length > 0,\n orientation: mediaOrientationOf(media),\n }\n}\n\nexport const MEDIA_TAG_MAX_COUNT = 20\nexport const MEDIA_TAG_MAX_LENGTH = 40\n\n/**\n * Trim, lowercase, dedupe, and cap tags. Accepts a comma-separated string\n * or an array; empty and oversized entries are dropped.\n */\nexport function normalizeMediaTags(input: string | string[]): string[] {\n const raw = Array.isArray(input) ? input : String(input ?? '').split(',')\n const seen = new Set<string>()\n const tags: string[] = []\n for (const entry of raw) {\n const tag = String(entry ?? '').trim().toLowerCase()\n if (!tag || tag.length > MEDIA_TAG_MAX_LENGTH || seen.has(tag)) continue\n seen.add(tag)\n tags.push(tag)\n if (tags.length >= MEDIA_TAG_MAX_COUNT) break\n }\n return tags\n}\n\n/**\n * What a placement should store for `alt` once the asset's own alt text is\n * taken into account (AGL-1896) — the DAM asset is the DEFAULT, the\n * placement keeps the override.\n *\n * `AglynHostMedia.alt` has existed since AGL-173 and the library drawer has\n * always been able to set it; what never existed is anybody READING it. Every\n * placement surface asked the author to type alt again from scratch, so the\n * same logo on eight pages needed its alt typed eight times and in practice\n * shipped blank — on a customer's published site.\n *\n * ONE function, called at pick time by every surface, rather than a rule\n * re-derived per surface. The three refusals are the whole contract:\n *\n * * **`decorative` wins outright.** It is the field AGL-1305 added to record\n * \"screen readers should skip this\", and `image.tsx` already forces\n * `alt=\"\"` over any alt text when it is on. Inheriting into a node that has\n * declared itself decorative would put text on a node whose renderer\n * discards it — invisible, and misleading to the next author who opens the\n * panel.\n * * **A non-blank placement alt wins.** That is the per-placement override,\n * and clobbering it is the one failure that would make this feature worse\n * than not having it: the author's sentence about THIS placement is better\n * than the asset's generic one by construction.\n * * **A blank asset alt yields nothing.** Never a fabricated default. The\n * file name is not alt text (\"IMG_4021.jpg\" announced to a screen reader is\n * worse than silence), and nothing here has seen the image. Returning\n * `undefined` is what lets callers omit the key entirely rather than\n * writing `alt: ''`, which on a besigner node is itself an authored value.\n *\n * A blank placement alt DOES inherit, deliberately. Presets ship `alt: ''`\n * (see `card.tsx`), so requiring an absent key would have skipped the single\n * commonest authoring path — dropping a preset and pointing its image at a\n * library asset — and left the issue open for the case it was filed about.\n *\n * @returns the alt to store, or `undefined` when the caller should write\n * nothing at all.\n */\nexport function inheritedMediaAlt(options: {\n /** The alt already on the placement — a node prop, a config field. */\n placementAlt?: unknown\n /** The placement's explicit \"skip me\" intent, when it has one. */\n decorative?: unknown\n /** The chosen DAM asset's stored alt text. */\n assetAlt?: unknown\n}): string | undefined {\n const { placementAlt, decorative, assetAlt } = options ?? {}\n if (decorative === true) return undefined\n if (typeof placementAlt === 'string' && placementAlt.trim()) return undefined\n const inherited = typeof assetAlt === 'string' ? assetAlt.trim() : ''\n if (!inherited) return undefined\n // Capped at the same length the library drawer saves through, so an alt\n // that reaches a placement is one the DAM would also have stored.\n return inherited.slice(0, MEDIA_ALT_MAX_LENGTH)\n}\n\n/**\n * Component ids whose renderer reads intrinsic pixel dimensions off the node.\n *\n * Component ids are persisted in screen documents and never renamed, which is\n * what makes matching on them safe. The list is the gate rather than a\n * decoration: a prop written onto an element that does not destructure it\n * reaches `...rest` and is spread onto the DOM, so an unlisted element would\n * gain an invalid `intrinsicwidth` attribute in its published HTML.\n *\n * ⛔ `video` is NOT here, and its absence is a decision rather than an\n * oversight (AGL-2749). A video needs the pair at least as badly as an image\n * does — `preload=\"none\"` means its metadata never arrives until someone\n * presses play, so the element is zero-height for the whole life of the page\n * without it — but it cannot get it from `width`/`height`. Those are read\n * from the bytes by the server and are absent on every video, deliberately:\n * AGL-2742 kept the client-measured triple in its own `video` record so a\n * reader can tell a server measurement from a browser's report.\n * {@link videoMediaProps} is where a video gets its pair.\n */\nconst INTRINSIC_SIZE_COMPONENT_IDS = new Set(['image'])\n\n/**\n * The intrinsic `width`/`height` to copy onto a node when an author picks a\n * library asset for it (AGL-2486).\n *\n * ## Why the copy happens at pick time, and why it is not the last word\n *\n * The pair rides on the node like every other prop, so the editor canvas, and\n * any page whose asset cannot be read, still reserve a box. It is not the last\n * word. A replace rewrites the asset's `width`/`height` and cannot reach the\n * nodes that copied them, so the tenant composition reads every placed\n * image's document, in one projected batch per page, and lays its current\n * pair over this one (`media-asset-facts.ts`, AGL-2833). What is written here\n * is what a page shows when that read cannot answer.\n *\n * ## Why it matters\n *\n * `image.tsx` lays images out with `width: 100%; height: auto`, under which an\n * `<img>` has NO height until its bytes decode. Without an intrinsic pair the\n * browser has no ratio to reserve a box from, so every image on the page\n * shifts the content below it as it lands.\n *\n * ## The rules\n *\n * * **Both or neither.** A browser derives an aspect-ratio only from the\n * pair; a lone `width` is read as a real dimension and reserves a box of\n * the wrong shape, which is worse than reserving none.\n * * **Only for renderers that read them**, see\n * `INTRINSIC_SIZE_COMPONENT_IDS` — otherwise the prop lands on the DOM.\n * * **Finite and positive.** Upload capture is best-effort, so a media\n * document may carry `0`, a partial capture, or nothing at all; `width=\"0\"`\n * collapses the element.\n * * **`{}` when anything is unknown**, never `{ intrinsicWidth: undefined }`.\n * Callers spread the result into a props object that `updateNodeProps`\n * REPLACES wholesale, so a key present with an undefined value would strip\n * a pair a previous pick had correctly stored.\n *\n * @returns the props to spread, or `{}` when the caller should write nothing.\n */\nexport function intrinsicMediaSize(options: {\n /** The element being written to — its persisted component id. */\n componentId?: unknown\n /** The attribute the picker was opened for; only `src` carries an image. */\n propName?: unknown\n /** The chosen asset's stored pixel width. */\n assetWidth?: unknown\n /** The chosen asset's stored pixel height. */\n assetHeight?: unknown\n}): { intrinsicWidth?: number; intrinsicHeight?: number } {\n const { componentId, propName, assetWidth, assetHeight } = options ?? {}\n if (propName !== 'src') return {}\n if (!INTRINSIC_SIZE_COMPONENT_IDS.has(String(componentId ?? ''))) return {}\n const usable = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0\n if (!usable(assetWidth) || !usable(assetHeight)) return {}\n return { intrinsicWidth: assetWidth, intrinsicHeight: assetHeight }\n}\n\n/** The one element that reads the props below off its node. */\nconst VIDEO_COMPONENT_ID = 'video'\n\n/**\n * The video-only companions to {@link intrinsicMediaSize}, copied onto the\n * node when an author picks a video asset (AGL-2741, rewritten against the\n * real document shape in AGL-2749).\n *\n * Same route as the image dimensions, and like theirs not the last word. A\n * replace rewrites the asset's `video` and `poster` records and cannot reach\n * the nodes that copied them, so the tenant composition reads each placed\n * film's document, in the same batch as the images', and lays its current\n * records over these (`media-asset-facts.ts`, AGL-2807). What is written here\n * is what a page shows when that read cannot answer.\n *\n * ## Why a video does not simply use {@link intrinsicMediaSize}\n *\n * A video's pixel dimensions are NOT in `media.width`/`media.height`. Those\n * are read from the bytes by the server and carry a stronger claim than a\n * browser's report, so AGL-2742 kept the client-measured triple in its own\n * `video` record rather than widening them — a reader that folded the two\n * together would lose the ability to tell which it had. A video therefore\n * reaches its intrinsic pair through here, and `intrinsicMediaSize` finds\n * nothing to copy for one.\n *\n * ## The three rules\n *\n * * **Only the `video` element, only its `src`.** A `durationSeconds` spread\n * onto an element that does not destructure it becomes an invalid attribute\n * in the published HTML.\n * * **The duration is stored in SECONDS**, because the author-facing field is\n * in seconds and a person types 63, not 63000. `videoDurationIso8601` takes\n * milliseconds, and the structured-data builder is the one place that\n * converts back.\n * * **`{}` when unknown**, never a key with an `undefined` value: callers\n * spread this into a props object `updateNodeProps` REPLACES wholesale, so\n * a present-but-undefined key strips what an earlier pick stored.\n *\n * `posterFromSource` is a FLAG rather than a url, deliberately. The generated\n * poster is `?poster=1` on the video's own reference, which\n * {@link mediaPosterSrc} builds without reading anything — and that url is\n * explicitly not a promise the poster exists. This flag IS the promise: it is\n * written only when the document records one, which is what lets\n * `videoPosterSrc` offer the derived url to an `<img>` and to a\n * `thumbnailUrl`, where a 404 would be a broken image and a rich result\n * pointing at nothing.\n *\n * An author's own poster is never touched here. It wins at render time\n * instead (`videoPosterSrc`), so re-picking the SOURCE cannot quietly replace\n * a frame somebody chose.\n *\n * @returns the props to spread, or `{}` when the caller should write nothing.\n */\nexport function videoMediaProps(options: {\n /** The element being written to — its persisted component id. */\n componentId?: unknown\n /** The attribute the picker was opened for; only `src` carries a video. */\n propName?: unknown\n /**\n * The chosen asset's `video` record, as {@link normalizeVideoMetadata}\n * bounds it — `durationMs`, `width` and `height`, all three or none.\n */\n assetVideo?: unknown\n /** The chosen asset's generated `poster` record, when it has one. */\n assetPoster?: unknown\n}): {\n durationSeconds?: number\n intrinsicWidth?: number\n intrinsicHeight?: number\n posterFromSource?: boolean\n} {\n const { componentId, propName, assetVideo, assetPoster } = options ?? {}\n if (propName !== 'src') return {}\n if (String(componentId ?? '') !== VIDEO_COMPONENT_ID) return {}\n const patch: {\n durationSeconds?: number\n intrinsicWidth?: number\n intrinsicHeight?: number\n posterFromSource?: boolean\n } = {}\n // Through the DAM's own validator rather than a second reading of the same\n // three numbers: it is all-or-nothing by design, and re-deriving that rule\n // here is how two files come to disagree about what a partial record means.\n const video = normalizeVideoMetadata(assetVideo)\n if (video) {\n // Never rounds to zero: a sub-second clip is still a clip, and a stored\n // `0` reads as \"no duration\" to everything downstream.\n patch.durationSeconds = Math.max(1, Math.round(video.durationMs / 1000))\n patch.intrinsicWidth = video.width\n patch.intrinsicHeight = video.height\n }\n // A poster RECORD, not a poster url — see the note above. Its mere presence\n // on the document is the whole fact being carried.\n if (assetPoster && typeof assetPoster === 'object') {\n patch.posterFromSource = true\n }\n return patch\n}\n\nexport interface ImageDimensions {\n width: number\n height: number\n}\n\nconst readU32BE = (bytes: Uint8Array, offset: number) =>\n (bytes[offset] << 24) |\n (bytes[offset + 1] << 16) |\n (bytes[offset + 2] << 8) |\n bytes[offset + 3]\n\nconst readU16BE = (bytes: Uint8Array, offset: number) =>\n (bytes[offset] << 8) | bytes[offset + 1]\n\nconst readU16LE = (bytes: Uint8Array, offset: number) =>\n bytes[offset] | (bytes[offset + 1] << 8)\n\n/**\n * Reads pixel dimensions from PNG, JPEG, GIF, and WebP headers. Returns\n * null for anything unrecognized or truncated — callers treat dimensions\n * as best-effort metadata, never a gate.\n */\nexport function readImageDimensions(\n bytes: Uint8Array,\n): ImageDimensions | null {\n if (bytes.length < 24) return null\n\n // PNG: 8-byte signature, IHDR width/height at offsets 16/20.\n if (\n bytes[0] === 0x89 &&\n bytes[1] === 0x50 &&\n bytes[2] === 0x4e &&\n bytes[3] === 0x47\n ) {\n const width = readU32BE(bytes, 16)\n const height = readU32BE(bytes, 20)\n return width > 0 && height > 0 ? { width, height } : null\n }\n\n // GIF87a/GIF89a: little-endian dimensions at offsets 6/8.\n if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {\n const width = readU16LE(bytes, 6)\n const height = readU16LE(bytes, 8)\n return width > 0 && height > 0 ? { width, height } : null\n }\n\n // JPEG: scan segments for a SOFn marker (C0–CF except C4/C8/CC).\n if (bytes[0] === 0xff && bytes[1] === 0xd8) {\n let offset = 2\n while (offset + 9 < bytes.length) {\n if (bytes[offset] !== 0xff) {\n offset += 1\n continue\n }\n const marker = bytes[offset + 1]\n if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd9)) {\n offset += 2\n continue\n }\n const length = readU16BE(bytes, offset + 2)\n if (\n marker >= 0xc0 &&\n marker <= 0xcf &&\n marker !== 0xc4 &&\n marker !== 0xc8 &&\n marker !== 0xcc\n ) {\n const height = readU16BE(bytes, offset + 5)\n const width = readU16BE(bytes, offset + 7)\n return width > 0 && height > 0 ? { width, height } : null\n }\n if (length < 2) return null\n offset += 2 + length\n }\n return null\n }\n\n // WebP: RIFF....WEBP then VP8/VP8L/VP8X chunk.\n if (\n bytes[0] === 0x52 &&\n bytes[1] === 0x49 &&\n bytes[2] === 0x46 &&\n bytes[3] === 0x46 &&\n bytes[8] === 0x57 &&\n bytes[9] === 0x45 &&\n bytes[10] === 0x42 &&\n bytes[11] === 0x50\n ) {\n const chunk = String.fromCharCode(\n bytes[12],\n bytes[13],\n bytes[14],\n bytes[15],\n )\n if (chunk === 'VP8X' && bytes.length >= 30) {\n const width = 1 + (bytes[24] | (bytes[25] << 8) | (bytes[26] << 16))\n const height = 1 + (bytes[27] | (bytes[28] << 8) | (bytes[29] << 16))\n return { width, height }\n }\n if (chunk === 'VP8 ' && bytes.length >= 30) {\n const width = readU16LE(bytes, 26) & 0x3fff\n const height = readU16LE(bytes, 28) & 0x3fff\n return width > 0 && height > 0 ? { width, height } : null\n }\n if (chunk === 'VP8L' && bytes.length >= 25) {\n const bits =\n bytes[21] | (bytes[22] << 8) | (bytes[23] << 16) | (bytes[24] << 24)\n const width = (bits & 0x3fff) + 1\n const height = ((bits >> 14) & 0x3fff) + 1\n return { width, height }\n }\n }\n\n return null\n}\n\n/**\n * What the platform knows about a VIDEO asset (AGL-2742).\n *\n * ## Why this is not `width`/`height` on the document beside an image's\n *\n * An image's dimensions are read from its own header by\n * {@link readImageDimensions}, server-side, from bytes the platform holds.\n * None of that is available for video: the dimensions live in a `moov` atom\n * that an MP4 is free to place at the END of the file, so reading them\n * server-side means fetching up to 200 MB back out of Storage to answer a\n * question worth twelve bytes — the exact download `generateStoredMediaVariants`\n * is written to avoid. So these numbers arrive from the BROWSER, which had\n * the file in hand and a decoder already loaded, and they are consequently\n * client data: every field is bounded here before it reaches a document.\n *\n * They live under their own key rather than widening `width`/`height`\n * because the provenance differs and a reader should be able to tell. A\n * `width` on a media document means \"measured from the bytes\"; a\n * `video.width` means \"reported by the uploader's browser\". Folding the two\n * together would make the weaker claim indistinguishable from the stronger\n * one on every asset in the library.\n */\nexport interface MediaVideoMetadata {\n /** Duration in whole milliseconds. Always finite and positive. */\n durationMs: number\n /** Coded frame width in pixels, after any display-aspect correction. */\n width: number\n height: number\n /**\n * A short label for what produced the file, when the browser offered one\n * (`video/mp4; codecs=\"avc1.640028\"` collapses to `avc1.640028`). Absent\n * far more often than present — no browser API reports the codec of a\n * local file, so this is only ever filled from a `MediaCapabilities` probe\n * or a container sniff, and nothing depends on it.\n */\n codec?: string\n}\n\n/**\n * The upper bound on a stored duration: 24 hours.\n *\n * Not a policy about what may be uploaded — the DAM's ceiling is 200 MB of\n * bytes and says nothing about running time. This is the bound past which a\n * number stops being a duration and starts being a bug, and the specific\n * bug it exists for is real: `HTMLMediaElement.duration` is `Infinity` for a\n * stream and for some WebM files until the element has been seeked to the\n * end. `Infinity` fails the finite test below before it reaches this\n * constant, but a browser that reports a plausible-looking 10^12 instead\n * would otherwise write a document claiming a 31-year film.\n */\nexport const MEDIA_VIDEO_MAX_DURATION_MS = 24 * 60 * 60 * 1000\n\n/** The largest coded dimension accepted, matching the 8K ceiling encoders use. */\nexport const MEDIA_VIDEO_MAX_DIMENSION = 16384\n\n/**\n * Bound a browser's report into something safe to store, or refuse it whole.\n *\n * All-or-nothing on purpose. A partial record — a duration with no\n * dimensions — is worse than none: `VideoObject` JSON-LD would emit a\n * `duration` and omit `width`, and a renderer sizing its container from\n * `video.height` would find the key present and the value absent. The three\n * numbers are produced by one `loadedmetadata` event and are meaningful only\n * together, so they are accepted or rejected together.\n *\n * `codec` is the exception and is dropped rather than refused, for the same\n * reason `formatMediaRef` drops a malformed content pin: it is a label\n * nothing branches on, and losing the whole record over it would trade a\n * working poster and a correct aspect ratio for a cosmetic string.\n */\nexport function normalizeVideoMetadata(\n input: unknown,\n): MediaVideoMetadata | null {\n if (!input || typeof input !== 'object') return null\n const source = input as Record<string, unknown>\n const durationMs = Math.round(Number(source['durationMs']))\n const width = Math.round(Number(source['width']))\n const height = Math.round(Number(source['height']))\n if (!Number.isFinite(durationMs) || durationMs <= 0) return null\n if (durationMs > MEDIA_VIDEO_MAX_DURATION_MS) return null\n if (!Number.isFinite(width) || width <= 0 || width > MEDIA_VIDEO_MAX_DIMENSION)\n return null\n if (\n !Number.isFinite(height) ||\n height <= 0 ||\n height > MEDIA_VIDEO_MAX_DIMENSION\n )\n return null\n const rawCodec = source['codec']\n const codec =\n typeof rawCodec === 'string' && /^[\\w.,\\- ]{1,64}$/.test(rawCodec.trim())\n ? rawCodec.trim()\n : undefined\n return codec ? { durationMs, width, height, codec } : { durationMs, width, height }\n}\n\n/**\n * The `duration` a schema.org `VideoObject` wants: an ISO 8601 duration.\n *\n * Whole seconds, and never a fractional component. Google's structured-data\n * documentation accepts `PT1M30S` and treats sub-second precision as noise,\n * and a `PT1M30.437S` in a rich result is a number nobody asked for. A\n * duration under one second rounds UP to `PT1S` rather than to `PT0S`, which\n * would read as \"no duration\" to a consumer that tests for truthiness.\n */\nexport function videoDurationIso8601(\n durationMs: number | undefined | null,\n): string | undefined {\n const total = Math.max(1, Math.round(Number(durationMs) / 1000))\n if (!Number.isFinite(total)) return undefined\n if (!durationMs || Number(durationMs) <= 0) return undefined\n const hours = Math.floor(total / 3600)\n const minutes = Math.floor((total % 3600) / 60)\n const seconds = total % 60\n const parts = [\n hours ? `${hours}H` : '',\n minutes ? `${minutes}M` : '',\n seconds ? `${seconds}S` : '',\n ].join('')\n return `PT${parts || '0S'}`\n}\n"],"names":["MEDIA_ALT_MAX_LENGTH","nameSearchKey","nameSearchToken","nameSearchTokens","MEDIA_KINDS","mediaKindOf","contentType","type","String","trim","toLowerCase","startsWith","MEDIA_ORIENTATIONS","positive","value","Number","isFinite","mediaOrientationOf","media","video","width","height","mediaNameWords","fileName","replace","mediaSearchToken","query","mediaFilterKeys","kind","nameLower","nameTokens","hasAlt","alt","length","orientation","MEDIA_TAG_MAX_COUNT","MEDIA_TAG_MAX_LENGTH","normalizeMediaTags","input","raw","Array","isArray","split","seen","Set","tags","entry","tag","has","add","push","inheritedMediaAlt","options","placementAlt","decorative","assetAlt","undefined","inherited","slice","INTRINSIC_SIZE_COMPONENT_IDS","intrinsicMediaSize","componentId","propName","assetWidth","assetHeight","usable","intrinsicWidth","intrinsicHeight","VIDEO_COMPONENT_ID","videoMediaProps","assetVideo","assetPoster","patch","normalizeVideoMetadata","durationSeconds","Math","max","round","durationMs","posterFromSource","readU32BE","bytes","offset","readU16BE","readU16LE","readImageDimensions","marker","chunk","fromCharCode","bits","MEDIA_VIDEO_MAX_DURATION_MS","MEDIA_VIDEO_MAX_DIMENSION","source","rawCodec","codec","test","videoDurationIso8601","total","hours","floor","minutes","seconds","parts","join"],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/** The most tokens the sender contributes. A name and an address are a few dozen. */
|
|
18
|
+
export declare const MESSAGE_SENDER_TOKENS_MAX = 60;
|
|
19
|
+
/** The most tokens one message's search array holds, the sender's included. */
|
|
20
|
+
export declare const MESSAGE_SEARCH_TOKENS_MAX = 200;
|
|
21
|
+
/** The most distinct words of the message's values that contribute prefixes. */
|
|
22
|
+
export declare const MESSAGE_SEARCH_WORDS_MAX = 40;
|
|
23
|
+
/**
|
|
24
|
+
* Who wrote in, by the field-name convention: the first non-empty value of
|
|
25
|
+
* a name-like field and of an address-like field. Reads the map in the
|
|
26
|
+
* order it is handed, so the first spelling of a reduced key wins.
|
|
27
|
+
*/
|
|
28
|
+
export declare function messageSender(fields: Record<string, unknown> | null | undefined): {
|
|
29
|
+
name?: string;
|
|
30
|
+
email?: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The two search arrays a message is stamped with — see the block above.
|
|
34
|
+
* Spread onto every write that creates one; a message's values never change
|
|
35
|
+
* after it arrives, so no other write touches them.
|
|
36
|
+
*/
|
|
37
|
+
export declare function messageSearchFields(fields: Record<string, unknown> | null | undefined): {
|
|
38
|
+
senderTokens: string[];
|
|
39
|
+
searchTokens: string[];
|
|
40
|
+
};
|