@hailer/mcp 1.3.21 → 1.3.32
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/.claude/skills/create-and-publish-app/SKILL.md +44 -56
- package/.claude/skills/hailer-app-builder/SKILL.md +414 -970
- package/.claude/skills/hailer-app-primitives/SKILL.md +742 -0
- package/.claude/skills/hailer-apps-pictures/SKILL.md +191 -87
- package/.claude/skills/hailer-design-patterns/SKILL.md +317 -0
- package/.claude/skills/hailer-design-system/SKILL.md +202 -149
- package/.claude/skills/hailer-docs-search/SKILL.md +75 -0
- package/.claude/skills/hailer-workflow-archetypes/SKILL.md +301 -0
- package/.claude/skills/insight-join-patterns/SKILL.md +313 -0
- package/.claude/skills/publish-hailer-app/SKILL.md +211 -0
- package/.claude/skills/sdk-activity-patterns/SKILL.md +257 -105
- package/.claude/skills/sdk-function-fields/SKILL.md +253 -492
- package/.claude/skills/sdk-insight-calculations/SKILL.md +364 -0
- package/.claude/skills/sdk-insight-queries/SKILL.md +192 -397
- package/.claude/skills/sdk-ws-config-skill/SKILL.md +265 -720
- package/.claude/skills/tool-response-verification/SKILL.md +143 -0
- package/CLAUDE.md +45 -19
- package/dist/bot/bot.d.ts.map +1 -1
- package/dist/bot/bot.js +21 -1
- package/dist/bot/bot.js.map +1 -1
- package/dist/bot/services/helper-prompt.d.ts +1 -1
- package/dist/bot/services/helper-prompt.d.ts.map +1 -1
- package/dist/bot/services/helper-prompt.js +4 -6
- package/dist/bot/services/helper-prompt.js.map +1 -1
- package/dist/bot/services/message-formatter.d.ts.map +1 -1
- package/dist/bot/services/message-formatter.js +2 -1
- package/dist/bot/services/message-formatter.js.map +1 -1
- package/dist/bot/services/system-prompt.d.ts.map +1 -1
- package/dist/bot/services/system-prompt.js +21 -34
- package/dist/bot/services/system-prompt.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -1
- package/dist/lib/object-id.d.ts +9 -0
- package/dist/lib/object-id.d.ts.map +1 -0
- package/dist/lib/object-id.js +15 -0
- package/dist/lib/object-id.js.map +1 -0
- package/dist/mcp/hailer-rpc.d.ts +3 -0
- package/dist/mcp/hailer-rpc.d.ts.map +1 -1
- package/dist/mcp/hailer-rpc.js +7 -0
- package/dist/mcp/hailer-rpc.js.map +1 -1
- package/dist/mcp/tool-profiles.d.ts +1 -0
- package/dist/mcp/tool-profiles.d.ts.map +1 -1
- package/dist/mcp/tool-profiles.js +21 -4
- package/dist/mcp/tool-profiles.js.map +1 -1
- package/dist/mcp/tools/aliases.d.ts.map +1 -1
- package/dist/mcp/tools/aliases.js +14 -6
- package/dist/mcp/tools/aliases.js.map +1 -1
- package/dist/mcp/tools/app-core.d.ts.map +1 -1
- package/dist/mcp/tools/app-core.js +6 -3
- package/dist/mcp/tools/app-core.js.map +1 -1
- package/dist/mcp/tools/app-marketplace.d.ts.map +1 -1
- package/dist/mcp/tools/app-marketplace.js +7 -2
- package/dist/mcp/tools/app-marketplace.js.map +1 -1
- package/dist/mcp/tools/docs.d.ts +20 -0
- package/dist/mcp/tools/docs.d.ts.map +1 -0
- package/dist/mcp/tools/docs.js +87 -0
- package/dist/mcp/tools/docs.js.map +1 -0
- package/dist/mcp/tools/function-fields.d.ts +17 -0
- package/dist/mcp/tools/function-fields.d.ts.map +1 -0
- package/dist/mcp/tools/function-fields.js +717 -0
- package/dist/mcp/tools/function-fields.js.map +1 -0
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +4 -0
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/workflow.d.ts +0 -1
- package/dist/mcp/tools/workflow.d.ts.map +1 -1
- package/dist/mcp/tools/workflow.js +1 -190
- package/dist/mcp/tools/workflow.js.map +1 -1
- package/dist/public-chat/graduate.d.ts +5 -4
- package/dist/public-chat/graduate.d.ts.map +1 -1
- package/dist/public-chat/graduate.js +226 -76
- package/dist/public-chat/graduate.js.map +1 -1
- package/dist/public-chat/handler.d.ts.map +1 -1
- package/dist/public-chat/handler.js +10 -2
- package/dist/public-chat/handler.js.map +1 -1
- package/dist/public-chat/index.d.ts.map +1 -1
- package/dist/public-chat/index.js +12 -1
- package/dist/public-chat/index.js.map +1 -1
- package/dist/public-chat/session-store.d.ts +22 -1
- package/dist/public-chat/session-store.d.ts.map +1 -1
- package/dist/public-chat/session-store.js +56 -2
- package/dist/public-chat/session-store.js.map +1 -1
- package/dist/public-chat/studio-prewarm.js +1 -1
- package/dist/public-chat/studio-prewarm.js.map +1 -1
- package/dist/public-chat/system-prompt.d.ts +1 -1
- package/dist/public-chat/system-prompt.d.ts.map +1 -1
- package/dist/public-chat/system-prompt.js +161 -83
- package/dist/public-chat/system-prompt.js.map +1 -1
- package/package.json +1 -1
- package/.claude/skills/hailer-project-protocol/SKILL.md +0 -398
|
@@ -1,80 +1,112 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hailer-apps-pictures
|
|
3
|
-
description:
|
|
4
|
-
version:
|
|
5
|
-
triggers:
|
|
3
|
+
description: Fetching and displaying images from Hailer in React apps — load when an app needs to show activity images, handle file uploads, build image carousels, or debug 403 image errors
|
|
4
|
+
version: 2.4.3
|
|
5
|
+
triggers:
|
|
6
|
+
- image
|
|
7
|
+
- picture
|
|
8
|
+
- photo
|
|
9
|
+
- file upload
|
|
10
|
+
- display image
|
|
11
|
+
- HailerImage
|
|
6
12
|
---
|
|
7
13
|
|
|
8
14
|
# Hailer Apps Image Handling
|
|
9
15
|
|
|
10
16
|
Pattern for fetching and displaying images from Hailer in React apps.
|
|
11
17
|
|
|
18
|
+
## Permissions / Auth Requirements
|
|
19
|
+
|
|
20
|
+
**Standard uploads (via the Hailer UI file modifier or `uploadFile` without `isPublic: true`) are auth-gated.**
|
|
21
|
+
|
|
22
|
+
A viewer must have **workflow read membership** on the workflow that owns the activity holding the file. Without it, image requests return 403. This applies even inside Hailer apps.
|
|
23
|
+
|
|
24
|
+
| Upload path | Public? | Required to view |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `uploadFile(file, name, { isPublic: true })` | Yes — via /public/ routes | Nothing |
|
|
27
|
+
| `uploadFile(file, name, { isPublic: false })` (or default) | No | Workflow read membership |
|
|
28
|
+
| Hailer UI file modifier (standard attachment) | No | Workflow read membership |
|
|
29
|
+
|
|
30
|
+
**`isPublic` must be set at upload time** — there is no API to flip a file from private to public after the fact.
|
|
31
|
+
|
|
32
|
+
**Practical design checklist:**
|
|
33
|
+
1. Will all app users have workflow read on the source workflow? → standard upload works.
|
|
34
|
+
2. Are some users members of the insight but NOT the source workflow? → grant them workflow read too.
|
|
35
|
+
3. Is the image truly public-facing (embed in email, external site)? → upload with `{ isPublic: true }` and use the `/public/image/{size}/{id}` route.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
12
39
|
## Image URL Format
|
|
13
40
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
41
|
+
Two route families — choose based on context:
|
|
42
|
+
|
|
43
|
+
| Route | When to use |
|
|
44
|
+
|-------|-------------|
|
|
45
|
+
| `/image/{size}/{id}` | Same-origin React app with active Hailer session (hlrkey cookie present) |
|
|
46
|
+
| `/public/image/{size}/{id}` | External embedding, email clients, cookieless cross-origin fetch, Node.js without a session — `isPublic: true` files only |
|
|
47
|
+
|
|
48
|
+
**The bare `/image/{size}/{id}` route returns 403 unauthenticated regardless of `isPublic`.** For any context where you cannot guarantee an active Hailer session cookie, use `/public/image/...`. (Live-verified 2026-06-10.)
|
|
18
49
|
|
|
19
50
|
### URL Patterns
|
|
20
51
|
|
|
21
|
-
**Correct formats
|
|
22
|
-
- `https://api.hailer.com/image/hires/{imageId}`
|
|
23
|
-
- `https://api.hailer.com/image/
|
|
52
|
+
**Correct formats:**
|
|
53
|
+
- `https://api.hailer.com/image/hires/{imageId}` — same-origin session; requires auth (hlrkey cookie)
|
|
54
|
+
- `https://api.hailer.com/image/thumb/{imageId}` — same-origin session thumbnail
|
|
55
|
+
- `https://api.hailer.com/public/image/hires/{imageId}` — external/cookieless; `isPublic: true` files only
|
|
56
|
+
- `https://api.hailer.com/public/image/thumb/{imageId}` — external/cookieless thumbnail
|
|
57
|
+
|
|
58
|
+
Valid size tokens: `square25, square50, square75, square100, square200, profile, smallface, bigface, thumb, gallerycover, fullhd, hires, lores, pdf, original`
|
|
24
59
|
|
|
25
60
|
**Wrong formats (don't use):**
|
|
26
|
-
- `https://app.hailer.com/api/files/{imageId}`
|
|
27
|
-
- `https://api.hailer.com/files/{imageId}`
|
|
28
|
-
- `https://api.hailer.com/image/{imageId}`
|
|
61
|
+
- `https://app.hailer.com/api/files/{imageId}` — CORS issues in browsers
|
|
62
|
+
- `https://api.hailer.com/files/{imageId}` — authentication required, may fail in apps
|
|
63
|
+
- `https://api.hailer.com/image/{imageId}` — missing size parameter
|
|
29
64
|
|
|
30
65
|
### Utility Function
|
|
31
66
|
|
|
32
67
|
```typescript
|
|
33
|
-
function getImageUrl(
|
|
34
|
-
|
|
68
|
+
function getImageUrl(
|
|
69
|
+
imageId: string,
|
|
70
|
+
size: 'hires' | 'thumb' = 'hires',
|
|
71
|
+
options: { external?: boolean } = {}
|
|
72
|
+
): string {
|
|
73
|
+
return options.external
|
|
74
|
+
? `https://api.hailer.com/public/image/${size}/${imageId}`
|
|
75
|
+
: `https://api.hailer.com/image/${size}/${imageId}`;
|
|
35
76
|
}
|
|
36
77
|
|
|
37
|
-
//
|
|
38
|
-
const url = getImageUrl('abc123', 'hires');
|
|
39
|
-
|
|
78
|
+
// Same-origin Hailer app (session cookie present):
|
|
79
|
+
const url = getImageUrl('abc123', 'hires');
|
|
80
|
+
// External embed / email / cookieless (isPublic: true files only):
|
|
81
|
+
const externalUrl = getImageUrl('abc123', 'hires', { external: true });
|
|
82
|
+
// Note: 'thumbnail' is NOT a valid size token — use 'thumb'
|
|
40
83
|
```
|
|
41
84
|
|
|
42
85
|
### Extracting imageId from Activity Fields
|
|
43
86
|
|
|
44
|
-
Image IDs typically come from file fields in activities. Extract them from the activity structure:
|
|
45
|
-
|
|
46
87
|
```typescript
|
|
47
|
-
// From a file field in an activity
|
|
48
|
-
const
|
|
49
|
-
const imageId =
|
|
88
|
+
// From a file-modifier field in an activity — value is a JSON-stringified array of file IDs
|
|
89
|
+
const ids: string[] = JSON.parse(activity.fields.image_file ?? '[]');
|
|
90
|
+
const imageId = ids[0];
|
|
50
91
|
|
|
51
92
|
// From insight data with embedded URLs
|
|
52
93
|
const imageUrl = item.mainProductImageUrl; // e.g., "https://api.hailer.com/image/hires/abc123"
|
|
53
|
-
const imageId = imageUrl.split('/').pop();
|
|
94
|
+
const imageId = imageUrl.split('/').pop();
|
|
54
95
|
|
|
55
96
|
// From JSON array of IDs
|
|
56
97
|
const imageIds = JSON.parse(item.allPictures); // ["id1", "id2", "id3"]
|
|
57
98
|
```
|
|
58
99
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Product/activity images typically come from insights with two fields:
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
interface WithImages {
|
|
65
|
-
mainProductImageUrl: string; // Full URL to main image
|
|
66
|
-
allPictures: string; // JSON string: '["id1", "id2", "id3"]'
|
|
67
|
-
}
|
|
68
|
-
```
|
|
100
|
+
---
|
|
69
101
|
|
|
70
102
|
## HailerImage Component
|
|
71
103
|
|
|
72
|
-
|
|
104
|
+
**iOS app version compatibility:** This component works as-is on iOS 2.2.4+ and all web. On older iOS app versions (pre-2.2.4), WKWebView blocks third-party auth cookies inside the `apps.hailer.com` iframe → `api.hailer.com` image requests fail.
|
|
73
105
|
|
|
74
106
|
```tsx
|
|
75
107
|
// components/HailerImage.tsx
|
|
76
108
|
import { Image, ImageProps, Icon } from '@chakra-ui/react';
|
|
77
|
-
import {
|
|
109
|
+
import { HailerImage as HailerImageIcon } from '../hailer/theme/icons/HailerImage'; // aliased — this component is also named HailerImage. No barrel index in the icons dir; import the specific file.
|
|
78
110
|
import { useState, useEffect } from 'react';
|
|
79
111
|
import useHailer from '../hailer/use-hailer';
|
|
80
112
|
|
|
@@ -89,23 +121,15 @@ export const HailerImage = ({
|
|
|
89
121
|
alt = 'Image',
|
|
90
122
|
...imageProps
|
|
91
123
|
}: HailerImageProps) => {
|
|
92
|
-
const { inside, hailer } = useHailer();
|
|
124
|
+
const { inside, hailer } = useHailer();
|
|
93
125
|
const [blobUrl, setBlobUrl] = useState<string | null>(null);
|
|
94
126
|
const [isLoading, setIsLoading] = useState(true);
|
|
95
127
|
const [isError, setIsError] = useState(false);
|
|
96
128
|
|
|
97
129
|
useEffect(() => {
|
|
98
130
|
const loadImage = async () => {
|
|
99
|
-
if (!hailerImageUrl) {
|
|
100
|
-
|
|
101
|
-
setIsLoading(false);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (!hailer?.ready) { // NOTE: verify .ready exists on SDK type — may need .activity or similar
|
|
106
|
-
setIsLoading(true);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
131
|
+
if (!hailerImageUrl) { setIsError(true); setIsLoading(false); return; }
|
|
132
|
+
if (!inside) { setIsLoading(true); return; }
|
|
109
133
|
|
|
110
134
|
try {
|
|
111
135
|
const isHailerApiUrl = hailerImageUrl.includes('api.hailer.com');
|
|
@@ -113,7 +137,6 @@ export const HailerImage = ({
|
|
|
113
137
|
|
|
114
138
|
if (isLocalDevelopment && isHailerApiUrl) {
|
|
115
139
|
// CORS blocks api.hailer.com in localhost
|
|
116
|
-
console.log('Local development: Hailer images blocked by CORS. Will work in production.');
|
|
117
140
|
setIsError(true);
|
|
118
141
|
setIsLoading(false);
|
|
119
142
|
return;
|
|
@@ -122,7 +145,6 @@ export const HailerImage = ({
|
|
|
122
145
|
setBlobUrl(hailerImageUrl);
|
|
123
146
|
setIsError(false);
|
|
124
147
|
} catch (error) {
|
|
125
|
-
console.error('Error loading image from Hailer:', error);
|
|
126
148
|
setIsError(true);
|
|
127
149
|
} finally {
|
|
128
150
|
setIsLoading(false);
|
|
@@ -132,15 +154,13 @@ export const HailerImage = ({
|
|
|
132
154
|
loadImage();
|
|
133
155
|
|
|
134
156
|
return () => {
|
|
135
|
-
if (blobUrl)
|
|
136
|
-
URL.revokeObjectURL(blobUrl);
|
|
137
|
-
}
|
|
157
|
+
if (blobUrl) URL.revokeObjectURL(blobUrl);
|
|
138
158
|
};
|
|
139
|
-
}, [hailerImageUrl,
|
|
159
|
+
}, [hailerImageUrl, inside]);
|
|
140
160
|
|
|
141
161
|
if (isLoading || isError || !blobUrl) {
|
|
142
162
|
return fallbackIcon ? (
|
|
143
|
-
<Icon as={
|
|
163
|
+
<Icon as={HailerImageIcon} color="gray.200" {...imageProps} />
|
|
144
164
|
) : null;
|
|
145
165
|
}
|
|
146
166
|
|
|
@@ -171,26 +191,22 @@ export const HailerImage = ({
|
|
|
171
191
|
|
|
172
192
|
## Usage: Image Carousel
|
|
173
193
|
|
|
174
|
-
For multiple images stored as JSON array of IDs:
|
|
194
|
+
For multiple images stored as a JSON array of IDs:
|
|
175
195
|
|
|
176
196
|
```tsx
|
|
177
197
|
const ProductImageCarousel = ({ allPictures, mainImageUrl }: Props) => {
|
|
178
198
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
179
199
|
|
|
180
|
-
// Parse JSON string to array of IDs
|
|
181
200
|
const imageIds = useMemo(() => {
|
|
182
201
|
if (!allPictures) return [];
|
|
183
202
|
try {
|
|
184
203
|
const parsed = JSON.parse(allPictures);
|
|
185
204
|
return Array.isArray(parsed) ? parsed : [];
|
|
186
|
-
} catch {
|
|
187
|
-
return [];
|
|
188
|
-
}
|
|
205
|
+
} catch { return []; }
|
|
189
206
|
}, [allPictures]);
|
|
190
207
|
|
|
191
208
|
const hasMultiple = imageIds.length > 1;
|
|
192
209
|
|
|
193
|
-
// Construct URL from ID
|
|
194
210
|
const getCurrentUrl = () => {
|
|
195
211
|
if (imageIds.length === 0) return mainImageUrl || '';
|
|
196
212
|
const id = imageIds[currentIndex];
|
|
@@ -206,25 +222,21 @@ const ProductImageCarousel = ({ allPictures, mainImageUrl }: Props) => {
|
|
|
206
222
|
|
|
207
223
|
{hasMultiple && (
|
|
208
224
|
<>
|
|
225
|
+
{/* Use the Hailer design-system chevron — never external icon libs.
|
|
226
|
+
Apply a rotate() transform if it doesn't already point left/right. */}
|
|
209
227
|
<IconButton
|
|
210
|
-
|
|
228
|
+
aria-label="Previous image"
|
|
229
|
+
icon={<HailerChevron />}
|
|
211
230
|
onClick={goPrev}
|
|
212
|
-
position="absolute"
|
|
213
|
-
left={2}
|
|
214
|
-
top="50%"
|
|
215
|
-
transform="translateY(-50%)"
|
|
231
|
+
position="absolute" left={2} top="50%" transform="translateY(-50%)"
|
|
216
232
|
/>
|
|
217
233
|
<IconButton
|
|
218
|
-
|
|
234
|
+
aria-label="Next image"
|
|
235
|
+
icon={<HailerChevron />}
|
|
219
236
|
onClick={goNext}
|
|
220
|
-
position="absolute"
|
|
221
|
-
right={2}
|
|
222
|
-
top="50%"
|
|
223
|
-
transform="translateY(-50%)"
|
|
237
|
+
position="absolute" right={2} top="50%" transform="translateY(-50%)"
|
|
224
238
|
/>
|
|
225
|
-
<Text textAlign="center">
|
|
226
|
-
{currentIndex + 1} / {imageIds.length}
|
|
227
|
-
</Text>
|
|
239
|
+
<Text textAlign="center">{currentIndex + 1} / {imageIds.length}</Text>
|
|
228
240
|
</>
|
|
229
241
|
)}
|
|
230
242
|
</Box>
|
|
@@ -232,38 +244,130 @@ const ProductImageCarousel = ({ allPictures, mainImageUrl }: Props) => {
|
|
|
232
244
|
};
|
|
233
245
|
```
|
|
234
246
|
|
|
235
|
-
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## File Upload Pattern
|
|
250
|
+
|
|
251
|
+
For app-sdk 2.4.x+, use `hailer.file.upload()` — `hailer.ui.files.uploadFile()` is marked `@deprecated` in 2.4.x. Both have the same signature and return a `fileId` string.
|
|
236
252
|
|
|
237
253
|
```typescript
|
|
238
|
-
//
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
254
|
+
// Preferred for app-sdk 2.4.x+
|
|
255
|
+
const fileId = await hailer.file.upload(file, fileName, { isPublic: false });
|
|
256
|
+
const publicFileId = await hailer.file.upload(file, fileName, { isPublic: true });
|
|
257
|
+
|
|
258
|
+
// Legacy (still works but deprecated in 2.4.x)
|
|
259
|
+
const fileId = await hailer.ui.files.uploadFile(file, fileName, { isPublic: false });
|
|
260
|
+
|
|
261
|
+
// Pass fileIds when creating an activity
|
|
262
|
+
await hailer.activity.create('workflowId', [
|
|
263
|
+
{ name: 'Activity', fields: { /* ... */ } }
|
|
264
|
+
], { fileIds: [fileId] });
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**`isPublic: true` must be set at upload time.** There is no API to promote a file from private to public after creation.
|
|
268
|
+
|
|
269
|
+
**`fileIds` only bind at `activity.create`.** There is no `attachFiles`/`addFiles` method on app-sdk. Defer activity creation until files are ready.
|
|
270
|
+
|
|
271
|
+
### Canonical upload-during-form-fill pattern
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
const uploadPromiseRef = useRef<Promise<string[]> | null>(null);
|
|
275
|
+
|
|
276
|
+
// When user selects files — start upload immediately
|
|
277
|
+
const handleFilesSelected = (files: File[]) => {
|
|
278
|
+
uploadPromiseRef.current = Promise.all(
|
|
279
|
+
files.map(f => hailer.file.upload(f, f.name, {}))
|
|
280
|
+
);
|
|
247
281
|
};
|
|
282
|
+
|
|
283
|
+
// On Save — await the in-flight promise, then create atomically
|
|
284
|
+
const handleSave = async () => {
|
|
285
|
+
const fileIds = uploadPromiseRef.current ? await uploadPromiseRef.current : [];
|
|
286
|
+
await hailer.activity.create(workflowId, [{ name, fields }], { fileIds });
|
|
287
|
+
};
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Server-to-Server Image Fetching
|
|
293
|
+
|
|
294
|
+
For **public files** (`isPublic: true`), use `/public/image/{size}/{id}` for cookieless or external fetches. The bare `/image/{size}/{id}` route requires the `hlrkey` cookie — it returns 403 unauthenticated regardless of `isPublic` (live-verified 2026-06-10).
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
# Cookieless/external (isPublic files only):
|
|
298
|
+
https://api.hailer.com/public/image/{size}/{imageId}
|
|
299
|
+
|
|
300
|
+
# Same-origin / hlrkey-authenticated:
|
|
301
|
+
https://api.hailer.com/image/{size}/{imageId}
|
|
248
302
|
```
|
|
249
303
|
|
|
304
|
+
Diagnostic: 404 on `/public/` = file not isPublic; 403 on bare route = missing auth.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
250
308
|
## Key Points
|
|
251
309
|
|
|
252
310
|
| Aspect | Details |
|
|
253
311
|
|--------|---------|
|
|
254
|
-
| **URL Format** |
|
|
255
|
-
| **
|
|
256
|
-
| **
|
|
312
|
+
| **URL Format** | Same-origin session: `/image/{size}/{id}`; external/cookieless: `/public/image/{size}/{id}` |
|
|
313
|
+
| **Auth — standard files** | Requires workflow read membership; bare `/image/` route only |
|
|
314
|
+
| **Auth — public files** | `isPublic: true` at upload time; use `/public/image/` for cookieless access |
|
|
315
|
+
| **CORS** | Blocked in localhost browser, works in production and from Node.js |
|
|
316
|
+
| **Fallback** | Use the design-system `HailerImage` icon (import aliased to `HailerImageIcon`) |
|
|
257
317
|
| **Performance** | Use `loading="lazy"` for catalog images |
|
|
258
318
|
| **Multiple Images** | Store as JSON array of IDs, construct URLs on demand |
|
|
259
319
|
| **Memory** | Revoke blob URLs on component cleanup |
|
|
260
320
|
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Diagnostic Overlay Pattern (iOS / Debug Builds)
|
|
324
|
+
|
|
325
|
+
Use when diagnosing image load failures on iOS — when console access isn't available.
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// At module top — NOT inside the component
|
|
329
|
+
const imageLoadStats = {
|
|
330
|
+
directLoadSuccess: 0,
|
|
331
|
+
S0_filget: 0,
|
|
332
|
+
S1_rpc: 0,
|
|
333
|
+
error: 0,
|
|
334
|
+
};
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
// components/DiagnosticOverlay.tsx
|
|
339
|
+
import { useEffect, useState } from 'react';
|
|
340
|
+
import { imageLoadStats } from '../components/YourImageComponent';
|
|
341
|
+
|
|
342
|
+
export const DiagnosticOverlay = () => {
|
|
343
|
+
const [snap, setSnap] = useState({ ...imageLoadStats });
|
|
344
|
+
|
|
345
|
+
useEffect(() => {
|
|
346
|
+
const id = setInterval(() => setSnap({ ...imageLoadStats }), 500);
|
|
347
|
+
return () => clearInterval(id);
|
|
348
|
+
}, []);
|
|
349
|
+
|
|
350
|
+
return (
|
|
351
|
+
<div style={{
|
|
352
|
+
position: 'fixed', bottom: 0, left: 0, right: 0,
|
|
353
|
+
background: 'rgba(0,0,0,0.75)', color: '#fff',
|
|
354
|
+
fontSize: 11, padding: '4px 8px', zIndex: 9999,
|
|
355
|
+
}}>
|
|
356
|
+
direct:{snap.directLoadSuccess} S0:{snap.S0_filget} S1:{snap.S1_rpc} err:{snap.error}
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
359
|
+
};
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Mount once in `App.tsx`. Bump the manifest version for each diagnostic build; strip the overlay after the fix is verified.
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
261
366
|
## Dependencies
|
|
262
367
|
|
|
263
368
|
```json
|
|
264
369
|
{
|
|
265
370
|
"@chakra-ui/react": "^2.x",
|
|
266
|
-
"react-icons": "^5.x",
|
|
267
371
|
"@hailer/app-sdk": "^2.x"
|
|
268
372
|
}
|
|
269
373
|
```
|