@things-factory/figure-service 10.1.112 → 10.1.114

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-service",
3
- "version": "10.1.112",
3
+ "version": "10.1.114",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -25,16 +25,16 @@
25
25
  "test:templates": "node --test scripts/templates.test.mts"
26
26
  },
27
27
  "dependencies": {
28
- "@hatiolab/figure-model": "^0.2.10",
28
+ "@hatiolab/figure-model": "^0.2.12",
29
29
  "@operato/typeorm-history": "^10.0.0",
30
- "@things-factory/ai-client-base": "^10.1.81",
31
- "@things-factory/attachment-base": "^10.1.112",
32
- "@things-factory/auth-base": "^10.1.111",
30
+ "@things-factory/ai-client-base": "^10.1.114",
31
+ "@things-factory/attachment-base": "^10.1.113",
32
+ "@things-factory/auth-base": "^10.1.113",
33
33
  "@things-factory/env": "^10.1.20",
34
34
  "@things-factory/shell": "^10.1.106",
35
35
  "graphql-scalars": "^1.22.4",
36
36
  "graphql-upload": "^15.0.2",
37
37
  "koa-router": "^7.4.0"
38
38
  },
39
- "gitHead": "e300d1b60b98ccb80380afe8df6d2807b1bcff91"
39
+ "gitHead": "0d8f9bc0ff0cb50431f2c76cb166aaf3809d3cf7"
40
40
  }
package/server/index.ts CHANGED
@@ -26,6 +26,8 @@ process.on('bootstrap-module-start' as any, () => {
26
26
  */
27
27
  import { getDataSource, inBootTurn } from '@things-factory/shell'
28
28
  import { moveBase64Thumbnails } from './service/figure/figure-thumbnail-upgrade.js'
29
+ /* Who may read a figure picture by its attachment path (ADR-0106): registered with attachment-base at load. */
30
+ import './service/figure/figure-thumbnail-reader.js'
29
31
  process.on('bootstrap-module-start' as any, () => {
30
32
  inBootTurn(() => moveBase64Thumbnails(getDataSource()))
31
33
  .then(moved => {
@@ -105,15 +105,22 @@ export async function sendFigureThumbnail(
105
105
  attachment 의 공개 경로가 쓰는 `public` 을 따라 쓰지 않는다(판정 조건 1). 같은 내용은 같은 해시이므로 ETag 가
106
106
  곧 내용이다. 부르는 쪽이 `?v=` 로 판을 가리므로 오래 두어도 낡은 그림이 남지 않는다.
107
107
  */
108
- context.type = picture.mimetype
109
- context.set('Cache-Control', 'private, max-age=31536000')
110
- context.set('ETag', `"${found.thumbnailHash}"`)
111
- if (found.thumbnailUpdatedAt) context.set('Last-Modified', new Date(found.thumbnailUpdatedAt).toUTCString())
108
+ const ours = () => {
109
+ context.type = picture.mimetype
110
+ context.set('Cache-Control', 'private, max-age=31536000')
111
+ context.set('ETag', `"${found.thumbnailHash}"`)
112
+ if (found.thumbnailUpdatedAt) context.set('Last-Modified', new Date(found.thumbnailUpdatedAt).toUTCString())
113
+ }
114
+ ours()
115
+ /* koa answers `fresh` only for a 2xx: before a body the status is still 404, and the check never matched. */
116
+ context.status = 200
112
117
  if (context.fresh) {
113
118
  context.status = 304
114
119
  return
115
120
  }
116
121
  await files.send(context, picture.path!, next)
122
+ /* The storage sets its own (S3 its ETag, attachment's Cache-Control): this door's are said last. */
123
+ ours()
117
124
  }
118
125
 
119
126
  /**
@@ -37,7 +37,10 @@ function mapFiles() {
37
37
  kept.set(path, bytes)
38
38
  return { id, path, size: bytes.length }
39
39
  },
40
+ /* As S3 does: its own tag and cache rule on the way out. */
40
41
  async send(koa: any, path: string) {
42
+ koa.set('ETag', '"storage-md5"')
43
+ koa.set('Cache-Control', 'public, max-age=31536000')
41
44
  koa.body = kept.get(path)
42
45
  }
43
46
  }
@@ -89,15 +92,16 @@ const attachments = (domainId?: string) =>
89
92
  function koa(ifNoneMatch?: string) {
90
93
  const headers: Record<string, string> = {}
91
94
  return {
92
- status: 200,
95
+ status: 404,
93
96
  type: '',
94
97
  body: undefined as any,
95
98
  headers,
96
99
  set(k: string, v: string) {
97
100
  headers[k] = v
98
101
  },
102
+ /* As koa: fresh only for a 2xx or 304, and only when the request's tag is the response's. */
99
103
  get fresh() {
100
- return !!ifNoneMatch && ifNoneMatch === headers['ETag']
104
+ return ((this.status >= 200 && this.status < 300) || this.status === 304) && !!ifNoneMatch && ifNoneMatch === headers['ETag']
101
105
  }
102
106
  }
103
107
  }
@@ -216,3 +220,17 @@ describe('the base64 an install holds, moved at start without losing one', () =>
216
220
  expect((await attachments(domain.id)).length).toBe(3)
217
221
  })
218
222
  })
223
+
224
+ /* ADR-0106 decision 2: by its attachment path, a figure picture opens to whoever may query figures, as by its figure. */
225
+ describe('read by its attachment path', () => {
226
+ it('figure registers the reader for its pictures, and it asks the figure query permission', async () => {
227
+ const { figureThumbnailReader } = loadCompiled('service/figure/figure-thumbnail-reader')
228
+ expect(attachmentBase.attachmentReaderOf('figure-thumbnail')).toBeDefined()
229
+ const asked: any[] = []
230
+ const check = async (...args: any[]) => (asked.push(args), false)
231
+ const context = { state: { domain: { id: 'd' }, user: { id: 'u' }, unsafeIP: false, prohibitedPrivileges: [] } }
232
+ expect(await figureThumbnailReader({}, context, check)).toBe(false)
233
+ expect(asked[0][0]).toEqual({ category: 'figure', privilege: 'query', owner: true })
234
+ expect(await figureThumbnailReader({}, context, async () => true)).toBe(true)
235
+ })
236
+ })
@@ -0,0 +1,20 @@
1
+ import { registerAttachmentReader } from '@things-factory/attachment-base'
2
+ import { checkPermission } from '@things-factory/auth-base'
3
+
4
+ import { THUMBNAIL_REF_TYPE } from './figure-thumbnail-store.js'
5
+
6
+ /*
7
+ * Apart from the store, and loaded only by the server's entry: the store is read by pure modules (the save path,
8
+ * figure-ui's tests), and auth-base brings shell's routers with it.
9
+ */
10
+ /**
11
+ * Who may read a figure picture by its attachment path (ADR-0106 decision 2): whoever may query figures -- the same door
12
+ * `/figure-thumbnail/:id` keeps, so the picture is no more open by its path than by its figure. attachment-base has
13
+ * already checked the attachment is of the request's domain.
14
+ */
15
+ export async function figureThumbnailReader(_attachment: unknown, context: any, check: typeof checkPermission = checkPermission): Promise<boolean> {
16
+ const { domain, user, unsafeIP, prohibitedPrivileges } = context?.state ?? {}
17
+ return !!(await check({ category: 'figure', privilege: 'query', owner: true }, user, domain, unsafeIP, prohibitedPrivileges))
18
+ }
19
+
20
+ registerAttachmentReader(THUMBNAIL_REF_TYPE, (attachment, context) => figureThumbnailReader(attachment, context))
@@ -105,3 +105,4 @@ export async function keepThumbnail(
105
105
  })
106
106
  return hash
107
107
  }
108
+