calmlens 0.23.0 → 0.23.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f31d8d0e9739bebd69a0bab938d60914fb7173c8543b5521bcd5ac97e441311a
4
- data.tar.gz: c85ad7397dd68637309b17204ff12dc7d52ec1e6415d66c42bacb7a000b9e9cf
3
+ metadata.gz: 0707eda417af2a9021104b813fecc82ca3a4cddbcc420d92fcd22642f844d61f
4
+ data.tar.gz: c834ec6d48c5815e93d1af413efbd2c8cab0b4fb67071d2da17ac9d97ffab018
5
5
  SHA512:
6
- metadata.gz: 7fb2274df7c0dfa834c97e786ab3b37816ec447c5d7694a293866a75882f11e4a052e684c3c1559a477a7e7419a02144a2af232291bf21f868bffa9fada5eed8
7
- data.tar.gz: '090730bd39a5d48f41a7d7cfdbda56538166304f1af6cf6b22c9046997135f15a66384054a9e02751d168696b46c5a811b01abf825fc77f8a1f04b808da1c8d1'
6
+ metadata.gz: cfa54ba17a0a73abea37f1b34a76df43a2172ffa93e6801f0ae1cd671b40970516675fdcca7014fed69a8acf686a542f78e101fc7000580cc1c11f31c07ac095
7
+ data.tar.gz: 71a628e67b7887807d5c241913b4125836bf5493b6bb54558d9321bb47a4331ea54079dd94d4161dcb02dcc79c798d5653bd891f9c1b3ff3d8848f7cebac9bbd
@@ -359,24 +359,31 @@ module CalmLens
359
359
  def poll_for_decision(asset_id, request_options, timeout)
360
360
  deadline = @session.monotonic + timeout
361
361
  delay = DECISION_POLL_FIRST_DELAY
362
+ processing = moderation = nil
362
363
  loop do
363
- asset = retrieve('assetId' => asset_id, 'request_options' => request_options)
364
+ remaining = deadline - @session.monotonic
365
+ break if timeout.positive? && !remaining.positive?
366
+
367
+ # The outer deadline covers transport retries and the entire response read.
368
+ asset = ::Timeout.timeout(timeout.zero? ? nil : remaining) do
369
+ retrieve('assetId' => asset_id, 'request_options' => request_options)
370
+ end
371
+ remaining = deadline - @session.monotonic
372
+ break if timeout.positive? && !remaining.positive?
373
+
364
374
  processing = asset.dig('classification', 'status')
365
375
  moderation = asset.dig('classification', 'moderation', 'status')
366
376
  return asset if SETTLED_STATUSES.include?(processing)
367
-
368
- remaining = deadline - @session.monotonic
369
- unless remaining.positive?
370
- raise TimeoutError.new(SdkErrorCodes::TIMEOUT,
371
- "Asset #{asset['id']} was not settled after #{timeout}s " \
372
- "(processing #{processing}, moderation #{moderation})",
373
- details: { 'assetId' => asset['id'], 'classificationStatus' => processing,
374
- 'moderationStatus' => moderation })
375
- end
377
+ break unless remaining.positive?
376
378
 
377
379
  sleep([delay, remaining].min)
378
380
  delay = [delay * DECISION_POLL_FACTOR, DECISION_POLL_MAX_DELAY].min
379
381
  end
382
+ raise ::Timeout::Error
383
+ rescue ::Timeout::Error
384
+ raise TimeoutError.new(SdkErrorCodes::TIMEOUT, "Asset #{asset_id} was not settled after #{timeout}s",
385
+ details: { 'assetId' => asset_id, 'classificationStatus' => processing,
386
+ 'moderationStatus' => moderation })
380
387
  end
381
388
 
382
389
  def submit(options, request_options, wait)
@@ -1782,8 +1782,8 @@ module CalmLens
1782
1782
  # Answers like the CDN (serveAssetResource.ts): `original` is the upload; `rendered` the image,
1783
1783
  # PDF, video or audio file `content.rendered` describes; `preview` the still `content.preview`
1784
1784
  # describes; `hls` the adaptive stream's files, which the mock never prepares; `embed` a small
1785
- # page standing in for the embed page of any asset; and `placeholder` the preview's ThumbHash as a PNG, public
1786
- # whatever the asset's visibility or moderation. A safety hold refuses every resource. Other resources pass delivery checks first,
1785
+ # page standing in for the embed page of any asset; and `placeholder` the preview's ThumbHash as a PNG.
1786
+ # A safety hold refuses every resource. Other resources pass delivery checks first,
1787
1787
  # which refuse everyone (content still uploading, blocked or purged, failed processing, removed
1788
1788
  # content), then an age-restricted asset needs `viewerClaims.ageConfirmed=true` whatever the key,
1789
1789
  # and a private asset needs an API key with `assets:download` for its folder. The mock converts
@@ -1809,21 +1809,21 @@ module CalmLens
1809
1809
  authorised = !grant.nil? || can_download?(item['folderId'], headers)
1810
1810
  return info(method, item, authorised, params) if resource == 'info'
1811
1811
  return not_available if item['content']['safetyHold']
1812
- return placeholder(method, item) if resource == 'placeholder'
1812
+ return placeholder(method, item, authorised) if resource == 'placeholder'
1813
1813
 
1814
1814
  deliver(method, item, resource, params, selected ? options || {} : nil, authorised)
1815
1815
  end
1816
1816
 
1817
1817
  # What a grant doesn't cover, or, like the CDN, a private asset for anyone without a grant or the
1818
1818
  # project's API key: it answers as an unknown asset does, whatever its state, except for its
1819
- # public placeholder and its info, which has its own rule. An error response, or nil.
1819
+ # info, which has its own rule. An error response, or nil.
1820
1820
  def refuse_viewer(item, resource, grant, headers)
1821
1821
  if resource == 'original' && grant && !grant[:original]
1822
1822
  return error(ErrorCodes::SIGNED_URL_ORIGINAL_NOT_GRANTED, "This signed asset URL doesn't grant the original file", status: 403)
1823
1823
  end
1824
1824
  return if grant || item['visibility'] == 'public' || header(headers, 'X-Api-Key') == @api_key
1825
1825
 
1826
- asset_not_found unless %w[placeholder info].include?(resource)
1826
+ asset_not_found unless resource == 'info'
1827
1827
  end
1828
1828
 
1829
1829
  # Whether the path after the resource selects a lens or transform, and its options; nil when the
@@ -1866,12 +1866,15 @@ module CalmLens
1866
1866
  restricted = item['visibility'] != 'public'
1867
1867
  return asset_not_found if restricted && !authorised
1868
1868
 
1869
+ restricted ||= %w[blocked purged].include?(item['classification']['moderation']['status'])
1870
+ return not_available if restricted && !authorised
1871
+
1869
1872
  claims = JSON.parse(params.fetch('viewerClaims', '{}'))
1870
1873
  body = { 'id' => item['id'], 'projectId' => item['projectId'], 'type' => item['type'], 'externalUrl' => item['externalUrl'],
1871
1874
  'content' => item['content'].slice(*%w[format width height durationSeconds pageCount rendered preview hls]),
1872
1875
  'availability' => availability(item, claims.is_a?(Hash) && claims['ageConfirmed'] == true) }
1873
1876
  cdn_response(method, JSON.generate(body), { 'content-type' => 'application/json',
1874
- 'cache-control' => "#{restricted ? 'private' : 'public'}, max-age=60",
1877
+ 'cache-control' => restricted ? 'private, no-store' : 'public, no-cache',
1875
1878
  'cache-tag' => "asset-#{item['id']}" })
1876
1879
  rescue JSON::ParserError
1877
1880
  error(ErrorCodes::INVALID_VIEWER_CLAIMS, 'Invalid viewer claims', status: 400)
@@ -1980,15 +1983,18 @@ module CalmLens
1980
1983
 
1981
1984
  def not_available = error('asset/not-available', 'Asset is not available', status: 403)
1982
1985
 
1983
- # Like the CDN's placeholder handler: the preview's ThumbHash, public whatever the asset's
1984
- # visibility or moderation; refused for purged assets and assets without one (not yet
1985
- # processed, without a preview, or content removed).
1986
- def placeholder(method, item)
1986
+ # A preview requires access for private or blocked assets and disappears with its content.
1987
+ def placeholder(method, item, authorised)
1988
+ private_asset = item['visibility'] != 'public'
1989
+ blocked = item['classification']['moderation']['status'] == 'blocked'
1990
+ return asset_not_found if private_asset && !authorised
1991
+ return not_available if blocked && !authorised
1992
+
1987
1993
  thumbhash = item.dig('content', 'preview', 'thumbhash')
1988
1994
  return not_available if thumbhash.nil? || item['classification']['moderation']['status'] == 'purged'
1989
1995
 
1990
1996
  cdn_response(method, Placeholder.png(thumbhash),
1991
- 'content-type' => 'image/png', 'cache-control' => 'public, max-age=3600',
1997
+ 'content-type' => 'image/png', 'cache-control' => private_asset || blocked ? 'private, no-store' : 'public, no-cache',
1992
1998
  'cache-tag' => "asset-#{item['id']}", 'etag' => "\"#{thumbhash}\"")
1993
1999
  end
1994
2000
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module CalmLens
4
4
  # The gem version. The gemspec and the User-Agent header read it from here.
5
- VERSION = '0.23.0'
5
+ VERSION = '0.23.1'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: calmlens
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.23.0
4
+ version: 0.23.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - CalmLens
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-11-09 00:00:00.000000000 Z
11
+ date: 2026-11-10 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'Server-side client for the CalmLens API: moderation, uploads, events,
14
14
  webhooks and CDN delivery.'