@proteos/sdk 0.26.3 → 0.28.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/dist/{chunk-4TXXJ47A.cjs → chunk-3SWQRVZX.cjs} +10 -6
- package/dist/chunk-3SWQRVZX.cjs.map +1 -0
- package/dist/{chunk-YMRMKSZL.js → chunk-Y5RY27JB.js} +10 -6
- package/dist/chunk-Y5RY27JB.js.map +1 -0
- package/dist/index.cjs +174 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +174 -35
- package/dist/index.d.ts +174 -35
- package/dist/index.js +92 -5
- package/dist/index.js.map +1 -1
- package/dist/meta/index.cjs +53 -53
- package/dist/meta/index.d.cts +1 -1
- package/dist/meta/index.d.ts +1 -1
- package/dist/meta/index.js +1 -1
- package/dist/{types-B0LqBATf.d.cts → types-BguERkVI.d.cts} +13 -1
- package/dist/{types-B0LqBATf.d.ts → types-BguERkVI.d.ts} +13 -1
- package/package.json +1 -1
- package/src/conversation/index.ts +133 -6
- package/src/conversation/types.ts +174 -26
- package/src/index.ts +23 -4
- package/src/meta/layout/control-registry.json +3 -2
- package/src/meta/layout/elements.ts +7 -0
- package/src/storage/files.ts +13 -5
- package/src/types/common.ts +6 -1
- package/dist/chunk-4TXXJ47A.cjs.map +0 -1
- package/dist/chunk-YMRMKSZL.js.map +0 -1
|
@@ -74,12 +74,15 @@ export interface UserRef {
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Inline snapshot of a person on a message/reaction — the integration-side
|
|
77
|
-
* identity plus, when resolved
|
|
77
|
+
* identity plus, when resolved, the platform user and the contact id.
|
|
78
|
+
* (Renamed from ParticipantRef in the contacts model; wire keys unchanged.)
|
|
78
79
|
*/
|
|
79
|
-
export interface
|
|
80
|
+
export interface ContactRef {
|
|
80
81
|
external_id?: string
|
|
81
82
|
name: string
|
|
82
83
|
email?: string
|
|
84
|
+
/** The resolved person; absent on pre-contacts snapshots and wire fallbacks. */
|
|
85
|
+
contact_id?: string
|
|
83
86
|
/** Resolved platform identity (email match); absent when unresolved. */
|
|
84
87
|
platform_user?: UserRef
|
|
85
88
|
}
|
|
@@ -90,7 +93,7 @@ export interface ParticipantRef {
|
|
|
90
93
|
* renders "who it's with" as the non-self subset (1 → single avatar, ≥2 →
|
|
91
94
|
* stacked).
|
|
92
95
|
*/
|
|
93
|
-
export interface ConversationParticipant extends
|
|
96
|
+
export interface ConversationParticipant extends ContactRef {
|
|
94
97
|
is_self: boolean
|
|
95
98
|
}
|
|
96
99
|
|
|
@@ -101,7 +104,7 @@ export type RecipientRole = 'to' | 'cc' | 'bcc'
|
|
|
101
104
|
* One addressee of a message with the role it was addressed under. Bcc is only
|
|
102
105
|
* present on messages the platform itself sent (inbound bcc is SMTP-stripped).
|
|
103
106
|
*/
|
|
104
|
-
export interface MessageRecipient extends
|
|
107
|
+
export interface MessageRecipient extends ContactRef {
|
|
105
108
|
role: RecipientRole
|
|
106
109
|
}
|
|
107
110
|
|
|
@@ -164,7 +167,7 @@ export interface Connection {
|
|
|
164
167
|
scope: ConnectionScope
|
|
165
168
|
/** Set only when scope=user (the connecting user); absent for org-wide. */
|
|
166
169
|
owner?: UserRef
|
|
167
|
-
|
|
170
|
+
external_account_id: string
|
|
168
171
|
credentials: ConnectionCredentials
|
|
169
172
|
settings: Record<string, unknown>
|
|
170
173
|
status: ConnectionStatus
|
|
@@ -232,7 +235,7 @@ export interface Conversation {
|
|
|
232
235
|
latest_message?: MessagePreview
|
|
233
236
|
messages_total?: number
|
|
234
237
|
/** Up to 3 most recent distinct senders excluding the root sender. */
|
|
235
|
-
last_repliers?:
|
|
238
|
+
last_repliers?: ContactRef[]
|
|
236
239
|
/** Requesting user's read marker; absent = never opened. */
|
|
237
240
|
last_read_at?: string
|
|
238
241
|
/**
|
|
@@ -252,7 +255,7 @@ export interface Conversation {
|
|
|
252
255
|
* ~500 chars of text, direction/status for outbound identity.
|
|
253
256
|
*/
|
|
254
257
|
export interface MessagePreview {
|
|
255
|
-
sender:
|
|
258
|
+
sender: ContactRef
|
|
256
259
|
text: string
|
|
257
260
|
direction: MessageDirection
|
|
258
261
|
status: MessageStatus
|
|
@@ -290,7 +293,7 @@ export interface Attachment {
|
|
|
290
293
|
export interface Reaction {
|
|
291
294
|
emoji: string
|
|
292
295
|
count: number
|
|
293
|
-
participants:
|
|
296
|
+
participants: ContactRef[]
|
|
294
297
|
}
|
|
295
298
|
|
|
296
299
|
export interface Message {
|
|
@@ -301,7 +304,7 @@ export interface Message {
|
|
|
301
304
|
channel: Channel
|
|
302
305
|
direction: MessageDirection
|
|
303
306
|
external_message_id: string
|
|
304
|
-
sender:
|
|
307
|
+
sender: ContactRef
|
|
305
308
|
/** Who the message was addressed to (To/Cc/Bcc), each tagged with its role. */
|
|
306
309
|
recipients?: MessageRecipient[]
|
|
307
310
|
content: ContentBlock[]
|
|
@@ -533,7 +536,8 @@ export interface ListConversationsQuery extends PaginationQuery {
|
|
|
533
536
|
*/
|
|
534
537
|
parent_conversation_id?: string
|
|
535
538
|
/** Threads whose roster contains this external party — the person stream. */
|
|
536
|
-
|
|
539
|
+
/** Contact stream: threads whose roster contains the resolved person. */
|
|
540
|
+
contact_id?: string
|
|
537
541
|
/** Opt into the root/latest message projection on each row. */
|
|
538
542
|
include?: 'messages_summary'
|
|
539
543
|
}
|
|
@@ -562,41 +566,141 @@ export interface ListAgentListenersQuery extends PaginationQuery {
|
|
|
562
566
|
is_enabled?: boolean
|
|
563
567
|
}
|
|
564
568
|
|
|
565
|
-
/**
|
|
566
|
-
|
|
567
|
-
|
|
569
|
+
/**
|
|
570
|
+
* Discriminates a send target: a person's contact address or a venue. The
|
|
571
|
+
* server also accepts the legacy 'participant' value as an alias for
|
|
572
|
+
* 'contact-address'.
|
|
573
|
+
*/
|
|
574
|
+
export type RecipientKind = 'contact-address' | 'room'
|
|
575
|
+
export type ContactAddressKind =
|
|
576
|
+
| 'email'
|
|
577
|
+
| 'phone'
|
|
578
|
+
| 'slack'
|
|
579
|
+
| 'linkedin'
|
|
580
|
+
| 'telegram'
|
|
581
|
+
| 'instagram'
|
|
582
|
+
| 'messenger'
|
|
583
|
+
| 'x'
|
|
584
|
+
| 'whatsapp'
|
|
585
|
+
export type ContactAddressSource = 'sync' | 'ingest' | 'manual' | 'merge'
|
|
586
|
+
export type ContactSource = 'sync' | 'ingest' | 'manual' | 'merge'
|
|
587
|
+
export type ContactStatus = 'active' | 'merged' | 'archived' | 'erased'
|
|
588
|
+
export type ConsentStatus = 'unknown' | 'opted_in' | 'opted_out'
|
|
589
|
+
export type PermissionEventType = 'opt_in' | 'opt_out' | 'block' | 'unblock'
|
|
590
|
+
export type PermissionEventSource = 'manual' | 'import' | 'link_click' | 'reply' | 'api' | 'system'
|
|
591
|
+
export type MergeProposalStatus = 'proposed' | 'approved' | 'rejected' | 'superseded'
|
|
592
|
+
export type ErasureRequestStatus = 'requested' | 'completed'
|
|
568
593
|
export type RoomSource = 'sync' | 'ingest'
|
|
569
594
|
|
|
570
595
|
/**
|
|
571
596
|
* Send-time addressing VO: who/where an originated message goes. Points at a
|
|
572
|
-
*
|
|
597
|
+
* ContactAddress or a Room by kind + external id (never a stored entity itself).
|
|
573
598
|
*/
|
|
574
599
|
export interface SendRecipient {
|
|
575
600
|
kind: RecipientKind
|
|
576
|
-
/** The connector-side id: a room's (Slack channel id) or a
|
|
601
|
+
/** The connector-side id: a room's (Slack channel id) or a contact address's wire identifier (Slack user id, email address). */
|
|
577
602
|
external_id: string
|
|
578
603
|
}
|
|
579
604
|
|
|
580
|
-
/**
|
|
581
|
-
export interface
|
|
605
|
+
/** The actual PERSON an org communicates with — org-scoped, channel-agnostic. */
|
|
606
|
+
export interface Contact {
|
|
582
607
|
id: string
|
|
583
608
|
org_id: string
|
|
584
|
-
connection_id: string
|
|
585
|
-
/** Slack user id / email address — the id to DM or email. */
|
|
586
|
-
external_id: string
|
|
587
609
|
name: string
|
|
588
|
-
|
|
589
|
-
/**
|
|
610
|
+
status: ContactStatus
|
|
611
|
+
/** Person-level suppression — only block/unblock events move it. */
|
|
612
|
+
is_blocked: boolean
|
|
613
|
+
has_legal_hold: boolean
|
|
614
|
+
consent_status: ConsentStatus
|
|
615
|
+
consent_updated_at?: string
|
|
616
|
+
/** Resolved platform identity when the person is a colleague. */
|
|
590
617
|
platform_user?: UserRef
|
|
618
|
+
/** Merge tombstone redirect (set when status is 'merged'). */
|
|
619
|
+
merged_into_contact_id?: string
|
|
620
|
+
source: ContactSource
|
|
621
|
+
has_manual_edits: boolean
|
|
622
|
+
/** The contact's reachable endpoints, embedded on reads. */
|
|
623
|
+
addresses?: ContactAddress[]
|
|
624
|
+
created_at: string
|
|
625
|
+
created_by: UserRef
|
|
626
|
+
updated_at: string
|
|
627
|
+
updated_by: UserRef
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** ONE canonical digital endpoint to reach a contact — the dedup backbone. */
|
|
631
|
+
export interface ContactAddress {
|
|
632
|
+
id: string
|
|
633
|
+
org_id: string
|
|
634
|
+
contact_id: string
|
|
635
|
+
kind: ContactAddressKind
|
|
636
|
+
/** Provider-tenant qualifier (Slack workspace id); empty for global kinds. */
|
|
637
|
+
scope?: string
|
|
638
|
+
/** Canonical scalar (lowercased email, E.164 phone) — the lookup key. */
|
|
639
|
+
value: string
|
|
640
|
+
/** The identifier as observed on the wire (WhatsApp JID, original casing). */
|
|
641
|
+
raw_value?: string
|
|
642
|
+
/** Per-channel display name. */
|
|
643
|
+
name: string
|
|
644
|
+
consent_status: ConsentStatus
|
|
645
|
+
consent_updated_at?: string
|
|
646
|
+
is_blocked: boolean
|
|
647
|
+
source: ContactAddressSource
|
|
591
648
|
metadata: Record<string, unknown>
|
|
592
|
-
source: ParticipantSource
|
|
593
|
-
last_seen_at: string
|
|
594
649
|
created_at: string
|
|
595
650
|
created_by: UserRef
|
|
596
651
|
updated_at: string
|
|
597
652
|
updated_by: UserRef
|
|
598
653
|
}
|
|
599
654
|
|
|
655
|
+
/** One entry in the append-only permission ledger (consent + suppression). */
|
|
656
|
+
export interface ContactPermissionEvent {
|
|
657
|
+
id: string
|
|
658
|
+
org_id: string
|
|
659
|
+
contact_id: string
|
|
660
|
+
/** Scopes the event to one address; absent/empty = contact-level. */
|
|
661
|
+
contact_address_id?: string
|
|
662
|
+
event: PermissionEventType
|
|
663
|
+
basis?: string
|
|
664
|
+
source: PermissionEventSource
|
|
665
|
+
occurred_at: string
|
|
666
|
+
note?: string
|
|
667
|
+
evidence?: Record<string, unknown>
|
|
668
|
+
created_at: string
|
|
669
|
+
created_by: UserRef
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** One candidate duplicate pair awaiting review. */
|
|
673
|
+
export interface ContactMergeProposal {
|
|
674
|
+
id: string
|
|
675
|
+
org_id: string
|
|
676
|
+
contact_a_id: string
|
|
677
|
+
contact_b_id: string
|
|
678
|
+
suggested_winner_id?: string
|
|
679
|
+
confidence?: number
|
|
680
|
+
evidence?: Record<string, unknown>
|
|
681
|
+
verdict?: Record<string, unknown>
|
|
682
|
+
status: MergeProposalStatus
|
|
683
|
+
resolved_by?: UserRef
|
|
684
|
+
resolved_at?: string
|
|
685
|
+
created_at: string
|
|
686
|
+
created_by: UserRef
|
|
687
|
+
updated_at: string
|
|
688
|
+
updated_by: UserRef
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** A GDPR Art. 17 erasure request (requested → completed). */
|
|
692
|
+
export interface ContactErasureRequest {
|
|
693
|
+
id: string
|
|
694
|
+
org_id: string
|
|
695
|
+
contact_id: string
|
|
696
|
+
identifier_hashes: Array<{ kind: ContactAddressKind; hmac: string }>
|
|
697
|
+
status: ErasureRequestStatus
|
|
698
|
+
completed_at?: string
|
|
699
|
+
stats?: Record<string, unknown>
|
|
700
|
+
created_at: string
|
|
701
|
+
created_by: UserRef
|
|
702
|
+
}
|
|
703
|
+
|
|
600
704
|
/** An addressable VENUE on a connection (Slack channel; later meeting rooms). */
|
|
601
705
|
export interface Room {
|
|
602
706
|
id: string
|
|
@@ -615,9 +719,53 @@ export interface Room {
|
|
|
615
719
|
updated_by: UserRef
|
|
616
720
|
}
|
|
617
721
|
|
|
618
|
-
export interface
|
|
619
|
-
/** Free-text needle matched against display name +
|
|
722
|
+
export interface ListContactAddressesQuery extends PaginationQuery {
|
|
723
|
+
/** Free-text needle matched against display name + canonical value. */
|
|
724
|
+
q?: string
|
|
725
|
+
kinds?: ContactAddressKind[]
|
|
726
|
+
scope?: string
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export interface ListContactsQuery extends PaginationQuery {
|
|
730
|
+
/** Free-text needle matched against contact name + address values. */
|
|
620
731
|
q?: string
|
|
732
|
+
status?: ContactStatus
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export interface UpdateContactRequest {
|
|
736
|
+
name?: string
|
|
737
|
+
status?: 'active' | 'archived'
|
|
738
|
+
has_legal_hold?: boolean
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export interface AttachContactAddressRequest {
|
|
742
|
+
kind: ContactAddressKind
|
|
743
|
+
scope?: string
|
|
744
|
+
value: string
|
|
745
|
+
name?: string
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export interface MergeContactsRequest {
|
|
749
|
+
source_contact_id: string
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export interface RecordPermissionEventRequest {
|
|
753
|
+
contact_address_id?: string
|
|
754
|
+
event: PermissionEventType
|
|
755
|
+
basis?: string
|
|
756
|
+
source?: PermissionEventSource
|
|
757
|
+
occurred_at?: string
|
|
758
|
+
note?: string
|
|
759
|
+
evidence?: Record<string, unknown>
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export interface BlockContactRequest {
|
|
763
|
+
contact_address_id?: string
|
|
764
|
+
note?: string
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
export interface ListContactMergeProposalsQuery extends PaginationQuery {
|
|
768
|
+
status?: MergeProposalStatus
|
|
621
769
|
}
|
|
622
770
|
|
|
623
771
|
export interface ListRoomsQuery extends PaginationQuery {
|
package/src/index.ts
CHANGED
|
@@ -196,6 +196,7 @@ export type {
|
|
|
196
196
|
ContentBlock as MessageContentBlock,
|
|
197
197
|
Conversation,
|
|
198
198
|
ConversationParticipant,
|
|
199
|
+
ContactService,
|
|
199
200
|
ConversationService,
|
|
200
201
|
ConversationStatus,
|
|
201
202
|
CreateAgentListenerRequest,
|
|
@@ -210,7 +211,9 @@ export type {
|
|
|
210
211
|
ListConversationsQuery,
|
|
211
212
|
ListGlossaryTermsQuery,
|
|
212
213
|
ListMessagesQuery,
|
|
213
|
-
|
|
214
|
+
ListContactAddressesQuery,
|
|
215
|
+
ListContactMergeProposalsQuery,
|
|
216
|
+
ListContactsQuery,
|
|
214
217
|
// Conversation-local page envelope ({meta, data}) — distinct from the
|
|
215
218
|
// PageIterator-based ListResult used by the other modules.
|
|
216
219
|
ListResponse,
|
|
@@ -222,9 +225,25 @@ export type {
|
|
|
222
225
|
MessageRecipient,
|
|
223
226
|
MessageService,
|
|
224
227
|
MessageStatus,
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
+
AttachContactAddressRequest,
|
|
229
|
+
BlockContactRequest,
|
|
230
|
+
ConsentStatus,
|
|
231
|
+
Contact,
|
|
232
|
+
ContactAddress,
|
|
233
|
+
ContactAddressKind,
|
|
234
|
+
ContactAddressSource,
|
|
235
|
+
ContactErasureRequest,
|
|
236
|
+
ContactMergeProposal,
|
|
237
|
+
ContactRef,
|
|
238
|
+
ContactSource,
|
|
239
|
+
ContactStatus,
|
|
240
|
+
ErasureRequestStatus,
|
|
241
|
+
MergeContactsRequest,
|
|
242
|
+
MergeProposalStatus,
|
|
243
|
+
PermissionEventSource,
|
|
244
|
+
PermissionEventType,
|
|
245
|
+
RecordPermissionEventRequest,
|
|
246
|
+
UpdateContactRequest,
|
|
228
247
|
Reaction,
|
|
229
248
|
ReactionCapability,
|
|
230
249
|
ReactionOption,
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"user-picker",
|
|
24
24
|
"currency",
|
|
25
25
|
"knowledge-text",
|
|
26
|
-
"file"
|
|
26
|
+
"file",
|
|
27
|
+
"file-viewer"
|
|
27
28
|
],
|
|
28
29
|
"controls": {
|
|
29
30
|
"string": {
|
|
@@ -65,6 +66,6 @@
|
|
|
65
66
|
"user": { "primary": "user-picker", "compatible": ["user-picker"] },
|
|
66
67
|
"currency": { "primary": "currency", "compatible": ["currency"] },
|
|
67
68
|
"knowledge-text": { "primary": "knowledge-text", "compatible": ["knowledge-text"] },
|
|
68
|
-
"file": { "primary": "file", "compatible": ["file"] }
|
|
69
|
+
"file": { "primary": "file", "compatible": ["file", "file-viewer"] }
|
|
69
70
|
}
|
|
70
71
|
}
|
|
@@ -88,6 +88,12 @@ export type RelatedListElement = CommonProps & {
|
|
|
88
88
|
related_entity_slug: string
|
|
89
89
|
via_attribute: string
|
|
90
90
|
list_slug?: string
|
|
91
|
+
/**
|
|
92
|
+
* When absent or true, the list enters row-edit mode together with the
|
|
93
|
+
* host page's edit mode. False keeps the list independent — rows are only
|
|
94
|
+
* editable through the list's own "Edit records" toggle.
|
|
95
|
+
*/
|
|
96
|
+
follows_parent_edit_mode?: boolean
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
export type ComponentElement = CommonProps & {
|
|
@@ -182,6 +188,7 @@ export const LayoutElementSchema: z.ZodType<LayoutElement> = z.lazy(() =>
|
|
|
182
188
|
related_entity_slug: z.string().min(1),
|
|
183
189
|
via_attribute: z.string().min(1),
|
|
184
190
|
list_slug: z.string().min(1).optional(),
|
|
191
|
+
follows_parent_edit_mode: z.boolean().optional(),
|
|
185
192
|
}),
|
|
186
193
|
z.object({
|
|
187
194
|
type: z.literal('component'),
|
package/src/storage/files.ts
CHANGED
|
@@ -40,11 +40,15 @@ export interface FileService {
|
|
|
40
40
|
create(metadata: CreateFileMetadata): Promise<StorageFile>
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* Updates a file's metadata (name, `public_access`)
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* Updates a file's metadata (name, `public_access`) and, when `file` is
|
|
44
|
+
* given, replaces the bytes IN PLACE — a new version under the SAME id, so
|
|
45
|
+
* every `FileRef { id }` pointing at it now resolves to the new content.
|
|
46
|
+
* Only the metadata fields present are changed. Setting
|
|
47
|
+
* `public_access: ["read"]` exposes the file on the UNAUTHENTICATED public
|
|
48
|
+
* download route; `[]` revokes it. Replacing a locked file is rejected by
|
|
49
|
+
* the storage-service.
|
|
46
50
|
*/
|
|
47
|
-
update(id: string, metadata: UpdateFileMetadata): Promise<StorageFile>
|
|
51
|
+
update(id: string, metadata: UpdateFileMetadata, file?: File): Promise<StorageFile>
|
|
48
52
|
|
|
49
53
|
/**
|
|
50
54
|
* Mints a short-lived upload URL for a file. The bytes are PUT directly to this URL
|
|
@@ -157,12 +161,16 @@ export class FileServiceImpl implements FileService {
|
|
|
157
161
|
return response.data
|
|
158
162
|
}
|
|
159
163
|
|
|
160
|
-
async update(id: string, metadata: UpdateFileMetadata): Promise<StorageFile> {
|
|
164
|
+
async update(id: string, metadata: UpdateFileMetadata, file?: File): Promise<StorageFile> {
|
|
165
|
+
// Part order is load-bearing: the storage-service reads `metadata` first
|
|
166
|
+
// and stops consuming parts at `file` — metadata appended after the file
|
|
167
|
+
// part would be silently dropped.
|
|
161
168
|
const formData = new FormData()
|
|
162
169
|
formData.append(
|
|
163
170
|
'metadata',
|
|
164
171
|
new Blob([JSON.stringify(metadata)], { type: 'application/json' }),
|
|
165
172
|
)
|
|
173
|
+
if (file) formData.append('file', file, metadata.name ?? file.name)
|
|
166
174
|
const response = await this.client.requestMultipart<{ data: StorageFile }>(
|
|
167
175
|
'PATCH',
|
|
168
176
|
`${FILES_BASE_PATH}/${id}`,
|
package/src/types/common.ts
CHANGED
|
@@ -103,13 +103,16 @@ export const PLATFORM_USER_ID = 'platform'
|
|
|
103
103
|
/**
|
|
104
104
|
* A reference to a stored file — the value stored by every `file` attribute.
|
|
105
105
|
* `id` is the storage-service file id; `name` is the denormalised filename the
|
|
106
|
-
* record carries so it has a human label without a storage round-trip.
|
|
106
|
+
* record carries so it has a human label without a storage round-trip.
|
|
107
|
+
* `content_type` is an optional denormalised MIME hint so renderers can pick a
|
|
108
|
+
* viewer without a storage round-trip; when absent (legacy values), size and
|
|
107
109
|
* content type are resolved from the storage-service by `id` on read. Mirrors
|
|
108
110
|
* the backend's `common.FileRef`.
|
|
109
111
|
*/
|
|
110
112
|
export interface FileRef {
|
|
111
113
|
id: string
|
|
112
114
|
name: string
|
|
115
|
+
content_type?: string
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
/**
|
|
@@ -118,6 +121,8 @@ export interface FileRef {
|
|
|
118
121
|
export const FileRefSchema = z.object({
|
|
119
122
|
id: z.string(),
|
|
120
123
|
name: z.string(),
|
|
124
|
+
// Mirrors the server-side validator: when present, must be non-empty.
|
|
125
|
+
content_type: z.string().min(1).optional(),
|
|
121
126
|
})
|
|
122
127
|
|
|
123
128
|
/**
|