calmlens 0.18.3 → 0.20.0

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.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CalmLens
4
- # CDN transform selectors: selector Strings and structured steps serialized the way every SDK does.
4
+ # CDN transform selectors: selector Strings and structured options serialized the way every SDK does.
5
5
  # @api private
6
6
  module Transforms
7
7
  module_function
@@ -14,46 +14,55 @@ module CalmLens
14
14
  # </generated: image-transform-colours>
15
15
  HEX_COLOUR = /\A#((?:\h{3}){1,2}|(?:\h{4}){1,2})\z/
16
16
 
17
- # Returns the selector the API and CDN accept for a transform: a selector String unchanged, or
18
- # structured steps (`{type:, options:}` or an Array of them) serialized by {serialize}.
17
+ # Returns the selector the API and CDN accept for a transform: a selector String checked and
18
+ # unchanged, or structured options (`{width: 640, fit: 'cover'}`) serialized by {serialize}.
19
19
  def selector(value)
20
- value.nil? || value.is_a?(String) ? value : serialize(value)
20
+ return serialize(value) unless value.nil? || value.is_a?(String)
21
+ # A transform is one path segment of options: it must not add a segment, lens, query or fragment.
22
+ if value&.match?(%r{[/?#]}) || value&.include?('..') || value&.start_with?('@')
23
+ raise Internal.invalid('Transform cannot contain "/", "..", "?" or "#", or start with "@"')
24
+ end
25
+
26
+ value
21
27
  end
22
28
 
23
- # Builds the CDN path of a transform. A selector String keeps its own step separators and is
24
- # encoded like JavaScript's encodeURI; serialized steps are already path-safe.
29
+ # Builds the CDN path text of a transform. A selector String is encoded like JavaScript's
30
+ # encodeURI; serialized options are already path-safe.
25
31
  def path(value)
26
- return serialize(value) unless value.is_a?(String)
27
-
28
- text = value.sub(%r{\A/+}, '')
29
- raise Internal.invalid('Transform cannot contain "..", "?" or "#"') if text.include?('..') || text.match?(/[?#]/)
32
+ text = selector(value)
33
+ return text unless value.is_a?(String)
30
34
 
31
35
  # Like JavaScript's encodeURI.
32
36
  text.b.gsub(%r{[^A-Za-z0-9\-_.!~*'();/:@&=+$,]}n) { |byte| format('%%%02X', byte.ord) }
33
37
  end
34
38
 
35
- # Serializes structured transforms as a CDN selector, one path segment per step, such as
36
- # `fit=cover,width=640/format=webp`. Option keys are sorted so equal transforms share a URL, nested
37
- # keys are dotted (`border.color`), hex colours are written without `#` and values are percent-encoded.
39
+ # Serializes structured transform options as a CDN selector, such as `fit=cover,width=640`. Option
40
+ # keys are sorted so equal transforms share a URL, nested keys are dotted (`border.color`), hex
41
+ # colours are written without `#` and values are percent-encoded.
38
42
  def serialize(value)
39
- steps = value.is_a?(Array) ? value : [value]
40
- raise Internal.invalid('Transform needs at least one step') if steps.empty?
43
+ raise Internal.invalid('Transform options must be a Hash') unless value.is_a?(Hash)
41
44
 
42
- steps.map do |step|
43
- raise Internal.invalid('Transform steps must be Hashes') unless step.is_a?(Hash)
45
+ assert_valid(value)
46
+ pairs = []
47
+ sorted_options(value).each { |key, item| append_pairs(pairs, key, item) }
48
+ raise Internal.invalid('A transform needs at least one option') if pairs.empty?
44
49
 
45
- step = step.transform_keys(&:to_s)
46
- raise Internal.invalid('Transform type must be "imageTransform"') unless step['type'] == 'imageTransform'
47
-
48
- options = step['options'] || {}
49
- raise Internal.invalid('Transform options must be a Hash') unless options.is_a?(Hash)
50
-
51
- pairs = []
52
- sorted_options(options).each { |key, item| append_pairs(pairs, key, item) }
53
- raise Internal.invalid('Transform steps need at least one option') if pairs.empty?
50
+ pairs.join(',')
51
+ end
54
52
 
55
- pairs.join(',')
56
- end.join('/')
53
+ # Raises `sdk/invalid-argument` naming each transform option the CDN would refuse, with
54
+ # `details` `{'unknownKeys', 'invalidKeys'}`.
55
+ def assert_valid(options)
56
+ unknown, invalid = TransformRules.check(options)
57
+ return if unknown.empty? && invalid.empty?
58
+
59
+ problems = []
60
+ problems << "unknown option#{'s' if unknown.size > 1} #{unknown.join(', ')}" unless unknown.empty?
61
+ problems << "invalid value#{'s' if invalid.size > 1} for #{invalid.join(', ')}" unless invalid.empty?
62
+ raise InvalidArgumentError.new(SdkErrorCodes::INVALID_ARGUMENT,
63
+ "Transform options are invalid: #{problems.join('; ')}. Pass the options " \
64
+ "themselves, such as { width: 640, format: 'webp' }.",
65
+ details: { 'unknownKeys' => unknown, 'invalidKeys' => invalid })
57
66
  end
58
67
 
59
68
  def sorted_options(options, prefix = '')
@@ -90,6 +90,14 @@ module CalmLens
90
90
  @source.rewind unless @source.is_a?(String)
91
91
  end
92
92
 
93
+ # `length` bytes from `offset` as a body of their own, read again from the source for each
94
+ # attempt. Only for a {#replayable?} body of known size: an IO is positioned at the slice.
95
+ def slice(offset, length)
96
+ return UploadBody.new(@source.byteslice(offset, length)) if @source.is_a?(String)
97
+
98
+ UploadBody.new(@source.slice(offset, length))
99
+ end
100
+
93
101
  # The bytes, read fully. Used only by the offline mock.
94
102
  def read_all
95
103
  return @source if @source.is_a?(String)
@@ -114,6 +122,12 @@ module CalmLens
114
122
 
115
123
  def rewindable? = !@start.nil?
116
124
 
125
+ # A reader of `length` bytes from `offset` of this rewindable stream, positioned there.
126
+ def slice(offset, length)
127
+ @io.seek(@start + offset)
128
+ IOReader.new(@io, length)
129
+ end
130
+
117
131
  def rewind
118
132
  @io.seek(@start)
119
133
  @remaining = @size
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generated by packages/sdk-contract/generate.ts from packages/shared/src/UploadMimeTypes.ts; do not edit.
4
+ # After lower-casing and dropping parameters, a type in REJECTED is refused; otherwise one
5
+ # that starts with a PREFIXES entry, ends with a SUFFIXES entry or is in TYPES is accepted,
6
+ # and any other type is refused with upload/invalid-mime-type.
7
+
8
+ module CalmLens
9
+ module Internal
10
+ UPLOAD_MIME_REJECTED = [
11
+ 'application/octet-stream',
12
+ 'multipart/form-data',
13
+ 'multipart/mixed',
14
+ 'multipart/alternative',
15
+ 'multipart/related',
16
+ 'multipart/signed',
17
+ 'multipart/encrypted'
18
+ ].freeze
19
+ UPLOAD_MIME_PREFIXES = [
20
+ 'image/',
21
+ 'video/',
22
+ 'audio/',
23
+ 'text/'
24
+ ].freeze
25
+ UPLOAD_MIME_SUFFIXES = [
26
+ '+json',
27
+ '+xml'
28
+ ].freeze
29
+ UPLOAD_MIME_TYPES = [
30
+ 'application/pdf',
31
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
32
+ 'application/msword',
33
+ 'application/rtf',
34
+ 'application/vnd.oasis.opendocument.text',
35
+ 'application/javascript',
36
+ 'application/json',
37
+ 'application/x-javascript',
38
+ 'application/x-yaml',
39
+ 'application/xml',
40
+ 'application/yaml'
41
+ ].freeze
42
+ end
43
+ end
@@ -9,6 +9,9 @@ module CalmLens
9
9
  # Signs upload tokens, which let a browser upload through an upload preset. Reach it through
10
10
  # {ServerClient#upload_tokens}. Signing happens locally with the API key; no request is made.
11
11
  class UploadTokens < Resource
12
+ # The prefix of this resource's operations in Internal::OPTION_KEYS.
13
+ OPERATIONS = 'uploadTokens'
14
+
12
15
  TOKEN_PREFIX = 'clut_'
13
16
  SIGNING_CONTEXT = 'calmlens-upload-token:v1:'
14
17
  DEFAULT_TTL_SECONDS = 600
@@ -26,7 +29,7 @@ module CalmLens
26
29
  # @option options [Integer] :expires_in_seconds lifetime, 1 to 3600 (default 600)
27
30
  # @return [Hash] `{'token' => String, 'expiresAt' => Integer}` (milliseconds since the Unix epoch)
28
31
  def sign(options)
29
- call(options) { UploadTokens.sign_at(@api_key, options, now: (Time.now.to_f * 1000).to_i, nonce: SecureRandom.uuid) }
32
+ call(options, __method__) { UploadTokens.sign_at(@api_key, options, now: (Time.now.to_f * 1000).to_i, nonce: SecureRandom.uuid) }
30
33
  end
31
34
 
32
35
  # Signs like {#sign} at a fixed time with a fixed nonce. {MockServerClient.sign_upload_token}
@@ -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.18.3'
5
+ VERSION = '0.20.0'
6
6
  end
data/lib/calmlens.rb CHANGED
@@ -8,8 +8,12 @@ end
8
8
  require_relative 'calmlens/version'
9
9
  require_relative 'calmlens/error_codes'
10
10
  require_relative 'calmlens/errors'
11
+ require_relative 'calmlens/option_keys'
12
+ require_relative 'calmlens/transform_schema'
13
+ require_relative 'calmlens/upload_mime_types'
11
14
  require_relative 'calmlens/internal'
12
15
  require_relative 'calmlens/transforms'
16
+ require_relative 'calmlens/transform_rules'
13
17
  require_relative 'calmlens/response'
14
18
  require_relative 'calmlens/transport'
15
19
  require_relative 'calmlens/upload_body'
@@ -19,6 +23,7 @@ require_relative 'calmlens/moderation_context'
19
23
  require_relative 'calmlens/assets'
20
24
  require_relative 'calmlens/folders'
21
25
  require_relative 'calmlens/hash_lists'
26
+ require_relative 'calmlens/policies'
22
27
  require_relative 'calmlens/text'
23
28
  require_relative 'calmlens/upload_tokens'
24
29
  require_relative 'calmlens/events'
data/sig/calmlens.rbs CHANGED
@@ -109,10 +109,14 @@ module CalmLens
109
109
  RETRY_MAX_DELAY: Integer
110
110
  RETRY_AFTER_MAX: Integer
111
111
  DEFAULT_MAX_RESPONSE_BYTES: Integer
112
+ DEFAULT_PART_SIZE_BYTES: Integer
113
+ MIN_PART_SIZE_BYTES: Integer
114
+ MAX_PART_SIZE_BYTES: Integer
112
115
  ENSURE_LENSES_MAX_PER_REQUEST: Integer
113
116
  attr_reader assets: Assets
114
117
  attr_reader folders: Folders
115
118
  attr_reader hash_lists: HashLists
119
+ attr_reader policies: Policies
116
120
  attr_reader text: Text
117
121
  attr_reader upload_tokens: UploadTokens
118
122
  attr_reader events: Events
@@ -122,7 +126,7 @@ module CalmLens
122
126
  attr_reader cdn_base_url: String
123
127
  def initialize: (api_key: String, project_id: String, ?base_url: String, ?cdn_base_url: String, ?timeout: number,
124
128
  ?open_timeout: number, ?upload_timeout: number?, ?max_retries: Integer, ?retry_base_delay: number,
125
- ?max_response_bytes: Integer, ?transport: Transport::_Transport?) -> void
129
+ ?max_response_bytes: Integer, ?part_size_bytes: Integer, ?transport: Transport::_Transport?) -> void
126
130
  def close: () -> nil
127
131
  def ensure_lenses: (ensure_lenses_options options) -> Array[lens]
128
132
  def ensure_lens: (ensure_lens_options options) -> lens
@@ -200,6 +204,17 @@ module CalmLens
200
204
  def remove_entry: (hash_list_entry_id_options options) -> nil
201
205
  end
202
206
 
207
+ class Policies < Resource
208
+ def list: (?list_policies_options options) -> policy_list
209
+ def retrieve: (policy_id_options options) -> policy
210
+ def create: (create_policy_options options) -> policy
211
+ def update: (update_policy_options options) -> policy
212
+ def apply_preset: (apply_policy_preset_options options) -> policy
213
+ def set_automatic_updates: (set_policy_automatic_updates_options options) -> policy
214
+ def delete: (policy_id_options options) -> nil
215
+ def assign: (assign_policy_options options) -> policy
216
+ end
217
+
203
218
  class UploadTokens < Resource
204
219
  TOKEN_PREFIX: String
205
220
  SIGNING_CONTEXT: String
@@ -338,11 +353,12 @@ module CalmLens
338
353
  def initialize: (?project_id: String, ?api_key: String, ?base_url: String, ?cdn_base_url: String, ?now: _Clock, ?create_id: _IdFactory,
339
354
  ?moderate: _Moderate?, ?timeout: number,
340
355
  ?open_timeout: number, ?upload_timeout: number?, ?max_retries: Integer, ?retry_base_delay: number,
341
- ?max_response_bytes: Integer) -> void
356
+ ?max_response_bytes: Integer, ?part_size_bytes: Integer) -> void
342
357
  # Replaces the mock API key's grants for later requests; the key stays the same.
343
358
  def api_key_grants=: (Array[Hash[Symbol | String, untyped]] grants) -> Array[Hash[Symbol | String, untyped]]
344
359
  # Fails the next request (or the next one `operation` sends) with `transport-error`, `timeout`, an
345
- # API error code (with optional `details` and `Retry-After` seconds) or an exception.
360
+ # API error code (with optional `details` and `Retry-After` seconds, which only an API error takes) or
361
+ # an exception.
346
362
  def fail_next_request: (?(String | Exception) failure, ?operation: String?, ?details: Hash[String | Symbol, untyped]?,
347
363
  ?retry_after: Integer?) -> self
348
364
  def state: () -> Testing::State
@@ -398,5 +414,45 @@ module CalmLens
398
414
  module MockClassification
399
415
  CATEGORIES: Array[String]
400
416
  QUALIFIERS: Hash[String, Array[String]]
417
+ # Qualifiers the input type sets, which a TestInputs scenario cannot list.
418
+ INPUT_QUALIFIERS: Array[String]
419
+ end
420
+
421
+ # Signed test inputs: text codes, test images and media phrases signed with a project's secret
422
+ # test input key from the portal.
423
+ module TestInputs
424
+ type action = "allow" | "limit" | "age-restrict" | "block"
425
+ type region = "DE" | "AT" | "FR" | "GB" | "EU" | "AU" | "BR"
426
+ type regional_action = { "region" => region, "action" => "limit" | "age-restrict" | "block" }
427
+ # One qualifying instance; set an action on every instance or on none.
428
+ type test_instance = { "category" => classification_category, ?"score" => number, ?"action" => action, "qualifiers" => Array[String] }
429
+ # A scenario with String keys; Symbol and snake_case keys are accepted as a plain Hash.
430
+ type scenario = { ?"regionalActions" => Array[regional_action], ?"unassessedCategories" => Array[classification_category],
431
+ "instances" => Array[test_instance] }
432
+ type signal = classification_category | "sexual.involving-minors" | "harassment.insult" | "sexual.adult"
433
+ | "hate.regional-symbol" | "violence.graphic"
434
+ type media_signal = classification_category | "violence.graphic"
435
+ type input = scenario | Hash[String | Symbol, untyped] | signal
436
+
437
+ # A painted test image: width by height pixels of 8-bit RGBA in the binary String `rgba`, and
438
+ # the same image as binary PNG bytes in `png`.
439
+ class Image < Struct[untyped]
440
+ attr_accessor width: Integer
441
+ attr_accessor height: Integer
442
+ attr_accessor rgba: String
443
+ attr_accessor png: String
444
+ def initialize: (width: Integer, height: Integer, rgba: String, png: String) -> void
445
+ end
446
+
447
+ REGIONS: Array[region]
448
+ SIGNALS: Array[signal]
449
+ MEDIA_SIGNALS: Array[media_signal]
450
+ # clean, limited, adult_image, adult_text, regional_delivery, germany and mixed (deeply frozen).
451
+ PRESETS: Hash[Symbol, scenario]
452
+
453
+ def self.for_signal: (signal signal, ?("text" | "image") kind) -> scenario
454
+ def self.generate_text_code: (input input, String test_input_key) -> String
455
+ def self.paint_image: (input input, String test_input_key) -> Image
456
+ def self.generate_media_code: (media_signal signal, String test_input_key) -> String
401
457
  end
402
458
  end
data/sig/error_codes.rbs CHANGED
@@ -7,6 +7,8 @@ module CalmLens
7
7
  INVALID_VIEWER_REGION: String
8
8
  VIEWER_REGION_REQUIRED: String
9
9
  REGIONAL_EVIDENCE_REQUIRED: String
10
+ SIGNED_URL_EXPIRED: String
11
+ SIGNED_URL_INVALID: String
10
12
  INVALID_TYPE: String
11
13
  TOO_SMALL: String
12
14
  TOO_BIG: String
@@ -34,9 +36,11 @@ module CalmLens
34
36
  API_KEY_NOT_ACCEPTED: String
35
37
  PERMISSION_DENIED: String
36
38
  FILE_TOO_LARGE: String
37
- MULTIPART_IMAGE_TOO_SMALL: String
38
39
  MULTIPART_IMAGE_TOO_LARGE: String
39
40
  IMAGE_EXCEEDED_MAX_DIMENSIONS: String
41
+ IMAGE_INVALID_INPUT: String
42
+ IMAGE_UNSUPPORTED_FORMAT: String
43
+ IMAGE_INPUT_LIMIT: String
40
44
  UPLOAD_LIMIT_EXCEEDED: String
41
45
  SPEND_LIMIT_REACHED: String
42
46
  INVALID_UPLOAD_MIME_TYPE: String
@@ -77,6 +81,15 @@ module CalmLens
77
81
  HASH_LIST_LIMIT_REACHED: String
78
82
  HASH_LIST_ENTRY_LIMIT_REACHED: String
79
83
  HASH_LIST_ASSET_NOT_HASHED: String
84
+ POLICY_NOT_FOUND: String
85
+ POLICY_REVISION_CONFLICT: String
86
+ POLICY_PROJECTS_CHANGED: String
87
+ POLICY_IN_USE: String
88
+ POLICY_USED_BY_ANOTHER_PROJECT: String
89
+ POLICY_NO_PRESET: String
90
+ POLICY_PRESET_VERSION_CONFLICT: String
91
+ POLICY_PRESET_UPDATE_UNAVAILABLE: String
92
+ POLICY_AUTOMATIC_UPDATES_UNAVAILABLE: String
80
93
  UPLOAD_PRESET_NOT_FOUND: String
81
94
  UPLOAD_PRESET_DISABLED: String
82
95
  UPLOAD_PRESET_TOKEN_REQUIRED: String
data/sig/types.rbs CHANGED
@@ -60,8 +60,8 @@ module CalmLens
60
60
  # Earlier posts to send, oldest first; only the posts CalmLens uses are sent.
61
61
  type moderation_context_input = { items: Array[moderation_context_item_input] }
62
62
 
63
- # ProcessingFeatures.ts: optional analyses to run, keyed by feature. Every feature is off unless
64
- # `enabled` is true.
63
+ # ProcessingFeatures.ts: optional analyses to run, keyed by feature. Link detection runs unless
64
+ # `enabled` is false; every other feature is off unless `enabled` is true.
65
65
  type processing_features_input = {
66
66
  ?ocr: { enabled: bool }?,
67
67
  ?person_detection: { enabled: bool }?,
@@ -308,21 +308,25 @@ module CalmLens
308
308
  # Link safety (preview, LinkSafety.ts).
309
309
  # What a link's destination is: `safe` when every reputation check passed, `adult` or `gambling`
310
310
  # from its domain's content categories, `phishing` or `malware` from a threat list or the domain's
311
- # security risks, and `unresolved` when a short link could not be followed or a check failed.
311
+ # security risks, and `unresolved` when a short link or redirect chain could not be followed or a
312
+ # check failed. Every address on a link's way is checked, and the most severe verdict is the
313
+ # link's.
312
314
  type link_safety_verdict = "safe" | "adult" | "gambling" | "phishing" | "malware" | "unresolved"
313
315
  # The verdicts a policy can act on.
314
316
  type flagged_link_verdict = "phishing" | "malware" | "adult" | "gambling" | "unresolved"
315
317
  # What a link does to its asset, weakest first. Purging content over a link is never offered.
316
318
  type link_safety_action = "allow" | "limit" | "age-restrict" | "block"
317
- # One reputation check of a link's destination: its domain's categories, or a threat-list lookup
318
- # of the destination URL. `failed` makes the link `unresolved`; `labels` is what the check reported.
319
+ # One reputation check of a link, every address it redirects through and its destination: their
320
+ # domain categories, or a threat-list lookup of their URLs. `failed` makes the link `unresolved`;
321
+ # `labels` is what the check reported for any of the addresses.
319
322
  type link_safety_evidence = {
320
323
  "check" => "domain-categories" | "threat-lists",
321
324
  "status" => "checked" | "failed",
322
325
  "labels" => Array[String]
323
326
  }
324
327
  # A link in the asset and what its destination is. `finalUrl` is where it leads after `redirects`
325
- # redirects, or `null` when a short link could not be followed.
328
+ # redirects, or `null` when a short link or redirect chain could not be
329
+ # followed.
326
330
  type checked_link = {
327
331
  "url" => String,
328
332
  "finalUrl" => String?,
@@ -450,7 +454,8 @@ module CalmLens
450
454
  ?blur: number,
451
455
  ?redact: {
452
456
  ?text: bool,
453
- ?personalInformation: bool
457
+ ?personalInformation: bool,
458
+ ?faces: bool
454
459
  },
455
460
  ?border: {
456
461
  ?color: String,
@@ -496,7 +501,8 @@ module CalmLens
496
501
  ?"blur" => number,
497
502
  ?"redact" => {
498
503
  ?"text" => bool,
499
- ?"personalInformation" => bool
504
+ ?"personalInformation" => bool,
505
+ ?"faces" => bool
500
506
  },
501
507
  ?"border" => {
502
508
  ?"color" => String,
@@ -533,12 +539,9 @@ module CalmLens
533
539
  }
534
540
  # </generated: image-transform-options>
535
541
 
536
- # One structured transform step. `imageTransform` is the only transform type.
537
- type transform_step_input = { type: "imageTransform", ?options: image_transform_options_input }
538
- type transform_step = { "type" => "imageTransform", ?"options" => image_transform_options }
539
- # A selector String with one step per path segment, such as `fit=cover,width=1024/blur=12`, or
540
- # structured steps, which the SDK serializes into a selector.
541
- type transform_input = String | transform_step_input | Array[transform_step_input]
542
+ # Transform options applied in one pass: a selector String such as `width=1024,fit=cover`, or
543
+ # structured options, which the SDK serializes into a selector.
544
+ type transform_input = String | image_transform_options_input
542
545
 
543
546
  type lens_type = "image" | "video" | "audio" | "document" | "website"
544
547
  # ENSURE_LENS_BODY_SCHEMA. Changing a saved lens needs a greater `version` (default 1).
@@ -549,7 +552,7 @@ module CalmLens
549
552
  active: bool,
550
553
  ?description: String?,
551
554
  ?version: Integer,
552
- ?transforms: Array[transform_step_input]?
555
+ ?transform: image_transform_options_input?
553
556
  }
554
557
  type lens = {
555
558
  "id" => String,
@@ -560,7 +563,7 @@ module CalmLens
560
563
  ?"description" => String?,
561
564
  "type" => lens_type,
562
565
  "active" => bool,
563
- ?"transforms" => Array[transform_step]?,
566
+ ?"transform" => image_transform_options?,
564
567
  "createdAt" => Integer,
565
568
  ?"updatedAt" => Integer
566
569
  }
@@ -619,6 +622,49 @@ module CalmLens
619
622
  "ordering" => Array[order]
620
623
  }
621
624
 
625
+ # ---- Policies (Policy.ts) ---------------------------------------------------------------------
626
+
627
+ type policy_preset_id = "general-community" | "children-platform" | "education-support" | "wholesome-community"
628
+ # What a policy does with one category: `action` when its policy score reaches 0.5 (default
629
+ # `block`), allowances that let covered content through or act on it more softly, and
630
+ # restrictions, which outweigh allowances.
631
+ type category_policy_settings = {
632
+ ?"action" => flagged_content_action,
633
+ ?"allowances" => Array[policy_rule_snapshot],
634
+ ?"restrictions" => Array[policy_rule_snapshot]
635
+ }
636
+ # Settings by category (`sexual`, `violence`, ...), and optionally link safety.
637
+ type policy_settings = {
638
+ "categorySettings" => Hash[String, category_policy_settings],
639
+ ?"linkSafety" => link_safety_settings
640
+ }
641
+ # Where a policy started from a built-in preset. `version` is the release it is pinned to,
642
+ # `latestVersion` the newest; `own` is whether this is the organisation's own policy for the preset
643
+ # rather than a copy made from it. An organisation has at most one per preset, and only it can
644
+ # follow automatic updates.
645
+ type policy_preset_origin = {
646
+ "id" => policy_preset_id,
647
+ "version" => Integer,
648
+ "latestVersion" => Integer,
649
+ "own" => bool
650
+ }
651
+ # A moderation policy. Policies belong to the project's organisation and its projects share them.
652
+ # Send `revision` with each change; a policy changed since is refused with `policy/revision-conflict`.
653
+ type policy = {
654
+ "id" => String,
655
+ "name" => String,
656
+ "description" => String,
657
+ "settings" => policy_settings,
658
+ "revision" => Integer,
659
+ "automaticUpdates" => bool,
660
+ "preset" => policy_preset_origin?,
661
+ "projectIds" => Array[String],
662
+ "createdAt" => Integer,
663
+ "updatedAt" => Integer
664
+ }
665
+ # Every policy of the project's organisation, oldest first, and the one the project moderates with.
666
+ type policy_list = { "policies" => Array[policy], "projectPolicyId" => String }
667
+
622
668
  # ---- Events and webhooks (Event.ts, EventCatalog.ts, WebhookPayload.ts) ----------------------
623
669
 
624
670
  # EVENT_TYPES in EventCatalog.ts. Ignore types your code does not handle. Webhook, Slack and email
@@ -661,7 +707,14 @@ module CalmLens
661
707
  "createdAt" => Integer
662
708
  }
663
709
  # Unlike other lists, `nextCursor` is never nil: keep it to receive later events.
664
- type events_page = { "items" => Array[event], "nextCursor" => String, "hasMore" => bool }
710
+ # `excludedTypes` are the event types the feed never includes, because the API key lacks the permission
711
+ # to see them.
712
+ type events_page = {
713
+ "items" => Array[event],
714
+ "nextCursor" => String,
715
+ "hasMore" => bool,
716
+ "excludedTypes" => Array[event_type]
717
+ }
665
718
 
666
719
  # ---- Method options -------------------------------------------------------------------------
667
720
 
@@ -720,7 +773,7 @@ module CalmLens
720
773
  # Defaults to `rendered` on the server; `original` takes no lens or transform.
721
774
  ?resource: "original" | "rendered",
722
775
  ?expires_in_seconds: Integer,
723
- # A lens runs before any additional transforms.
776
+ # Transform options replace the lens's options of the same name.
724
777
  ?lens: String,
725
778
  ?transform: transform_input,
726
779
  ?request_options: request_options
@@ -859,6 +912,37 @@ module CalmLens
859
912
  ?request_options: request_options }
860
913
  type hash_list_entry_id_options = { entry_id: String, ?request_options: request_options }
861
914
 
915
+ # Policy settings as options: snake_case Symbol keys, or a policy's `settings` as the API returned it.
916
+ type policy_rule_input = { when: Array[String], action: flagged_content_action, ?name: String, ?match: "all", ?unless: Array[String] }
917
+ type category_policy_settings_input = {
918
+ ?action: flagged_content_action,
919
+ ?allowances: Array[policy_rule_input],
920
+ ?restrictions: Array[policy_rule_input]
921
+ }
922
+ type link_safety_settings_input = {
923
+ verdict_actions: Hash[String | Symbol, link_safety_action],
924
+ ?off_platform_links: { action: "limit" | "age-restrict" | "block", allowed_domains: Array[String] }
925
+ }
926
+ type policy_settings_input = policy_settings | {
927
+ category_settings: Hash[String | Symbol, category_policy_settings_input],
928
+ ?link_safety: link_safety_settings_input
929
+ }
930
+ type list_policies_options = { ?request_options: request_options }
931
+ type policy_id_options = { policy_id: String, ?request_options: request_options }
932
+ # Send `settings` (defining every category) with a `name`, or `preset_id` to copy the latest
933
+ # release of a built-in preset, named after it unless you give a `name`.
934
+ type create_policy_options = { ?name: String, ?description: String, ?settings: policy_settings_input,
935
+ ?preset_id: policy_preset_id, ?request_options: request_options }
936
+ type update_policy_options = { policy_id: String, revision: Integer, ?name: String, ?description: String,
937
+ ?settings: policy_settings_input, ?request_options: request_options }
938
+ # `reset` restores the pinned preset version (send `preset.version`); `update` moves an unchanged
939
+ # copy to the latest release (send `preset.latestVersion`).
940
+ type apply_policy_preset_options = { policy_id: String, operation: "reset" | "update", revision: Integer,
941
+ preset_version: Integer, ?request_options: request_options }
942
+ type set_policy_automatic_updates_options = { policy_id: String, enabled: bool, revision: Integer,
943
+ preset_version: Integer, ?request_options: request_options }
944
+ type assign_policy_options = { policy_id: String, ?request_options: request_options }
945
+
862
946
  # `page_size` is 1 to 100, default 50.
863
947
  type list_events_options = { ?cursor: String, ?page_size: Integer, ?request_options: request_options }
864
948
 
@@ -870,7 +954,7 @@ module CalmLens
870
954
  active: bool,
871
955
  ?description: String?,
872
956
  ?version: Integer,
873
- ?transforms: Array[transform_step_input]?,
957
+ ?transform: image_transform_options_input?,
874
958
  ?request_options: request_options
875
959
  }
876
960
 
@@ -881,7 +965,7 @@ module CalmLens
881
965
  # `rendered` is the default. `original` takes no lens or transform; `placeholder` is a small blurred PNG
882
966
  # that takes no lens or transform.
883
967
  ?resource: "original" | "rendered" | "placeholder",
884
- # A lens runs before any additional transforms.
968
+ # Transform options replace the lens's options of the same name.
885
969
  ?lens: String,
886
970
  ?transform: transform_input,
887
971
  # Appends `?ageConfirmed=true`, so a public `age-restricted` asset is served from this URL. Set it
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.18.3
4
+ version: 0.20.0
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-03 00:00:00.000000000 Z
11
+ date: 2026-11-04 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.'
@@ -30,6 +30,8 @@ files:
30
30
  - lib/calmlens/internal.rb
31
31
  - lib/calmlens/lenses.rb
32
32
  - lib/calmlens/moderation_context.rb
33
+ - lib/calmlens/option_keys.rb
34
+ - lib/calmlens/policies.rb
33
35
  - lib/calmlens/resource.rb
34
36
  - lib/calmlens/response.rb
35
37
  - lib/calmlens/server_client.rb
@@ -39,10 +41,16 @@ files:
39
41
  - lib/calmlens/testing/fake_api.rb
40
42
  - lib/calmlens/testing/mock_decision.rb
41
43
  - lib/calmlens/testing/placeholder.rb
44
+ - lib/calmlens/testing/test_image_grid.rb
45
+ - lib/calmlens/testing/test_input_scenario.rb
46
+ - lib/calmlens/testing/test_inputs.rb
42
47
  - lib/calmlens/text.rb
48
+ - lib/calmlens/transform_rules.rb
49
+ - lib/calmlens/transform_schema.rb
43
50
  - lib/calmlens/transforms.rb
44
51
  - lib/calmlens/transport.rb
45
52
  - lib/calmlens/upload_body.rb
53
+ - lib/calmlens/upload_mime_types.rb
46
54
  - lib/calmlens/upload_tokens.rb
47
55
  - lib/calmlens/version.rb
48
56
  - lib/calmlens/webhooks.rb