studio-engine 0.67.1 → 0.67.2

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: 69e6eab8f9186157077a967e6fd21a6b075a94245969f26a42b57bf5154e59f0
4
- data.tar.gz: d88f16a0143527684b091c40c0fd33dfc561661381fdcdc32ac08d09fa27119b
3
+ metadata.gz: 79895e24dd5ade937ec8c0b4afacd84605ad495530a4c37f94e053a2c1040170
4
+ data.tar.gz: 0f1716ef07d10fc5388ea15853767f6c7e8911e076e280a096a8424a494eba9c
5
5
  SHA512:
6
- metadata.gz: de70d91aedcb0d23734ddde03e24272df70a982eea531583247608451f5da7bf86f485857cdc6ceeefaedf91319d318e56254d6e90c702be91b4317f0d34af68
7
- data.tar.gz: 507a127ab69e870f0f82c0cfff93b5ca2d68b5241c4925eb58fae838ff78821a219614c31c2952fd7fe197b2f2c7eb2e906bfd3feff5ff3d9e973e8cdce15e78
6
+ metadata.gz: 7c6e391302c996753b399019f2baeded267820a69d1f7451fe7ff68f82609fc625cc477cbdebb6127f74d0d5ed3a326ed4357d02c24b595e0fd1c213c3605895
7
+ data.tar.gz: 26bda400a871f3998a7baa64c72f43ed71a2d6d4ff177e02c4c6ae76d9bff65438e0bb33d48564802ea982bc2f628631906fc25e0a24167ec11d38796908310d
data/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ### Fixed
8
+
9
+ - **Knowledge layer hardening** — the five findings from the 0.67.0 reviews:
10
+ upload keys carry a random suffix (same-named uploads in the same second no
11
+ longer overwrite the first object before the unique index can refuse);
12
+ `normalize_path` drops dot segments (`a/../b` → `a/b` — the S3 key mirrors
13
+ the path, and a `..` reads as traversal to every scanner); `access_for`
14
+ normalizes the queried agent the way writes normalize keys (a capitalized
15
+ `Studio.knowledge_agents` roster entry no longer reads `none`); the inbox
16
+ badge counts before the status filter (filtering to `filed` no longer zeroes
17
+ it); chip styles moved to one shared partial (`_chip_styles`); and reference
18
+ migrations are now written omakase-rubocop-compatible, with the convention
19
+ noted in the migration header (consumer copies are linted by the consumer's
20
+ rubocop — measured on the Industries adoption ship).
21
+
7
22
  ### Added
8
23
 
9
24
  - **Knowledge layer primitive** — `Studio::KnowledgeDoc` + `/admin/knowledge`:
@@ -25,11 +25,14 @@ module Studio
25
25
 
26
26
  scope = Studio::KnowledgeDoc.order(created_at: :desc)
27
27
  scope = scope.for_entity(@entity) if @entity
28
- scope = scope.where(status: @status) if @status
29
28
 
30
- @entities = Studio::KnowledgeDoc.distinct.pluck(:entity).sort
29
+ @entities = Studio::KnowledgeDoc.distinct.pluck(:entity).sort
30
+ # Counted BEFORE the status filter: the badge answers "what waits for
31
+ # triage in this entity", and filtering to status=filed must not zero it.
31
32
  @inbox_size = scope.inbox.count
32
33
 
34
+ scope = scope.where(status: @status) if @status
35
+
33
36
  if @view == "folders"
34
37
  @folders = scope.folders_under(@folder)
35
38
  @docs = scope.in_folder(@folder)
@@ -93,8 +93,13 @@ module Studio
93
93
  end
94
94
 
95
95
  # "/a//b/" -> "a/b". Nil-safe; the empty string is the root folder.
96
+ # Dot segments are dropped ("a/../b" -> "a/b"): harmless server-side, but
97
+ # the S3 key mirrors the path and a ".." reads as traversal to every
98
+ # scanner and human who ever greps the bucket.
96
99
  def normalize_path(value)
97
- value.to_s.strip.squeeze("/").delete_prefix("/").delete_suffix("/")
100
+ value.to_s.strip.squeeze("/").split("/")
101
+ .reject { |segment| segment.empty? || segment == "." || segment == ".." }
102
+ .join("/")
98
103
  end
99
104
 
100
105
  def default_title(file)
@@ -105,8 +110,11 @@ module Studio
105
110
 
106
111
  # --- access ---------------------------------------------------------------
107
112
 
113
+ # The queried agent is normalized the same way written keys are, so a
114
+ # capitalized Studio.knowledge_agents roster entry reads its real level
115
+ # instead of silently defaulting to "none".
108
116
  def access_for(agent)
109
- level = access.is_a?(Hash) ? access[agent.to_s] : nil
117
+ level = access.is_a?(Hash) ? access[agent.to_s.strip.downcase] : nil
110
118
  ACCESS_LEVELS.include?(level) ? level : "none"
111
119
  end
112
120
 
@@ -133,9 +141,13 @@ module Studio
133
141
  content_type = file.respond_to?(:content_type) ? file.content_type : nil
134
142
  body = file.respond_to?(:read) ? file.read : file.to_s
135
143
 
144
+ # The random suffix is load-bearing: the timestamp is second-granularity,
145
+ # so two same-named uploads in one second would otherwise write the same
146
+ # key — the second S3 PUT overwrites the first object BEFORE the unique
147
+ # index rejects the second row (found in PR #251 review, twice over).
136
148
  key = [
137
149
  "knowledge", entity, path.presence,
138
- "#{Time.current.strftime('%Y%m%d%H%M%S')}-#{self.class.sanitize_filename(filename)}"
150
+ "#{Time.current.strftime('%Y%m%d%H%M%S')}-#{SecureRandom.hex(4)}-#{self.class.sanitize_filename(filename)}"
139
151
  ].compact.join("/")
140
152
 
141
153
  Studio::S3.upload(key: key, body: body, content_type: content_type)
@@ -11,12 +11,8 @@
11
11
  Styling note: gem-specific looks live in the scoped <style> below because a
12
12
  host's Tailwind build may not scan this gem's views (same reasoning as the
13
13
  geo grid). Layout leans on utilities every host already ships. %>
14
+ <%= render "studio/knowledge_docs/chip_styles" %>
14
15
  <style>
15
- .knowledge-chip { display: inline-block; padding: 0 .5rem; border-radius: 9999px;
16
- font-size: .7rem; line-height: 1.4rem; border: 1px solid transparent; white-space: nowrap; }
17
- .knowledge-chip-full { background: rgb(16 185 129 / .12); border-color: rgb(16 185 129 / .4); color: rgb(5 150 105); }
18
- .knowledge-chip-aware { background: rgb(245 158 11 / .12); border-color: rgb(245 158 11 / .4); color: rgb(180 83 9); }
19
- .knowledge-chip-none { background: rgb(148 163 184 / .12); border-color: rgb(148 163 184 / .4); color: rgb(100 116 139); }
20
16
  .knowledge-status { font-size: .7rem; padding: 0 .45rem; border-radius: .25rem; border: 1px solid rgb(148 163 184 / .5); }
21
17
  .knowledge-status-inbox { border-color: rgb(245 158 11 / .6); color: rgb(180 83 9); }
22
18
  .knowledge-status-superseded { text-decoration: line-through; opacity: .6; }
@@ -0,0 +1,10 @@
1
+ <%# The access-chip look, shared by the browser and the show page — one
2
+ definition so the two can never drift. Scoped <style> rather than utility
3
+ classes because a host's Tailwind build may not scan this gem's views. %>
4
+ <style>
5
+ .knowledge-chip { display: inline-block; padding: 0 .5rem; border-radius: 9999px;
6
+ font-size: .7rem; line-height: 1.4rem; border: 1px solid transparent; white-space: nowrap; }
7
+ .knowledge-chip-full { background: rgb(16 185 129 / .12); border-color: rgb(16 185 129 / .4); color: rgb(5 150 105); }
8
+ .knowledge-chip-aware { background: rgb(245 158 11 / .12); border-color: rgb(245 158 11 / .4); color: rgb(180 83 9); }
9
+ .knowledge-chip-none { background: rgb(148 163 184 / .12); border-color: rgb(148 163 184 / .4); color: rgb(100 116 139); }
10
+ </style>
@@ -1,12 +1,6 @@
1
1
  <%# /admin/knowledge/:id — one document: metadata, access, download, and the
2
2
  triage form that files it out of the inbox. %>
3
- <style>
4
- .knowledge-chip { display: inline-block; padding: 0 .5rem; border-radius: 9999px;
5
- font-size: .7rem; line-height: 1.4rem; border: 1px solid transparent; white-space: nowrap; }
6
- .knowledge-chip-full { background: rgb(16 185 129 / .12); border-color: rgb(16 185 129 / .4); color: rgb(5 150 105); }
7
- .knowledge-chip-aware { background: rgb(245 158 11 / .12); border-color: rgb(245 158 11 / .4); color: rgb(180 83 9); }
8
- .knowledge-chip-none { background: rgb(148 163 184 / .12); border-color: rgb(148 163 184 / .4); color: rgb(100 116 139); }
9
- </style>
3
+ <%= render "studio/knowledge_docs/chip_styles" %>
10
4
 
11
5
  <div class="mb-4 text-sm">
12
6
  <%= link_to "← Knowledge", admin_knowledge_path(folder: @doc.path.presence) %>
@@ -29,7 +23,7 @@
29
23
  <div><dt class="opacity-60">Document date</dt><dd><%= @doc.document_date&.iso8601 || "—" %></dd></div>
30
24
  <div><dt class="opacity-60">Uploaded</dt><dd><%= @doc.created_at&.to_date&.iso8601 %> by <%= @doc.uploaded_by.presence || "—" %></dd></div>
31
25
  <div><dt class="opacity-60">Source</dt><dd><%= @doc.source_note.presence || "—" %></dd></div>
32
- <div><dt class="opacity-60">File</dt><dd><%= @doc.file? ? "#{@doc.mime_type.presence || 'file'} · #{number_to_human_size(@doc.byte_size) if @doc.byte_size}" : "metadata only" %></dd></div>
26
+ <div><dt class="opacity-60">File</dt><dd><%= @doc.file? ? [ @doc.mime_type.presence || "file", (number_to_human_size(@doc.byte_size) if @doc.byte_size) ].compact.join(" · ") : "metadata only" %></dd></div>
33
27
  <div class="md:col-span-2"><dt class="opacity-60">Access</dt><dd><%= render "studio/knowledge_docs/access_chips", doc: @doc %></dd></div>
34
28
  </dl>
35
29
 
@@ -3,6 +3,11 @@
3
3
  # (`bin/rails studio_engine:install:migrations && bin/rails db:migrate`) so the
4
4
  # table is created in the app's database. Do not hand-copy — a hand copy
5
5
  # collides with the installed copy on `class CreateStudioKnowledgeDocs`.
6
+ #
7
+ # STYLE NOTE: reference migrations are written omakase-rubocop-compatible
8
+ # (spaces inside array brackets) because the installed copy is linted by the
9
+ # CONSUMER's rubocop, not this gem's — an engine-styled copy fails the
10
+ # consumer's cert untouched (measured on the Industries adoption ship).
6
11
  class CreateStudioKnowledgeDocs < ActiveRecord::Migration[7.2]
7
12
  def change
8
13
  create_table :studio_knowledge_docs do |t|
@@ -41,8 +46,8 @@ class CreateStudioKnowledgeDocs < ActiveRecord::Migration[7.2]
41
46
  t.timestamps
42
47
  end
43
48
 
44
- add_index :studio_knowledge_docs, [:entity, :status]
45
- add_index :studio_knowledge_docs, [:entity, :path]
49
+ add_index :studio_knowledge_docs, [ :entity, :status ]
50
+ add_index :studio_knowledge_docs, [ :entity, :path ]
46
51
  add_index :studio_knowledge_docs, :s3_key, unique: true
47
52
  add_index :studio_knowledge_docs, :superseded_by_id
48
53
  end
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.67.1"
2
+ VERSION = "0.67.2"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.67.1
4
+ version: 0.67.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
@@ -440,6 +440,7 @@ files:
440
440
  - app/views/studio/geo_settings/edit.html.erb
441
441
  - app/views/studio/knowledge_docs/_access_chips.html.erb
442
442
  - app/views/studio/knowledge_docs/_browser.html.erb
443
+ - app/views/studio/knowledge_docs/_chip_styles.html.erb
443
444
  - app/views/studio/knowledge_docs/_doc_row.html.erb
444
445
  - app/views/studio/knowledge_docs/_upload_form.html.erb
445
446
  - app/views/studio/knowledge_docs/index.html.erb