@12-apps/storage 1.0.0 → 2.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/storage",
3
- "version": "1.0.0",
3
+ "version": "2.0.1",
4
4
  "type": "module",
5
5
  "description": "Image uploads that land at YOUR OWN ORIGIN, never at the bucket (12-20). Isomorphic core (format allowlist, upload ceiling, object-key grammar, rendition set, refusal taxonomy, wire schemas), host-mounted backend surface (./server: createApiStorage, the local-disk driver, the image-pipeline port, the orphan reclaim), Hono adapter (./hono), the S3-compatible driver behind its own subpath (./s3) and the bound picker + upload hook (./react). Standardized adoption contract in ADOPTING.md.",
6
6
  "exports": {
@@ -40,12 +40,12 @@
40
40
  }
41
41
  },
42
42
  "dependencies": {
43
- "@12-apps/ui": "^4.0.1",
43
+ "@12-apps/ui": "^6.1.0",
44
44
  "zod": "^4.3.5"
45
45
  },
46
46
  "devDependencies": {
47
- "@12-apps/eslint-config": "^1.20.0",
48
- "@12-apps/typescript-config": "^1.20.0",
47
+ "@12-apps/eslint-config": "^1.21.1",
48
+ "@12-apps/typescript-config": "^1.20.1",
49
49
  "@aws-sdk/client-s3": "^3.703.0",
50
50
  "@testing-library/react": "^16.1.0",
51
51
  "@types/react": "19.2.2",
@@ -58,6 +58,38 @@ export interface WebStorageMessages {
58
58
  session_expired: string;
59
59
  /** `fetch` rejected outright; no response ever arrived. */
60
60
  transport: string;
61
+
62
+ /**
63
+ * THE FIVE BELOW ARE THE REFUSALS THAT USED TO HAVE NO KNOB AT ALL.
64
+ *
65
+ * They are decided in the browser before a request leaves it (see
66
+ * {@link rejectFileUpfront}) or after a response nothing could parse, and they
67
+ * were written as literals inside those functions — not defaults a host could
68
+ * override, but sentences a host could not reach. The docstring at the top of
69
+ * this file already argued they should be here ("every sentence here reaches a
70
+ * store owner, so it is product copy"); these four simply never arrived.
71
+ *
72
+ * A host overriding `file_too_large` and finding its own words replaced two
73
+ * lines later by ours is the specific failure this closes.
74
+ *
75
+ * Three take the fact they are about, because the sentence is worthless
76
+ * without it: the size that was refused, the type that was refused. Functions
77
+ * rather than tokens in a string, so a host writing "8,4 MB é grande demais"
78
+ * can put the number where its own grammar wants it.
79
+ */
80
+
81
+ /** A 0-byte file — an interrupted copy or an unreadable share. */
82
+ empty_file: string;
83
+ /** Over the ceiling, before any request. Both sizes already formatted. */
84
+ file_too_large_upfront: (context: { size: string; limit: string }) => string;
85
+ /** The OS identified no type at all — an extensionless drop, most often. */
86
+ unknown_content_type: string;
87
+ /** A type we do not accept. Carries it, because the owner will dispute it. */
88
+ unsupported_content_type_upfront: (context: { contentType: string }) => string;
89
+ /** 2xx with no key in the body — accepted, and unusable. */
90
+ missing_key: string;
91
+ /** Anything thrown on the way. The last resort, and it says so. */
92
+ upload_failed: string;
61
93
  }
62
94
 
63
95
  /** Copy that only makes sense with the configured ceiling in it. */
@@ -82,16 +114,34 @@ export function defaultWebStorageMessages(
82
114
  not_found: 'Essa imagem não está mais disponível.',
83
115
  session_expired: 'Sua sessão expirou. Entre novamente e repita o envio.',
84
116
  transport: 'Não foi possível falar com o servidor. Verifique sua conexão e tente de novo.',
117
+ empty_file: 'Arquivo vazio (0 bytes). Escolha a imagem novamente.',
118
+ file_too_large_upfront: ({ size, limit }) =>
119
+ `Imagem muito grande: ${size}. O limite é ${limit} — reduza a imagem e tente de novo.`,
120
+ unknown_content_type:
121
+ 'Não foi possível identificar o tipo do arquivo. Envie um PNG, JPG ou WebP.',
122
+ unsupported_content_type_upfront: ({ contentType }) =>
123
+ `Formato não suportado (${contentType}). Envie PNG, JPG, WebP ou GIF.`,
124
+ missing_key: 'O servidor aceitou a imagem mas não devolveu a chave. Tente de novo.',
125
+ upload_failed: 'Falha ao enviar a imagem. Tente de novo em instantes.',
85
126
  };
86
127
  }
87
128
 
88
- function resolve(
129
+ /**
130
+ * The messages in force for a mount's ceiling.
131
+ *
132
+ * Exported because `use-upload` needs the same table for the two refusals it
133
+ * phrases itself, and a second `{ ...defaults, ...overrides }` there would be
134
+ * the exact drift this file exists to prevent.
135
+ */
136
+ export function messagesOf(
89
137
  maxBytes: number,
90
- overrides: Partial<WebStorageMessages> | undefined,
138
+ overrides?: Partial<WebStorageMessages>,
91
139
  ): WebStorageMessages {
92
140
  return { ...defaultWebStorageMessages({ limit: megabytes(maxBytes) }), ...overrides };
93
141
  }
94
142
 
143
+ const resolve = messagesOf;
144
+
95
145
  /**
96
146
  * The sentence for a refusal CODE, or `undefined` for a code with no entry.
97
147
  *
@@ -101,7 +151,11 @@ function resolve(
101
151
  */
102
152
  function sentenceFor(messages: WebStorageMessages, code: string): string | undefined {
103
153
  if (!Object.prototype.hasOwnProperty.call(messages, code)) return undefined;
104
- return messages[code as keyof WebStorageMessages];
154
+ const entry = messages[code as keyof WebStorageMessages];
155
+ // Only the plain-string entries are reachable by a server CODE. The three
156
+ // that take a fact are browser-side refusals with no server equivalent, and
157
+ // a response naming one would otherwise render a function at a store owner.
158
+ return typeof entry === 'string' ? entry : undefined;
105
159
  }
106
160
 
107
161
  /**
@@ -116,20 +170,26 @@ function sentenceFor(messages: WebStorageMessages, code: string): string | undef
116
170
  * extensionless drop, most often) — worth its own sentence, because "formato não
117
171
  * suportado" reads as an accusation about a file the owner knows is a PNG.
118
172
  */
119
- export function rejectFileUpfront(file: File, maxBytes: number): string | null {
173
+ export function rejectFileUpfront(
174
+ file: File,
175
+ maxBytes: number,
176
+ overrides?: Partial<WebStorageMessages>,
177
+ ): string | null {
178
+ const messages = resolve(maxBytes, overrides);
120
179
  if (file.size === 0) {
121
- return 'Arquivo vazio (0 bytes). Escolha a imagem novamente.';
180
+ return messages.empty_file;
122
181
  }
123
182
  if (file.size > maxBytes) {
124
- return `Imagem muito grande: ${megabytes(file.size)}. O limite é ${megabytes(
125
- maxBytes,
126
- )} — reduza a imagem e tente de novo.`;
183
+ return messages.file_too_large_upfront({
184
+ size: megabytes(file.size),
185
+ limit: megabytes(maxBytes),
186
+ });
127
187
  }
128
188
  if (file.type === '') {
129
- return 'Não foi possível identificar o tipo do arquivo. Envie um PNG, JPG ou WebP.';
189
+ return messages.unknown_content_type;
130
190
  }
131
191
  if (!ACCEPTED_CONTENT_TYPES.includes(file.type)) {
132
- return `Formato não suportado (${file.type}). Envie PNG, JPG, WebP ou GIF.`;
192
+ return messages.unsupported_content_type_upfront({ contentType: file.type });
133
193
  }
134
194
  return null;
135
195
  }
@@ -2,6 +2,7 @@ import { useState } from 'react';
2
2
 
3
3
  import { STORAGE_PATHS } from '../paths';
4
4
  import {
5
+ messagesOf,
5
6
  rejectFileUpfront,
6
7
  transportFailure,
7
8
  uploadFailure,
@@ -87,7 +88,7 @@ export function useUpload(config: UseUploadConfig): UploadState {
87
88
  const payload = (await response.json()) as { data?: { imageKey?: string } };
88
89
  const key = payload.data?.imageKey;
89
90
  if (!key) {
90
- setError('O servidor aceitou a imagem mas não devolveu a chave. Tente de novo.');
91
+ setError(messagesOf(config.maxBytes, config.messages).missing_key);
91
92
  return null;
92
93
  }
93
94
  return key;
@@ -103,14 +104,14 @@ export function useUpload(config: UseUploadConfig): UploadState {
103
104
  const upright =
104
105
  options.optimize === false ? file : await optimizeImage(file, config.profile);
105
106
 
106
- const upfront = rejectFileUpfront(upright, config.maxBytes);
107
+ const upfront = rejectFileUpfront(upright, config.maxBytes, config.messages);
107
108
  if (upfront) {
108
109
  setError(upfront);
109
110
  return null;
110
111
  }
111
112
  return await send(upright);
112
113
  } catch {
113
- setError('Falha ao enviar a imagem. Tente de novo em instantes.');
114
+ setError(messagesOf(config.maxBytes, config.messages).upload_failed);
114
115
  return null;
115
116
  } finally {
116
117
  setUploading(false);