@12-apps/storage 2.0.0 → 2.1.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.
- package/package.json +4 -4
- package/src/hono/index.ts +8 -3
- package/src/index.ts +1 -1
- package/src/problems.ts +0 -23
- package/src/pt-BR.ts +30 -0
- package/src/react/create-web-storage.tsx +14 -14
- package/src/react/failures.ts +30 -39
- package/src/react/index.ts +1 -1
- package/src/react/pt-BR.ts +38 -0
- package/src/react/use-upload.ts +6 -2
- package/src/server/create-api-storage.ts +9 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/storage",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
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": "^
|
|
43
|
+
"@12-apps/ui": "^6.2.0",
|
|
44
44
|
"zod": "^4.3.5"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@12-apps/eslint-config": "^1.
|
|
48
|
-
"@12-apps/typescript-config": "^1.20.
|
|
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",
|
package/src/hono/index.ts
CHANGED
|
@@ -38,8 +38,13 @@ export type ResolveStorageActor = (
|
|
|
38
38
|
|
|
39
39
|
export interface StorageHonoConfig extends ApiStorageConfig {
|
|
40
40
|
resolveActor: ResolveStorageActor;
|
|
41
|
-
/**
|
|
42
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The 401 body's sentence when `resolveActor` answers null — REQUIRED, the
|
|
43
|
+
* host's words (pt-BR hosts: `PT_BR_STORAGE_UNAUTHENTICATED` from
|
|
44
|
+
* `../pt-BR`). The old default was the same silent leak every other surface
|
|
45
|
+
* here just paid down.
|
|
46
|
+
*/
|
|
47
|
+
unauthenticatedMessage: string;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
export interface StorageHono extends ApiStorage {
|
|
@@ -67,7 +72,7 @@ function send(c: Context, response: StorageRouteResponse): Response {
|
|
|
67
72
|
export function storageRouter(config: StorageHonoConfig): StorageHono {
|
|
68
73
|
const { resolveActor, unauthenticatedMessage, ...apiConfig } = config;
|
|
69
74
|
const api = createApiStorage(apiConfig);
|
|
70
|
-
const unauthenticated = unauthenticatedMessage
|
|
75
|
+
const unauthenticated = unauthenticatedMessage;
|
|
71
76
|
const router = new Hono();
|
|
72
77
|
|
|
73
78
|
for (const route of api.routes) {
|
package/src/index.ts
CHANGED
|
@@ -70,12 +70,12 @@ export {
|
|
|
70
70
|
STORAGE_PROBLEM_STATUS,
|
|
71
71
|
StorageNotConfiguredError,
|
|
72
72
|
StorageProblemError,
|
|
73
|
-
defaultStorageMessages,
|
|
74
73
|
isStorageProblem,
|
|
75
74
|
type StorageMessageContext,
|
|
76
75
|
type StorageMessages,
|
|
77
76
|
type StorageProblem,
|
|
78
77
|
} from './problems';
|
|
78
|
+
export { PT_BR_STORAGE_MESSAGES, PT_BR_STORAGE_UNAUTHENTICATED } from './pt-BR';
|
|
79
79
|
|
|
80
80
|
export {
|
|
81
81
|
imageSources,
|
package/src/problems.ts
CHANGED
|
@@ -56,29 +56,6 @@ export interface StorageMessageContext {
|
|
|
56
56
|
limit: string;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/**
|
|
60
|
-
* The default pt-BR copy. `limit` is interpolated rather than hard-coded so a
|
|
61
|
-
* host that raises `maxBytes` cannot end up with a message naming the old
|
|
62
|
-
* number — the single commonest way this kind of copy goes stale.
|
|
63
|
-
*/
|
|
64
|
-
export function defaultStorageMessages(context: StorageMessageContext): StorageMessages {
|
|
65
|
-
return {
|
|
66
|
-
invalid_base64: 'A imagem enviada não está codificada corretamente em base64.',
|
|
67
|
-
empty_file: 'A imagem enviada está vazia.',
|
|
68
|
-
file_too_large: `A imagem enviada é maior que o limite de ${context.limit}.`,
|
|
69
|
-
content_mismatch:
|
|
70
|
-
'O conteúdo da imagem não corresponde ao formato informado. Envie um PNG, JPEG, WebP ou GIF.',
|
|
71
|
-
unsupported_content_type: 'Formato não suportado. Envie PNG, JPG, WebP ou GIF.',
|
|
72
|
-
image_unreadable:
|
|
73
|
-
'Não foi possível ler a imagem enviada — o arquivo parece estar corrompido.',
|
|
74
|
-
image_dimensions_too_large:
|
|
75
|
-
'A imagem enviada tem dimensões grandes demais para ser processada.',
|
|
76
|
-
storage_not_configured: 'O armazenamento de imagens ainda não foi configurado nesta loja.',
|
|
77
|
-
storage_unavailable:
|
|
78
|
-
'Não foi possível salvar a imagem agora. Tente novamente em instantes.',
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
59
|
/**
|
|
83
60
|
* A refusal, carrying the status and the finished sentence.
|
|
84
61
|
*
|
package/src/pt-BR.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { StorageMessageContext, StorageMessages } from './problems';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The pt-BR pack — a NAMED factory a host passes by hand
|
|
5
|
+
* (`messages: PT_BR_STORAGE_MESSAGES`), never a default. The filename is what
|
|
6
|
+
* exempts this file from the copy-portability gate: Portuguese may ship, it
|
|
7
|
+
* may not be silent. `limit` is interpolated rather than hard-coded so a
|
|
8
|
+
* host that raises `maxBytes` cannot end up with a message naming the old
|
|
9
|
+
* number — the single commonest way this kind of copy goes stale.
|
|
10
|
+
*/
|
|
11
|
+
export function PT_BR_STORAGE_MESSAGES(context: StorageMessageContext): StorageMessages {
|
|
12
|
+
return {
|
|
13
|
+
invalid_base64: 'A imagem enviada não está codificada corretamente em base64.',
|
|
14
|
+
empty_file: 'A imagem enviada está vazia.',
|
|
15
|
+
file_too_large: `A imagem enviada é maior que o limite de ${context.limit}.`,
|
|
16
|
+
content_mismatch:
|
|
17
|
+
'O conteúdo da imagem não corresponde ao formato informado. Envie um PNG, JPEG, WebP ou GIF.',
|
|
18
|
+
unsupported_content_type: 'Formato não suportado. Envie PNG, JPG, WebP ou GIF.',
|
|
19
|
+
image_unreadable:
|
|
20
|
+
'Não foi possível ler a imagem enviada — o arquivo parece estar corrompido.',
|
|
21
|
+
image_dimensions_too_large:
|
|
22
|
+
'A imagem enviada tem dimensões grandes demais para ser processada.',
|
|
23
|
+
storage_not_configured: 'O armazenamento de imagens ainda não foi configurado nesta loja.',
|
|
24
|
+
storage_unavailable:
|
|
25
|
+
'Não foi possível salvar a imagem agora. Tente novamente em instantes.',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The 401 the mounted router answers when the host resolves no actor. */
|
|
30
|
+
export const PT_BR_STORAGE_UNAUTHENTICATED = 'Não autenticado.';
|
|
@@ -4,7 +4,7 @@ import { Stack } from '@12-apps/ui/mui/Stack';
|
|
|
4
4
|
import { Text } from '@12-apps/ui/typography/Text';
|
|
5
5
|
|
|
6
6
|
import { DEFAULT_MAX_UPLOAD_BYTES, megabytes } from '../limits';
|
|
7
|
-
import type { WebStorageMessages } from './failures';
|
|
7
|
+
import type { WebStorageMessageContext, WebStorageMessages } from './failures';
|
|
8
8
|
import { ImageField, type ImageFieldProps } from './image-field';
|
|
9
9
|
import type { ImageProfile } from './optimize-image';
|
|
10
10
|
import { useUpload, type UploadState, type UseUploadConfig } from './use-upload';
|
|
@@ -33,13 +33,14 @@ export interface WebStorageConfig {
|
|
|
33
33
|
/** How requests are made. Default: same-origin `fetch`. */
|
|
34
34
|
fetchImpl?: typeof fetch;
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
36
|
+
* Every sentence this surface renders — REQUIRED, the host's own factory of
|
|
37
|
+
* the mount's ceiling, mirroring the server mount's `messages` (pt-BR
|
|
38
|
+
* hosts: `PT_BR_WEB_STORAGE_MESSAGES` from `./pt-BR`). Write `forbidden` to
|
|
39
|
+
* name YOUR reason: the package states only that the account may not
|
|
40
|
+
* upload, because `mayUpload` is one host-computed boolean and a package
|
|
41
|
+
* that named a role model would be wrong for every host but the first.
|
|
41
42
|
*/
|
|
42
|
-
messages
|
|
43
|
+
messages: (context: WebStorageMessageContext) => WebStorageMessages;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export interface WebStorage {
|
|
@@ -60,7 +61,7 @@ export function createWebStorage(config: WebStorageConfig): WebStorage {
|
|
|
60
61
|
maxBytes,
|
|
61
62
|
...(config.profile ? { profile: config.profile } : {}),
|
|
62
63
|
...(config.fetchImpl ? { fetchImpl: config.fetchImpl } : {}),
|
|
63
|
-
|
|
64
|
+
messages: config.messages,
|
|
64
65
|
};
|
|
65
66
|
const useBoundUpload = (): UploadState => useUpload(uploadConfig);
|
|
66
67
|
|
|
@@ -75,17 +76,16 @@ export function createWebStorage(config: WebStorageConfig): WebStorage {
|
|
|
75
76
|
*/
|
|
76
77
|
function UploadPage(): JSX.Element {
|
|
77
78
|
const [imageKey, setImageKey] = useState<string | null>(null);
|
|
79
|
+
const messages = config.messages({ limit: megabytes(maxBytes) });
|
|
78
80
|
return (
|
|
79
81
|
<Stack spacing={2} data-testid="storage-upload-page">
|
|
80
|
-
<Text as="h2">
|
|
81
|
-
<Text as="p">
|
|
82
|
-
{`Envie um PNG, JPG, WebP ou GIF de até ${megabytes(maxBytes)}.`}
|
|
83
|
-
</Text>
|
|
82
|
+
<Text as="h2">{messages.pageTitle}</Text>
|
|
83
|
+
<Text as="p">{messages.pageIntro}</Text>
|
|
84
84
|
<BoundImageField
|
|
85
85
|
value={imageKey}
|
|
86
86
|
onChange={setImageKey}
|
|
87
|
-
label=
|
|
88
|
-
helperText=
|
|
87
|
+
label={messages.fieldLabel}
|
|
88
|
+
helperText={messages.fieldHelper}
|
|
89
89
|
/>
|
|
90
90
|
</Stack>
|
|
91
91
|
);
|
package/src/react/failures.ts
CHANGED
|
@@ -88,8 +88,25 @@ export interface WebStorageMessages {
|
|
|
88
88
|
unsupported_content_type_upfront: (context: { contentType: string }) => string;
|
|
89
89
|
/** 2xx with no key in the body — accepted, and unusable. */
|
|
90
90
|
missing_key: string;
|
|
91
|
+
/**
|
|
92
|
+
* A refusal whose body carried no sentence and no code this table knows —
|
|
93
|
+
* the status (and the raw code, when there was one) ride along, because
|
|
94
|
+
* they are what turns "it failed" into something greppable.
|
|
95
|
+
*/
|
|
96
|
+
upload_failed_http: (context: { status: number; detail: string }) => string;
|
|
91
97
|
/** Anything thrown on the way. The last resort, and it says so. */
|
|
92
98
|
upload_failed: string;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The standalone upload page's own chrome — heading, the one-line
|
|
102
|
+
* instruction (which carries the ceiling, same rule as the refusals), the
|
|
103
|
+
* field label and its helper. These rendered as literals inside
|
|
104
|
+
* `createWebStorage` until copy became required config.
|
|
105
|
+
*/
|
|
106
|
+
pageTitle: string;
|
|
107
|
+
pageIntro: string;
|
|
108
|
+
fieldLabel: string;
|
|
109
|
+
fieldHelper: string;
|
|
93
110
|
}
|
|
94
111
|
|
|
95
112
|
/** Copy that only makes sense with the configured ceiling in it. */
|
|
@@ -98,34 +115,6 @@ export interface WebStorageMessageContext {
|
|
|
98
115
|
limit: string;
|
|
99
116
|
}
|
|
100
117
|
|
|
101
|
-
/**
|
|
102
|
-
* The default pt-BR copy. `limit` is interpolated rather than hard-coded for the
|
|
103
|
-
* same reason the server does it: a host that raises the ceiling must not end up
|
|
104
|
-
* with a message naming the old number.
|
|
105
|
-
*/
|
|
106
|
-
export function defaultWebStorageMessages(
|
|
107
|
-
context: WebStorageMessageContext,
|
|
108
|
-
): WebStorageMessages {
|
|
109
|
-
return {
|
|
110
|
-
forbidden: 'Sua conta não tem permissão para enviar imagens nesta loja.',
|
|
111
|
-
unsupported_content_type: 'Formato não suportado. Envie PNG, JPG, WebP ou GIF.',
|
|
112
|
-
file_too_large: `Imagem muito grande. O limite é ${context.limit}.`,
|
|
113
|
-
invalid_key: 'Não foi possível localizar essa imagem.',
|
|
114
|
-
not_found: 'Essa imagem não está mais disponível.',
|
|
115
|
-
session_expired: 'Sua sessão expirou. Entre novamente e repita o envio.',
|
|
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.',
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
118
|
/**
|
|
130
119
|
* The messages in force for a mount's ceiling.
|
|
131
120
|
*
|
|
@@ -135,9 +124,9 @@ export function defaultWebStorageMessages(
|
|
|
135
124
|
*/
|
|
136
125
|
export function messagesOf(
|
|
137
126
|
maxBytes: number,
|
|
138
|
-
|
|
127
|
+
factory: (context: WebStorageMessageContext) => WebStorageMessages,
|
|
139
128
|
): WebStorageMessages {
|
|
140
|
-
return
|
|
129
|
+
return factory({ limit: megabytes(maxBytes) });
|
|
141
130
|
}
|
|
142
131
|
|
|
143
132
|
const resolve = messagesOf;
|
|
@@ -173,9 +162,9 @@ function sentenceFor(messages: WebStorageMessages, code: string): string | undef
|
|
|
173
162
|
export function rejectFileUpfront(
|
|
174
163
|
file: File,
|
|
175
164
|
maxBytes: number,
|
|
176
|
-
|
|
165
|
+
factory: (context: WebStorageMessageContext) => WebStorageMessages,
|
|
177
166
|
): string | null {
|
|
178
|
-
const messages = resolve(maxBytes,
|
|
167
|
+
const messages = resolve(maxBytes, factory);
|
|
179
168
|
if (file.size === 0) {
|
|
180
169
|
return messages.empty_file;
|
|
181
170
|
}
|
|
@@ -230,9 +219,9 @@ async function statedError(response: Response): Promise<string | null> {
|
|
|
230
219
|
export async function uploadFailure(
|
|
231
220
|
response: Response,
|
|
232
221
|
maxBytes: number,
|
|
233
|
-
|
|
222
|
+
factory: (context: WebStorageMessageContext) => WebStorageMessages,
|
|
234
223
|
): Promise<string> {
|
|
235
|
-
const messages = resolve(maxBytes,
|
|
224
|
+
const messages = resolve(maxBytes, factory);
|
|
236
225
|
if (response.status === 401) return messages.session_expired;
|
|
237
226
|
const stated = await statedError(response);
|
|
238
227
|
// A sentence rather than a code: the server already phrased it for the owner,
|
|
@@ -245,7 +234,7 @@ export async function uploadFailure(
|
|
|
245
234
|
// what turns a support request from "it failed" into something greppable — so it
|
|
246
235
|
// rides along with the status rather than being mapped away.
|
|
247
236
|
const detail = stated ? ` (${stated})` : '';
|
|
248
|
-
return
|
|
237
|
+
return messages.upload_failed_http({ status: response.status, detail });
|
|
249
238
|
}
|
|
250
239
|
|
|
251
240
|
/**
|
|
@@ -254,8 +243,10 @@ export async function uploadFailure(
|
|
|
254
243
|
* One sentence, and it can afford to be definite: the upload goes to our own
|
|
255
244
|
* origin, so there is no cross-origin case to hedge about.
|
|
256
245
|
*/
|
|
257
|
-
export function transportFailure(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
246
|
+
export function transportFailure(
|
|
247
|
+
factory: (context: WebStorageMessageContext) => WebStorageMessages,
|
|
248
|
+
): string {
|
|
249
|
+
// No ceiling to interpolate on this one, so the factory is read without a
|
|
250
|
+
// limit rather than through a table built from a number that would be a lie.
|
|
251
|
+
return factory({ limit: '' }).transport;
|
|
261
252
|
}
|
package/src/react/index.ts
CHANGED
|
@@ -35,10 +35,10 @@ export {
|
|
|
35
35
|
} from './optimize-image';
|
|
36
36
|
|
|
37
37
|
export {
|
|
38
|
-
defaultWebStorageMessages,
|
|
39
38
|
rejectFileUpfront,
|
|
40
39
|
transportFailure,
|
|
41
40
|
uploadFailure,
|
|
42
41
|
type WebStorageMessageContext,
|
|
43
42
|
type WebStorageMessages,
|
|
44
43
|
} from './failures';
|
|
44
|
+
export { PT_BR_WEB_STORAGE_MESSAGES } from './pt-BR';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { WebStorageMessageContext, WebStorageMessages } from './failures';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The pt-BR pack — a NAMED factory a host passes by hand, never a default.
|
|
5
|
+
* `limit` is interpolated rather than hard-coded for the same reason the
|
|
6
|
+
* server pack does it: a host that raises the ceiling must not end up with
|
|
7
|
+
* copy naming the old number. The filename is what exempts this file from
|
|
8
|
+
* the copy-portability gate: Portuguese may ship, it may not be silent.
|
|
9
|
+
*/
|
|
10
|
+
export function PT_BR_WEB_STORAGE_MESSAGES(
|
|
11
|
+
context: WebStorageMessageContext,
|
|
12
|
+
): WebStorageMessages {
|
|
13
|
+
return {
|
|
14
|
+
forbidden: 'Sua conta não tem permissão para enviar imagens nesta loja.',
|
|
15
|
+
unsupported_content_type: 'Formato não suportado. Envie PNG, JPG, WebP ou GIF.',
|
|
16
|
+
file_too_large: `Imagem muito grande. O limite é ${context.limit}.`,
|
|
17
|
+
invalid_key: 'Não foi possível localizar essa imagem.',
|
|
18
|
+
not_found: 'Essa imagem não está mais disponível.',
|
|
19
|
+
session_expired: 'Sua sessão expirou. Entre novamente e repita o envio.',
|
|
20
|
+
transport: 'Não foi possível falar com o servidor. Verifique sua conexão e tente de novo.',
|
|
21
|
+
empty_file: 'Arquivo vazio (0 bytes). Escolha a imagem novamente.',
|
|
22
|
+
file_too_large_upfront: ({ size, limit }) =>
|
|
23
|
+
`Imagem muito grande: ${size}. O limite é ${limit} — reduza a imagem e tente de novo.`,
|
|
24
|
+
unknown_content_type:
|
|
25
|
+
'Não foi possível identificar o tipo do arquivo. Envie um PNG, JPG ou WebP.',
|
|
26
|
+
unsupported_content_type_upfront: ({ contentType }) =>
|
|
27
|
+
`Formato não suportado (${contentType}). Envie PNG, JPG, WebP ou GIF.`,
|
|
28
|
+
missing_key: 'O servidor aceitou a imagem mas não devolveu a chave. Tente de novo.',
|
|
29
|
+
upload_failed_http: ({ status, detail }) =>
|
|
30
|
+
`Não foi possível enviar a imagem (HTTP ${status}${detail}). Tente de novo em instantes.`,
|
|
31
|
+
upload_failed: 'Falha ao enviar a imagem. Tente de novo em instantes.',
|
|
32
|
+
|
|
33
|
+
pageTitle: 'Imagens da loja',
|
|
34
|
+
pageIntro: `Envie um PNG, JPG, WebP ou GIF de até ${context.limit}.`,
|
|
35
|
+
fieldLabel: 'Foto do produto',
|
|
36
|
+
fieldHelper: 'A imagem é reduzida no navegador antes do envio.',
|
|
37
|
+
};
|
|
38
|
+
}
|
package/src/react/use-upload.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
rejectFileUpfront,
|
|
7
7
|
transportFailure,
|
|
8
8
|
uploadFailure,
|
|
9
|
+
type WebStorageMessageContext,
|
|
9
10
|
type WebStorageMessages,
|
|
10
11
|
} from './failures';
|
|
11
12
|
import { optimizeImage, type ImageProfile } from './optimize-image';
|
|
@@ -56,8 +57,11 @@ export interface UseUploadConfig {
|
|
|
56
57
|
profile?: ImageProfile;
|
|
57
58
|
/** How the request is made. Default: same-origin `fetch`. */
|
|
58
59
|
fetchImpl?: typeof fetch;
|
|
59
|
-
/**
|
|
60
|
-
|
|
60
|
+
/**
|
|
61
|
+
* The refusal copy, as the host's own factory of the mount's ceiling —
|
|
62
|
+
* REQUIRED (pt-BR hosts: `PT_BR_WEB_STORAGE_MESSAGES` from `./pt-BR`).
|
|
63
|
+
*/
|
|
64
|
+
messages: (context: WebStorageMessageContext) => WebStorageMessages;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
export function useUpload(config: UseUploadConfig): UploadState {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ACCEPTED_CONTENT_TYPES } from '../content-types';
|
|
2
2
|
import { DEFAULT_KEY_PREFIX } from '../keys';
|
|
3
3
|
import { megabytes } from '../limits';
|
|
4
|
-
import {
|
|
4
|
+
import type { StorageMessageContext, StorageMessages } from '../problems';
|
|
5
5
|
import { CATALOG_RENDITIONS, type RenditionSpec } from '../renditions';
|
|
6
6
|
import { imageSources, objectUrl, versionedObjectUrl, type ImageSources } from '../urls';
|
|
7
7
|
import { inlineImageSchema, objectKeySchema, type InlineImage } from '../wire';
|
|
@@ -94,8 +94,13 @@ export interface ApiStorageConfig {
|
|
|
94
94
|
renditions?: readonly RenditionSpec[];
|
|
95
95
|
/** Top-level key segment. Default `products`. */
|
|
96
96
|
keyPrefix?: string;
|
|
97
|
-
/**
|
|
98
|
-
|
|
97
|
+
/**
|
|
98
|
+
* The sentence each refusal is stated in — REQUIRED, the host's words. A
|
|
99
|
+
* factory of the mount's own context so a host that raises `maxBytes`
|
|
100
|
+
* cannot end up with copy naming the old ceiling. pt-BR hosts pass
|
|
101
|
+
* `PT_BR_STORAGE_MESSAGES` from `../pt-BR`.
|
|
102
|
+
*/
|
|
103
|
+
messages: (context: StorageMessageContext) => StorageMessages;
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
/** What the mount reports about itself — the anti-drift surface. */
|
|
@@ -167,10 +172,7 @@ const SILENT: StorageLogger = { error: () => undefined };
|
|
|
167
172
|
export function createApiStorage(config: ApiStorageConfig): ApiStorage {
|
|
168
173
|
const specs = config.renditions ?? CATALOG_RENDITIONS;
|
|
169
174
|
const keyPrefix = config.keyPrefix ?? DEFAULT_KEY_PREFIX;
|
|
170
|
-
const messages: StorageMessages = {
|
|
171
|
-
...defaultStorageMessages({ limit: megabytes(config.maxBytes) }),
|
|
172
|
-
...config.messages,
|
|
173
|
-
};
|
|
175
|
+
const messages: StorageMessages = config.messages({ limit: megabytes(config.maxBytes) });
|
|
174
176
|
const writeDeps: StoreImageDeps = {
|
|
175
177
|
driver: config.driver,
|
|
176
178
|
pipeline: config.imagePipeline,
|