@aglyn/tenant-data-admin 1.0.0-beta.224 → 1.0.0-beta.226

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.
@@ -18,6 +18,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { BANDWIDTH_CAP_RETRY_AFTER_SECONDS, bandwidthCapApplies, bandwidthCapEngaged, bandwidthCeilingDegradesHost, bandwidthCeilingMonthKey, isLockdownActive, MEDIA_BANDWIDTH_DAY_FIELD, MEDIA_CDN_POSTER_PARAM, MEDIA_CDN_RENDITION_AUTO, MEDIA_CDN_RENDITION_PARAM, MEDIA_CDN_ROUTE, MEDIA_CDN_VERSION_PARAM, MEDIA_POSTER_OBJECT_SUFFIX, MEDIA_VARIANT_ENCODER_VERSION, MEDIA_VARIANT_ENCODER_VERSION_FIELD, isSecurityClassLockdownReason, mediaDisplayObjectPath, mediaPosterObjectPath, mediaVariantEncoderVersionOf, mediaRenditionObjectPath, normalizeHostLockdown, normalizeOrgLockdown, parseMediaCdnVersionToken, parseMediaRenditions } from "@aglyn/aglyn/server";
19
19
  // By path, and out of every barrel: see the module note in `media-cdn-scope`.
20
20
  import { MEDIA_CDN_SEGMENT, mediaCdnScopeRefusal, parseMediaCdnScope } from "@aglyn/aglyn/app-utils/media-cdn-scope";
21
+ import { scheduleAfterResponse } from "@aglyn/aglyn/app-utils/after-response";
21
22
  import { mediaDeliveryProvider } from "@aglyn/aglyn/plugin-manager/media-delivery-provider";
22
23
  import { parseSiteIconSpec, SITE_ICON_BACKGROUND_PARAM, SITE_ICON_PARAM, SITE_ICON_VERSION_PARAM } from "@aglyn/aglyn/app-utils/site-icon-set";
23
24
  import { analyticsDayExpiresAt } from "./analytics-retention.js";
@@ -173,6 +174,21 @@ const SEGMENT = MEDIA_CDN_SEGMENT;
173
174
  * hour, which is what `s-maxage` is for. Images only — every other type keeps
174
175
  * the stable policy, for the AGL-1515 reasons above.
175
176
  */ export const MEDIA_CDN_VERSIONED_CACHE_CONTROL = 'public, max-age=31536000, s-maxage=3600, stale-while-revalidate=86400, immutable';
177
+ /**
178
+ * The generated width that answers a `?w=` request (AGL-3486): the width
179
+ * itself when the asset has it, otherwise the smallest larger one, and `null`
180
+ * when every variant is narrower than asked — then the display copy or the
181
+ * original is the closest picture there is.
182
+ */ export function mediaCdnVariantFor(variants, width) {
183
+ if (!(width > 0)) return null;
184
+ let best = null;
185
+ for (const candidate of variants){
186
+ if (typeof candidate !== 'number' || !Number.isInteger(candidate)) continue;
187
+ if (candidate === width) return width;
188
+ if (candidate > width && (best === null || candidate < best)) best = candidate;
189
+ }
190
+ return best;
191
+ }
176
192
  /**
177
193
  * Whether a versioned request may be answered with
178
194
  * {@link MEDIA_CDN_VERSIONED_CACHE_CONTROL} (AGL-3485).
@@ -617,23 +633,6 @@ const lockPending = new Map();
617
633
  contentType
618
634
  };
619
635
  }
620
- /**
621
- * Runs `task` once the response has been sent, through Next's `after()`.
622
- * Required when first asked for rather than imported, for the reason
623
- * `capture-email-check.ts` gives; false where there is no request to run
624
- * after — a spec, a script — and the task is then not run at all.
625
- */ function scheduleAfterResponse(task) {
626
- try {
627
- const loaded = require('next/server');
628
- if (typeof (loaded == null ? void 0 : loaded.after) !== 'function') return false;
629
- loaded.after(()=>task().catch((error)=>{
630
- console.error('[media-cdn] after-response task failed', error);
631
- }));
632
- return true;
633
- } catch (unused) {
634
- return false;
635
- }
636
- }
637
636
  /**
638
637
  * Whether `?poster=1` was asked for (AGL-2743).
639
638
  *
@@ -1180,9 +1179,10 @@ export function parseMediaCdnRange(header, size) {
1180
1179
  *
1181
1180
  * ## Falling back to the master is right for a WIDTH and wrong for a POSTER
1182
1181
  *
1183
- * `?w=` on an asset with no such variant serves the original, and that has
1184
- * always been safe because both answers are the same KIND of thing: an
1185
- * image, larger than asked for. A rendition inherits it for the same
1182
+ * `?w=` on an asset with no such variant serves the next larger variant,
1183
+ * or the display copy or original when none is larger, and that is safe
1184
+ * because every answer is the same KIND of thing: an image, larger than
1185
+ * asked for. A rendition inherits it for the same
1186
1186
  * reason — an unknown `?r=` serves the master, which is the same video in
1187
1187
  * more bytes.
1188
1188
  *
@@ -1237,7 +1237,18 @@ export function parseMediaCdnRange(header, size) {
1237
1237
  // handed out without it would be reused across representations.
1238
1238
  res.setHeader('Vary', 'Accept');
1239
1239
  }
1240
- const useVariant = !usePoster && !rendition && Boolean(width) && variants.includes(width);
1240
+ /*
1241
+ * The variant that answers `?w=`: the width itself, or the smallest one
1242
+ * above it the asset has (AGL-3486). An asset made by an older encoder
1243
+ * lacks steps the page now asks for — generation 1 had 320/640/1280/1920
1244
+ * and srcsets ask for 768 — and answering those from the original sent
1245
+ * every phone the full-size JPEG. The next width up is the same picture,
1246
+ * a little larger than asked for, as a WebP. Only an exact width is a
1247
+ * `variantServed` for the year-long policy: a stand-in is replaced by the
1248
+ * real width once the asset is regenerated.
1249
+ */ const variantWidth = !usePoster && !rendition && width > 0 ? mediaCdnVariantFor(variants, width) : null;
1250
+ const useVariant = variantWidth !== null;
1251
+ const exactVariant = variantWidth === width;
1241
1252
  // Read only here, past every gate — a refusal above returns before the
1242
1253
  // parameter is ever looked at, so `?download=1` can never be the reason
1243
1254
  // a response happens (AGL-1411).
@@ -1272,7 +1283,7 @@ export function parseMediaCdnRange(header, size) {
1272
1283
  * revalidates. Between an image variant and its original that is a wrong
1273
1284
  * size; between a poster and its video it is a 40 KB still standing in
1274
1285
  * for a 40 MB film, or the reverse.
1275
- */ const representation = usePoster ? `-poster${usePosterVariant ? `-w${width}` : ''}` : rendition ? `-r${rendition.key}` : useVariant ? `-w${width}${encoderTag}` : useDisplay ? `-d${encoderTag}` : '';
1286
+ */ const representation = usePoster ? `-poster${usePosterVariant ? `-w${width}` : ''}` : rendition ? `-r${rendition.key}` : useVariant ? `-w${variantWidth}${encoderTag}` : useDisplay ? `-d${encoderTag}` : '';
1276
1287
  // Stable URL: revalidate against an ETag so a replaced asset is picked
1277
1288
  // up (a conditional GET returns 304 while the content is unchanged).
1278
1289
  //
@@ -1298,7 +1309,7 @@ export function parseMediaCdnRange(header, size) {
1298
1309
  token: parseMediaCdnVersionToken(req.query[MEDIA_CDN_VERSION_PARAM]),
1299
1310
  currentHash,
1300
1311
  widthRequested: Boolean(width),
1301
- variantServed: useVariant,
1312
+ variantServed: exactVariant,
1302
1313
  otherRepresentation: usePoster || Boolean(rendition),
1303
1314
  documentEncoderVersion: snapshot.get(MEDIA_VARIANT_ENCODER_VERSION_FIELD)
1304
1315
  });
@@ -1443,7 +1454,7 @@ export function parseMediaCdnRange(header, size) {
1443
1454
  [MEDIA_VARIANT_ENCODER_VERSION_FIELD]: snapshot.get(MEDIA_VARIANT_ENCODER_VERSION_FIELD),
1444
1455
  variantRegeneration: snapshot.get('variantRegeneration')
1445
1456
  })) {
1446
- scheduleAfterResponse(async ()=>{
1457
+ await scheduleAfterResponse(async ()=>{
1447
1458
  const regenerationPath = mediaStoragePathInScope({
1448
1459
  storagePath: snapshot.get('storagePath'),
1449
1460
  base: `${isOrg ? 'orgs' : 'hosts'}/${scopeId}`,
@@ -1458,7 +1469,7 @@ export function parseMediaCdnRange(header, size) {
1458
1469
  if (outcome === 'failed') {
1459
1470
  console.error('[media-cdn] delivery copies could not be regenerated', scopeSegment, mediaId);
1460
1471
  }
1461
- });
1472
+ }, '[media-cdn]');
1462
1473
  }
1463
1474
  if (!hashed) {
1464
1475
  setCacheControl(stableCacheControlFor(docServedType));
@@ -1497,7 +1508,7 @@ export function parseMediaCdnRange(header, size) {
1497
1508
  // and `parseMediaRendition` refuses a key or extension that is not
1498
1509
  // `[a-z0-9-]`. So no representation can address an object the master
1499
1510
  // could not.
1500
- const objectPath = usePoster ? usePosterVariant ? `${basePath}${MEDIA_POSTER_OBJECT_SUFFIX}__w${width}.webp` : mediaPosterObjectPath(basePath) : rendition ? mediaRenditionObjectPath(basePath, rendition) : useVariant ? `${basePath}__w${width}.webp` : useDisplay ? mediaDisplayObjectPath(basePath) : basePath;
1511
+ const objectPath = usePoster ? usePosterVariant ? `${basePath}${MEDIA_POSTER_OBJECT_SUFFIX}__w${width}.webp` : mediaPosterObjectPath(basePath) : rendition ? mediaRenditionObjectPath(basePath, rendition) : useVariant ? `${basePath}__w${variantWidth}.webp` : useDisplay ? mediaDisplayObjectPath(basePath) : basePath;
1501
1512
  let file = bucket.file(objectPath);
1502
1513
  // The caller's count (AGL-2812), asked here and nowhere earlier: past every
1503
1514
  // gate and the 304 exit, where the Storage reads begin. It starts before the
@@ -1672,8 +1683,9 @@ export function parseMediaCdnRange(header, size) {
1672
1683
  merge: true
1673
1684
  }).catch((error)=>logUncountedServe(scopeSegment, mediaId, error));
1674
1685
  // Totalled beside the stream rather than ahead of it, and awaited — with
1675
- // the count above — once the stream ends, so the function lives until
1676
- // both are done.
1686
+ // the count above — once the stream ends. The request is over by then;
1687
+ // `runLegacyHandler` hands what this still awaits to `after()`, which is
1688
+ // what keeps the function alive until both are written.
1677
1689
  const evaluation = countedBytes > 0 ? mediaCdnBandwidthEvaluation(scope, countedBytes) : null;
1678
1690
  if (partial) res.status(206);
1679
1691
  // `start`/`end` are inclusive in `createReadStream`, matching the parsed