@fabriktor/fx 0.0.31 → 0.0.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.
@@ -3,7 +3,9 @@
3
3
  // Licensed under the Apache License, Version 2.0 (the "License"); you may
4
4
  // not use this file except in compliance with the License. You may obtain
5
5
  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
- import { errVideoTokenMintFailed } from "../core/err.js";
6
+ import { errResolveFailed } from "../core/err.js";
7
+ const resourceVideoToken = "video_token";
8
+ const msgUnableToMintVideoToken = "Unable to mint video token.";
7
9
  export class CallsFX {
8
10
  calls;
9
11
  notifications;
@@ -26,10 +28,14 @@ export class CallsFX {
26
28
  }
27
29
  async requireVideoToken(opts) {
28
30
  const out = await this.calls.mintVideoToken(opts);
29
- if (out.value === undefined || out.value.trim() === "") {
30
- throw errVideoTokenMintFailed();
31
+ const token = out.value?.trim();
32
+ if (token === undefined || token === "") {
33
+ throw errResolveFailed({
34
+ resource: resourceVideoToken,
35
+ msg: msgUnableToMintVideoToken,
36
+ });
31
37
  }
32
- return out.value;
38
+ return token;
33
39
  }
34
40
  }
35
41
  export function newCallsFX(opts) {
@@ -0,0 +1,89 @@
1
+ import { type Chat, type ChatRoom, type ChatRoomMember, type PLChatReact } from "@fabriktor/schema";
2
+ import type { ChatOperator, WSCloseHandler, WSConnection, WSErrorHandler, WSMessageHandler } from "@fabriktor/client";
3
+ export type ChatFXOptions = {
4
+ chat: ChatOperator;
5
+ };
6
+ export type LatestChatsOptions = {
7
+ chat_room_id: string;
8
+ page?: number;
9
+ per_page?: number;
10
+ };
11
+ export type LatestChatsCursor = {
12
+ chat_room_id: string;
13
+ next_page: number;
14
+ per_page: number;
15
+ done: boolean;
16
+ };
17
+ export type NextLatestChatsOptions = {
18
+ cursor: LatestChatsCursor;
19
+ };
20
+ export type LatestChatsPage = {
21
+ chats: Chat[];
22
+ page: number;
23
+ per_page: number;
24
+ total_count?: number;
25
+ last_page?: number;
26
+ has_more: boolean;
27
+ next_cursor: LatestChatsCursor;
28
+ };
29
+ export type ChatRoomMembersOptions = {
30
+ chat_room_id: string;
31
+ };
32
+ export type MyChatRoomMembersOptions = {
33
+ owner_id: string;
34
+ };
35
+ export type OpenChatRoomOptions = {
36
+ owner_id: string;
37
+ chat_room_id: string;
38
+ per_page?: number;
39
+ on_chat: WSMessageHandler<Chat>;
40
+ on_chat_room: WSMessageHandler<ChatRoom>;
41
+ on_error?: WSErrorHandler;
42
+ on_close?: WSCloseHandler;
43
+ };
44
+ export type OpenGlobalChatRoomsOptions = {
45
+ owner_id: string;
46
+ on_chat_room: WSMessageHandler<ChatRoom>;
47
+ on_error?: WSErrorHandler;
48
+ on_close?: WSCloseHandler;
49
+ };
50
+ export type ReactUploadedChatOptions = {
51
+ chat: Chat;
52
+ reaction: PLChatReact;
53
+ };
54
+ export type ChatRoomSession = {
55
+ latest: LatestChatsPage;
56
+ chats_ws: WSConnection;
57
+ chat_room_ws: WSConnection;
58
+ closeNormal(reason?: string): void;
59
+ closeLeavingPage(reason?: string): void;
60
+ };
61
+ export type GlobalChatRoomsSession = {
62
+ members: ChatRoomMember[];
63
+ chat_rooms_ws: WSConnection;
64
+ closeNormal(reason?: string): void;
65
+ closeLeavingPage(reason?: string): void;
66
+ };
67
+ export interface ChatFXOperator {
68
+ newLatestChatsCursor(opts: LatestChatsOptions): LatestChatsCursor;
69
+ latestChats(opts: LatestChatsOptions): Promise<LatestChatsPage>;
70
+ nextLatestChats(opts: NextLatestChatsOptions): Promise<LatestChatsPage>;
71
+ chatRoomMembers(opts: ChatRoomMembersOptions): Promise<ChatRoomMember[]>;
72
+ myChatRoomMembers(opts: MyChatRoomMembersOptions): Promise<ChatRoomMember[]>;
73
+ openChatRoom(opts: OpenChatRoomOptions): Promise<ChatRoomSession>;
74
+ openGlobalChatRooms(opts: OpenGlobalChatRoomsOptions): Promise<GlobalChatRoomsSession>;
75
+ reactUploadedChat(opts: ReactUploadedChatOptions): Promise<void>;
76
+ }
77
+ export declare class ChatFX implements ChatFXOperator {
78
+ private chat;
79
+ constructor(opts: ChatFXOptions);
80
+ newLatestChatsCursor(opts: LatestChatsOptions): LatestChatsCursor;
81
+ latestChats(opts: LatestChatsOptions): Promise<LatestChatsPage>;
82
+ nextLatestChats(opts: NextLatestChatsOptions): Promise<LatestChatsPage>;
83
+ chatRoomMembers(opts: ChatRoomMembersOptions): Promise<ChatRoomMember[]>;
84
+ myChatRoomMembers(opts: MyChatRoomMembersOptions): Promise<ChatRoomMember[]>;
85
+ openChatRoom(opts: OpenChatRoomOptions): Promise<ChatRoomSession>;
86
+ openGlobalChatRooms(opts: OpenGlobalChatRoomsOptions): Promise<GlobalChatRoomsSession>;
87
+ reactUploadedChat(opts: ReactUploadedChatOptions): Promise<void>;
88
+ }
89
+ export declare function newChatFX(opts: ChatFXOptions): ChatFX;
@@ -0,0 +1,194 @@
1
+ // Copyright (C) Fabriktor, Inc. 2025-present.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+ // not use this file except in compliance with the License. You may obtain
5
+ // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ import { SearchPageKey, SearchPerPageKey, SearchResultResultsKey, } from "@fabriktor/schema";
7
+ import { errValidation } from "../core/err.js";
8
+ const firstPage = 1;
9
+ const defaultPerPage = 30;
10
+ const closeCodeNormal = 1000;
11
+ const closeCodeLeavingPage = 1001;
12
+ const msgChatObjectsUploading = "Cannot react to a chat while objects are still uploading.";
13
+ export class ChatFX {
14
+ chat;
15
+ constructor(opts) {
16
+ this.chat = opts.chat;
17
+ }
18
+ newLatestChatsCursor(opts) {
19
+ return {
20
+ chat_room_id: opts.chat_room_id,
21
+ next_page: normalizePage(opts.page),
22
+ per_page: normalizePerPage(opts.per_page),
23
+ done: false,
24
+ };
25
+ }
26
+ async latestChats(opts) {
27
+ const page = normalizePage(opts.page);
28
+ const per_page = normalizePerPage(opts.per_page);
29
+ const raw = await this.chat.searchManyChats({
30
+ query: {
31
+ chat_room_id: opts.chat_room_id,
32
+ [SearchPageKey]: page,
33
+ [SearchPerPageKey]: per_page,
34
+ },
35
+ });
36
+ return latestChatsPage({
37
+ chat_room_id: opts.chat_room_id,
38
+ page,
39
+ per_page,
40
+ raw,
41
+ });
42
+ }
43
+ async nextLatestChats(opts) {
44
+ if (opts.cursor.done) {
45
+ return emptyLatestChatsPage(opts.cursor);
46
+ }
47
+ return await this.latestChats({
48
+ chat_room_id: opts.cursor.chat_room_id,
49
+ page: opts.cursor.next_page,
50
+ per_page: opts.cursor.per_page,
51
+ });
52
+ }
53
+ async chatRoomMembers(opts) {
54
+ const out = await this.chat.queryChatRoomMembers({
55
+ query: {
56
+ chat_room_id: opts.chat_room_id,
57
+ },
58
+ });
59
+ return out.value ?? [];
60
+ }
61
+ async myChatRoomMembers(opts) {
62
+ const out = await this.chat.queryChatRoomMembers({
63
+ query: {
64
+ owner_id: opts.owner_id,
65
+ },
66
+ });
67
+ return out.value ?? [];
68
+ }
69
+ async openChatRoom(opts) {
70
+ const latest = await this.latestChats({
71
+ chat_room_id: opts.chat_room_id,
72
+ per_page: opts.per_page,
73
+ });
74
+ const chats_ws = await this.chat.wsChats({
75
+ owner_id: opts.owner_id,
76
+ chat_room_id: opts.chat_room_id,
77
+ on_message: opts.on_chat,
78
+ on_error: opts.on_error,
79
+ on_close: opts.on_close,
80
+ });
81
+ const chat_room_ws = await this.chat.wsChatRoom({
82
+ id: opts.chat_room_id,
83
+ on_message: opts.on_chat_room,
84
+ on_error: opts.on_error,
85
+ on_close: opts.on_close,
86
+ });
87
+ return {
88
+ latest,
89
+ chats_ws,
90
+ chat_room_ws,
91
+ closeNormal(reason) {
92
+ closeWS([chats_ws, chat_room_ws], closeCodeNormal, reason);
93
+ },
94
+ closeLeavingPage(reason) {
95
+ closeWS([chats_ws, chat_room_ws], closeCodeLeavingPage, reason);
96
+ },
97
+ };
98
+ }
99
+ async openGlobalChatRooms(opts) {
100
+ const members = await this.myChatRoomMembers({
101
+ owner_id: opts.owner_id,
102
+ });
103
+ const chat_rooms_ws = await this.chat.wsMineChatRooms({
104
+ owner_id: opts.owner_id,
105
+ on_message: opts.on_chat_room,
106
+ on_error: opts.on_error,
107
+ on_close: opts.on_close,
108
+ });
109
+ return {
110
+ members,
111
+ chat_rooms_ws,
112
+ closeNormal(reason) {
113
+ closeWS([chat_rooms_ws], closeCodeNormal, reason);
114
+ },
115
+ closeLeavingPage(reason) {
116
+ closeWS([chat_rooms_ws], closeCodeLeavingPage, reason);
117
+ },
118
+ };
119
+ }
120
+ async reactUploadedChat(opts) {
121
+ if (opts.chat.has_objects && !opts.chat.objects_uploaded) {
122
+ throw errValidation({
123
+ field: "chat_id",
124
+ msg: msgChatObjectsUploading,
125
+ });
126
+ }
127
+ await this.chat.reactChat(opts.reaction);
128
+ }
129
+ }
130
+ export function newChatFX(opts) {
131
+ return new ChatFX(opts);
132
+ }
133
+ function latestChatsPage(opts) {
134
+ const value = opts.raw.value;
135
+ const chats = value?.[SearchResultResultsKey] ?? [];
136
+ const total_count = value?.total_count;
137
+ const last_page = value?.last_page;
138
+ const has_more = hasMoreChats({
139
+ chats,
140
+ page: opts.page,
141
+ per_page: opts.per_page,
142
+ total_count,
143
+ last_page,
144
+ });
145
+ return {
146
+ chats,
147
+ page: opts.page,
148
+ per_page: opts.per_page,
149
+ total_count,
150
+ last_page,
151
+ has_more,
152
+ next_cursor: {
153
+ chat_room_id: opts.chat_room_id,
154
+ next_page: has_more ? opts.page + 1 : opts.page,
155
+ per_page: opts.per_page,
156
+ done: !has_more,
157
+ },
158
+ };
159
+ }
160
+ function emptyLatestChatsPage(cursor) {
161
+ return {
162
+ chats: [],
163
+ page: cursor.next_page,
164
+ per_page: cursor.per_page,
165
+ has_more: false,
166
+ next_cursor: cursor,
167
+ };
168
+ }
169
+ function hasMoreChats(opts) {
170
+ if (opts.last_page !== undefined) {
171
+ return opts.page < opts.last_page;
172
+ }
173
+ if (opts.total_count !== undefined) {
174
+ return opts.page * opts.per_page < opts.total_count;
175
+ }
176
+ return opts.chats.length >= opts.per_page;
177
+ }
178
+ function closeWS(conns, code, reason) {
179
+ for (const conn of conns) {
180
+ conn.close(code, reason);
181
+ }
182
+ }
183
+ function normalizePage(v) {
184
+ if (v === undefined || !Number.isFinite(v)) {
185
+ return firstPage;
186
+ }
187
+ return Math.max(firstPage, Math.trunc(v));
188
+ }
189
+ function normalizePerPage(v) {
190
+ if (v === undefined || !Number.isFinite(v)) {
191
+ return defaultPerPage;
192
+ }
193
+ return Math.max(1, Math.trunc(v));
194
+ }
@@ -0,0 +1,67 @@
1
+ import { Friend, Hired, Manager, Na, OldHire, type Contact, type ID, type OperationResult } from "@fabriktor/schema";
2
+ import type { ContactsOperator } from "@fabriktor/client";
3
+ export type ContactRelation = typeof Na | typeof Friend | typeof OldHire | typeof Hired | typeof Manager;
4
+ export type ActiveContactRelation = typeof Friend | typeof Hired | typeof Manager;
5
+ export type ContactsFXOptions = {
6
+ contacts: ContactsOperator;
7
+ };
8
+ export type LoadContactIndexOptions = {
9
+ owner_id: string;
10
+ };
11
+ export type ContactIndex = {
12
+ contacts: Contact[];
13
+ peer_ids: Set<string>;
14
+ };
15
+ export type PresenceActiveHandler = (active_ids: ID[]) => void;
16
+ export type PresenceErrorHandler = (err: unknown) => void;
17
+ export type StartPresenceOptions = {
18
+ on_active: PresenceActiveHandler;
19
+ on_error?: PresenceErrorHandler;
20
+ };
21
+ export type PresenceSession = {
22
+ stop(): void;
23
+ };
24
+ export type SendFriendRequestOptions = {
25
+ contact_id: string;
26
+ };
27
+ export type RequestRelationUpgradeOptions = {
28
+ contact: Contact;
29
+ requested_relation: ActiveContactRelation;
30
+ };
31
+ export type DowngradeRelationOptions = {
32
+ contact: Contact;
33
+ relation: ActiveContactRelation;
34
+ };
35
+ export type AcceptContactRequestOptions = {
36
+ contact: Contact;
37
+ };
38
+ export type RejectContactRequestOptions = {
39
+ contact: Contact;
40
+ };
41
+ export type EndHireRelationOptions = {
42
+ contact: Contact;
43
+ };
44
+ export interface ContactsFXOperator {
45
+ loadContactIndex(opts: LoadContactIndexOptions): Promise<ContactIndex>;
46
+ startPresence(opts: StartPresenceOptions): Promise<PresenceSession>;
47
+ sendFriendRequest(opts: SendFriendRequestOptions): Promise<OperationResult>;
48
+ requestRelationUpgrade(opts: RequestRelationUpgradeOptions): Promise<OperationResult>;
49
+ downgradeRelation(opts: DowngradeRelationOptions): Promise<OperationResult>;
50
+ acceptContactRequest(opts: AcceptContactRequestOptions): Promise<OperationResult>;
51
+ rejectContactRequest(opts: RejectContactRequestOptions): Promise<OperationResult>;
52
+ endHireRelation(opts: EndHireRelationOptions): Promise<OperationResult>;
53
+ }
54
+ export declare class ContactsFX implements ContactsFXOperator {
55
+ private contacts;
56
+ constructor(opts: ContactsFXOptions);
57
+ loadContactIndex(opts: LoadContactIndexOptions): Promise<ContactIndex>;
58
+ startPresence(opts: StartPresenceOptions): Promise<PresenceSession>;
59
+ sendFriendRequest(opts: SendFriendRequestOptions): Promise<OperationResult>;
60
+ requestRelationUpgrade(opts: RequestRelationUpgradeOptions): Promise<OperationResult>;
61
+ downgradeRelation(opts: DowngradeRelationOptions): Promise<OperationResult>;
62
+ acceptContactRequest(opts: AcceptContactRequestOptions): Promise<OperationResult>;
63
+ rejectContactRequest(opts: RejectContactRequestOptions): Promise<OperationResult>;
64
+ endHireRelation(opts: EndHireRelationOptions): Promise<OperationResult>;
65
+ private replaceContact;
66
+ }
67
+ export declare function newContactsFX(opts: ContactsFXOptions): ContactsFX;
@@ -0,0 +1,179 @@
1
+ // Copyright (C) Fabriktor, Inc. 2025-present.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+ // not use this file except in compliance with the License. You may obtain
5
+ // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ import { Friend, Na, OldHire, } from "@fabriktor/schema";
7
+ import { errRequired, errValidation } from "../core/err.js";
8
+ const presenceIntervalMS = 120_000;
9
+ const msgOwnerIDRequired = "Owner ID is required.";
10
+ const msgContactIDRequired = "Contact ID is required.";
11
+ const msgRelationUpgradeInvalid = "Requested relation must be higher than the current relation.";
12
+ const msgRelationDowngradeInvalid = "Requested relation must be lower than the current relation.";
13
+ const msgPendingRelationRequired = "Contact request does not contain a pending relation.";
14
+ export class ContactsFX {
15
+ contacts;
16
+ constructor(opts) {
17
+ this.contacts = opts.contacts;
18
+ }
19
+ async loadContactIndex(opts) {
20
+ const owner_id = opts.owner_id.trim();
21
+ if (owner_id === "") {
22
+ throw errRequired({
23
+ field: "owner_id",
24
+ msg: msgOwnerIDRequired,
25
+ });
26
+ }
27
+ const out = await this.contacts.findContacts();
28
+ const contacts = out.value ?? [];
29
+ const peer_ids = new Set();
30
+ for (const contact of contacts) {
31
+ const peer_id = contactPeerID(contact, owner_id);
32
+ if (peer_id !== undefined) {
33
+ peer_ids.add(peer_id);
34
+ }
35
+ }
36
+ return {
37
+ contacts,
38
+ peer_ids,
39
+ };
40
+ }
41
+ async startPresence(opts) {
42
+ const initial = await this.contacts.signal();
43
+ opts.on_active(initial.value ?? []);
44
+ let stopped = false;
45
+ let running = false;
46
+ const tick = async () => {
47
+ if (stopped || running) {
48
+ return;
49
+ }
50
+ running = true;
51
+ try {
52
+ const out = await this.contacts.signal();
53
+ if (!stopped) {
54
+ opts.on_active(out.value ?? []);
55
+ }
56
+ }
57
+ catch (err) {
58
+ if (!stopped) {
59
+ opts.on_error?.(err);
60
+ }
61
+ }
62
+ finally {
63
+ running = false;
64
+ }
65
+ };
66
+ const timer = setInterval(() => {
67
+ void tick();
68
+ }, presenceIntervalMS);
69
+ return {
70
+ stop() {
71
+ if (stopped) {
72
+ return;
73
+ }
74
+ stopped = true;
75
+ clearInterval(timer);
76
+ },
77
+ };
78
+ }
79
+ async sendFriendRequest(opts) {
80
+ const contact_id = opts.contact_id.trim();
81
+ if (contact_id === "") {
82
+ throw errRequired({
83
+ field: "contact_id",
84
+ msg: msgContactIDRequired,
85
+ });
86
+ }
87
+ return await this.contacts.insertOneContact({
88
+ in: {
89
+ contact_id,
90
+ is_pending: true,
91
+ relation: Na,
92
+ requested_relation: Friend,
93
+ is_archived: false,
94
+ },
95
+ });
96
+ }
97
+ async requestRelationUpgrade(opts) {
98
+ if (opts.requested_relation <= opts.contact.relation) {
99
+ throw errValidation({
100
+ field: "requested_relation",
101
+ msg: msgRelationUpgradeInvalid,
102
+ });
103
+ }
104
+ return await this.replaceContact(opts.contact, {
105
+ is_pending: true,
106
+ relation: opts.contact.relation,
107
+ requested_relation: opts.requested_relation,
108
+ is_archived: false,
109
+ });
110
+ }
111
+ async downgradeRelation(opts) {
112
+ if (opts.relation >= opts.contact.relation) {
113
+ throw errValidation({
114
+ field: "relation",
115
+ msg: msgRelationDowngradeInvalid,
116
+ });
117
+ }
118
+ return await this.replaceContact(opts.contact, {
119
+ is_pending: false,
120
+ relation: opts.relation,
121
+ requested_relation: Na,
122
+ is_archived: false,
123
+ });
124
+ }
125
+ async acceptContactRequest(opts) {
126
+ if (opts.contact.requested_relation === Na) {
127
+ throw errValidation({
128
+ field: "contact",
129
+ msg: msgPendingRelationRequired,
130
+ });
131
+ }
132
+ return await this.replaceContact(opts.contact, {
133
+ is_pending: false,
134
+ relation: opts.contact.requested_relation,
135
+ requested_relation: Na,
136
+ is_archived: false,
137
+ });
138
+ }
139
+ async rejectContactRequest(opts) {
140
+ return await this.replaceContact(opts.contact, {
141
+ is_archived: true,
142
+ });
143
+ }
144
+ async endHireRelation(opts) {
145
+ return await this.replaceContact(opts.contact, {
146
+ is_pending: false,
147
+ relation: OldHire,
148
+ requested_relation: Na,
149
+ is_archived: false,
150
+ });
151
+ }
152
+ async replaceContact(contact, overrides) {
153
+ return await this.contacts.replaceOneContact({
154
+ id: contact.id,
155
+ in: {
156
+ contact_id: contact.contact_id,
157
+ is_pending: contact.is_pending,
158
+ relation: contact.relation,
159
+ requested_relation: contact.requested_relation,
160
+ is_archived: contact.is_archived,
161
+ ...overrides,
162
+ },
163
+ });
164
+ }
165
+ }
166
+ export function newContactsFX(opts) {
167
+ return new ContactsFX(opts);
168
+ }
169
+ function contactPeerID(contact, owner_id) {
170
+ const contact_owner_id = contact.owner_id.trim();
171
+ const contact_id = contact.contact_id.trim();
172
+ if (contact_owner_id === owner_id && contact_id !== "") {
173
+ return contact_id;
174
+ }
175
+ if (contact_id === owner_id && contact_owner_id !== "") {
176
+ return contact_owner_id;
177
+ }
178
+ return undefined;
179
+ }
@@ -29,11 +29,6 @@ export type ResolveFailedFXErrOptions<T extends object = Record<string, unknown>
29
29
  field?: FieldOf<T>;
30
30
  cause?: unknown;
31
31
  };
32
- export type UsernameInvalidFXErrOptions<T extends object = Record<string, unknown>> = {
33
- field: FieldOf<T>;
34
- msg: string;
35
- cause?: unknown;
36
- };
37
32
  type FXErrRuntimeOptions = {
38
33
  kind: FXErrKind;
39
34
  msg: string;
@@ -52,11 +47,6 @@ export declare function errFX<T extends object = Record<string, unknown>>(opts:
52
47
  export declare function errRequired<T extends object = Record<string, unknown>>(opts: RequiredFXErrOptions<T>): FXErr;
53
48
  export declare function errValidation<T extends object = Record<string, unknown>>(opts: ValidationFXErrOptions<T>): FXErr;
54
49
  export declare function errResolveFailed<T extends object = Record<string, unknown>>(opts: ResolveFailedFXErrOptions<T>): FXErr;
55
- export declare function errUsernameResolveFailed<T extends object = Record<string, unknown>>(field?: FieldOf<T>): FXErr;
56
- export declare function errVideoTokenMintFailed(): FXErr;
57
- export declare function errUsernameInvalid<T extends object = Record<string, unknown>>(opts: UsernameInvalidFXErrOptions<T>): FXErr;
58
- export declare function errPhoneRequired<T extends object = Record<string, unknown>>(field: FieldOf<T>): FXErr;
59
- export declare function errPhoneCodeRequired<T extends object = Record<string, unknown>>(field: FieldOf<T>): FXErr;
60
50
  export declare function errUnknown(cause: unknown): FXErr;
61
51
  export declare function isFXErr(err: unknown): err is FXErr;
62
52
  export declare function fxErrMessage(err: unknown): string;
@@ -4,12 +4,6 @@
4
4
  // not use this file except in compliance with the License. You may obtain
5
5
  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
6
  const msgUnknownFXErr = "Unknown FX error.";
7
- const msgUnableToResolveUsername = "Unable to resolve username.";
8
- const msgUnableToMintVideoToken = "Unable to mint video token.";
9
- const msgPhoneRequired = "Phone number is required.";
10
- const msgPhoneCodeRequired = "Verification code is required.";
11
- const resourceUsername = "username";
12
- const resourceVideoToken = "video_token";
13
7
  export const FXErrKind = {
14
8
  Required: "required",
15
9
  Validation: "validation",
@@ -64,38 +58,6 @@ export function errResolveFailed(opts) {
64
58
  cause: opts.cause,
65
59
  });
66
60
  }
67
- export function errUsernameResolveFailed(field) {
68
- return errResolveFailed({
69
- resource: resourceUsername,
70
- field,
71
- msg: msgUnableToResolveUsername,
72
- });
73
- }
74
- export function errVideoTokenMintFailed() {
75
- return errResolveFailed({
76
- resource: resourceVideoToken,
77
- msg: msgUnableToMintVideoToken,
78
- });
79
- }
80
- export function errUsernameInvalid(opts) {
81
- return errValidation({
82
- field: opts.field,
83
- msg: opts.msg,
84
- cause: opts.cause,
85
- });
86
- }
87
- export function errPhoneRequired(field) {
88
- return errRequired({
89
- field,
90
- msg: msgPhoneRequired,
91
- });
92
- }
93
- export function errPhoneCodeRequired(field) {
94
- return errRequired({
95
- field,
96
- msg: msgPhoneCodeRequired,
97
- });
98
- }
99
61
  export function errUnknown(cause) {
100
62
  if (isFXErr(cause)) {
101
63
  return cause;
package/dist/src/fx.d.ts CHANGED
@@ -1,19 +1,26 @@
1
- import type { BillableOperator, CallsOperator, NotificationsOperator, PreferencesOperator, TmpPhonesOperator, TmpUsernamesOperator, UsersOperator } from "@fabriktor/client";
1
+ import type { BillableOperator, CallsOperator, ChatOperator, ConnectionsOperator, ContactsOperator, NotificationsOperator, PreferencesOperator, StorageOperator, TmpPhonesOperator, TmpUsernamesOperator, UsersOperator } from "@fabriktor/client";
2
2
  import type { AuthOperator } from "./auth/auth.js";
3
3
  import type { BillableFX, BillableFXOperator } from "./billable/billable.js";
4
4
  import type { CallsFX, CallsFXOperator } from "./calls/calls.js";
5
+ import type { ChatFX, ChatFXOperator } from "./chat/chat.js";
6
+ import type { ContactsFX, ContactsFXOperator } from "./contacts/contacts.js";
5
7
  import type { PreferencesFX, PreferencesFXOperator } from "./preferences/preferences.js";
6
8
  import type { SessionFX, SessionFXOperator } from "./session/session.js";
9
+ import type { StorageFX, StorageFXOperator } from "./storage/storage.js";
7
10
  import type { UsersFX, UsersFXOperator } from "./users/users.js";
8
11
  export type FXOptions = {
9
12
  auth: AuthOperator;
10
13
  billable: BillableOperator;
11
14
  calls: CallsOperator;
15
+ chat: ChatOperator;
16
+ connections: ConnectionsOperator;
17
+ contacts: ContactsOperator;
12
18
  notifications: NotificationsOperator;
13
- users: UsersOperator;
14
- tmp_usernames: TmpUsernamesOperator;
15
- tmp_phones: TmpPhonesOperator;
16
19
  preferences: PreferencesOperator;
20
+ storage: StorageOperator;
21
+ tmp_phones: TmpPhonesOperator;
22
+ tmp_usernames: TmpUsernamesOperator;
23
+ users: UsersOperator;
17
24
  };
18
25
  export type DefaultFXOptions = {
19
26
  base_url: string;
@@ -22,16 +29,22 @@ export type DefaultFXOptions = {
22
29
  export interface FXOperator {
23
30
  billable: BillableFXOperator;
24
31
  calls: CallsFXOperator;
25
- users: UsersFXOperator;
26
- session: SessionFXOperator;
32
+ chat: ChatFXOperator;
33
+ contacts: ContactsFXOperator;
27
34
  preferences: PreferencesFXOperator;
35
+ session: SessionFXOperator;
36
+ storage: StorageFXOperator;
37
+ users: UsersFXOperator;
28
38
  }
29
39
  export declare class FX implements FXOperator {
30
40
  billable: BillableFX;
31
41
  calls: CallsFX;
32
- users: UsersFX;
33
- session: SessionFX;
42
+ chat: ChatFX;
43
+ contacts: ContactsFX;
34
44
  preferences: PreferencesFX;
45
+ session: SessionFX;
46
+ storage: StorageFX;
47
+ users: UsersFX;
35
48
  constructor(opts: FXOptions);
36
49
  }
37
50
  export declare function newFX(opts: FXOptions): FX;
package/dist/src/fx.js CHANGED
@@ -1,20 +1,26 @@
1
- // Copyright (C) Fabriktor, Inc. 2025-present.
1
+ // Copyright (C) Fab// Copyright (C) Fabriktor, Inc. 2025-present.
2
2
  //
3
3
  // Licensed under the Apache License, Version 2.0 (the "License"); you may
4
4
  // not use this file except in compliance with the License. You may obtain
5
5
  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
- import { newBillableClient, newCallsClient, newHTTPClient, newNotificationsClient, newPreferencesClient, newTmpPhonesClient, newTmpUsernamesClient, newUsersClient, } from "@fabriktor/client";
6
+ import { newBillableClient, newCallsClient, newChatClient, newConnectionsClient, newContactsClient, newHTTPClient, newNotificationsClient, newPreferencesClient, newStorageClient, newTmpPhonesClient, newTmpUsernamesClient, newUsersClient, } from "@fabriktor/client";
7
7
  import { newBillableFX } from "./billable/billable.js";
8
8
  import { newCallsFX } from "./calls/calls.js";
9
+ import { newChatFX } from "./chat/chat.js";
10
+ import { newContactsFX } from "./contacts/contacts.js";
9
11
  import { newPreferencesFX } from "./preferences/preferences.js";
10
12
  import { newSessionFX } from "./session/session.js";
13
+ import { newStorageFX } from "./storage/storage.js";
11
14
  import { newUsersFX } from "./users/users.js";
12
15
  export class FX {
13
16
  billable;
14
17
  calls;
15
- users;
16
- session;
18
+ chat;
19
+ contacts;
17
20
  preferences;
21
+ session;
22
+ storage;
23
+ users;
18
24
  constructor(opts) {
19
25
  this.billable = newBillableFX({
20
26
  billable: opts.billable,
@@ -23,16 +29,26 @@ export class FX {
23
29
  calls: opts.calls,
24
30
  notifications: opts.notifications,
25
31
  });
32
+ this.chat = newChatFX({
33
+ chat: opts.chat,
34
+ });
35
+ this.contacts = newContactsFX({
36
+ contacts: opts.contacts,
37
+ });
38
+ this.preferences = newPreferencesFX({
39
+ preferences: opts.preferences,
40
+ });
41
+ this.storage = newStorageFX({
42
+ storage: opts.storage,
43
+ });
26
44
  this.users = newUsersFX({
27
45
  users: opts.users,
28
46
  tmp_usernames: opts.tmp_usernames,
29
47
  tmp_phones: opts.tmp_phones,
30
48
  });
31
- this.preferences = newPreferencesFX({
32
- preferences: opts.preferences,
33
- });
34
49
  this.session = newSessionFX({
35
50
  auth: opts.auth,
51
+ connections: opts.connections,
36
52
  users: opts.users,
37
53
  users_fx: this.users,
38
54
  });
@@ -56,17 +72,32 @@ export function newDefaultFX(opts) {
56
72
  base_url: opts.base_url,
57
73
  get_token,
58
74
  }),
75
+ chat: newChatClient({
76
+ http,
77
+ base_url: opts.base_url,
78
+ get_token,
79
+ }),
80
+ connections: newConnectionsClient({
81
+ http,
82
+ base_url: opts.base_url,
83
+ get_token,
84
+ }),
85
+ contacts: newContactsClient({
86
+ http,
87
+ base_url: opts.base_url,
88
+ get_token,
89
+ }),
59
90
  notifications: newNotificationsClient({
60
91
  http,
61
92
  base_url: opts.base_url,
62
93
  get_token,
63
94
  }),
64
- users: newUsersClient({
95
+ preferences: newPreferencesClient({
65
96
  http,
66
97
  base_url: opts.base_url,
67
98
  get_token,
68
99
  }),
69
- tmp_usernames: newTmpUsernamesClient({
100
+ storage: newStorageClient({
70
101
  http,
71
102
  base_url: opts.base_url,
72
103
  get_token,
@@ -76,7 +107,12 @@ export function newDefaultFX(opts) {
76
107
  base_url: opts.base_url,
77
108
  get_token,
78
109
  }),
79
- preferences: newPreferencesClient({
110
+ tmp_usernames: newTmpUsernamesClient({
111
+ http,
112
+ base_url: opts.base_url,
113
+ get_token,
114
+ }),
115
+ users: newUsersClient({
80
116
  http,
81
117
  base_url: opts.base_url,
82
118
  get_token,
@@ -1,10 +1,13 @@
1
1
  export * from "./auth/auth.js";
2
2
  export * from "./billable/billable.js";
3
3
  export * from "./calls/calls.js";
4
+ export * from "./chat/chat.js";
5
+ export * from "./contacts/contacts.js";
4
6
  export * from "./core/err.js";
5
7
  export * from "./fx.js";
6
8
  export * from "./preferences/preferences.js";
7
9
  export * from "./session/session.js";
10
+ export * from "./storage/storage.js";
8
11
  export * from "./users/phone.js";
9
12
  export * from "./users/username.js";
10
13
  export * from "./users/users.js";
package/dist/src/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  export * from "./auth/auth.js";
2
2
  export * from "./billable/billable.js";
3
3
  export * from "./calls/calls.js";
4
+ export * from "./chat/chat.js";
5
+ export * from "./contacts/contacts.js";
4
6
  export * from "./core/err.js";
5
7
  export * from "./fx.js";
6
8
  export * from "./preferences/preferences.js";
7
9
  export * from "./session/session.js";
10
+ export * from "./storage/storage.js";
8
11
  export * from "./users/phone.js";
9
12
  export * from "./users/username.js";
10
13
  export * from "./users/users.js";
@@ -1,18 +1,27 @@
1
- import type { InUser, PLUsersForgotPassword, PLUsersSignInLink, PLUsersVerifyEmail, UpsertResult } from "@fabriktor/schema";
2
- import type { BootstrapOptions, OperationResultOf, UsersOperator } from "@fabriktor/client";
1
+ import type { InConnection, InUser, PLUsersForgotPassword, PLUsersSignInLink, PLUsersVerifyEmail, UpsertResult } from "@fabriktor/schema";
2
+ import type { BootstrapOptions, ConnectionsOperator, OperationResultOf, UsersOperator } from "@fabriktor/client";
3
3
  import type { AuthEmailOptions, AuthOperator } from "../auth/auth.js";
4
4
  import type { UsersFXOperator } from "../users/users.js";
5
- export type SignInMode = "email" | "username";
5
+ export declare const SignInMode: {
6
+ readonly Email: "email";
7
+ readonly Username: "username";
8
+ };
9
+ export type SignInMode = (typeof SignInMode)[keyof typeof SignInMode];
6
10
  export type SignInOptions = {
7
11
  mode: SignInMode;
8
12
  identifier: string;
9
13
  password: string;
14
+ connection: InConnection;
15
+ };
16
+ export type SignInWithEmailOptions = AuthEmailOptions & {
17
+ connection: InConnection;
10
18
  };
11
19
  export type SignUpAndBootstrapOptions = AuthEmailOptions & {
12
20
  in: InUser;
13
21
  };
14
22
  export type SessionFXOptions = {
15
23
  auth: AuthOperator;
24
+ connections: ConnectionsOperator;
16
25
  users: UsersOperator;
17
26
  users_fx: UsersFXOperator;
18
27
  };
@@ -21,7 +30,7 @@ export interface SessionFXOperator {
21
30
  finishSignUp(opts: BootstrapOptions): Promise<OperationResultOf<UpsertResult>>;
22
31
  signUpAndBootstrap(opts: SignUpAndBootstrapOptions): Promise<OperationResultOf<UpsertResult>>;
23
32
  signIn(opts: SignInOptions): Promise<void>;
24
- signInWithEmail(opts: AuthEmailOptions): Promise<void>;
33
+ signInWithEmail(opts: SignInWithEmailOptions): Promise<void>;
25
34
  signOut(): Promise<void>;
26
35
  forgotPassword(opts: PLUsersForgotPassword): Promise<void>;
27
36
  sendSignInLink(opts: PLUsersSignInLink): Promise<void>;
@@ -30,6 +39,7 @@ export interface SessionFXOperator {
30
39
  }
31
40
  export declare class SessionFX implements SessionFXOperator {
32
41
  private auth;
42
+ private connections;
33
43
  private users;
34
44
  private users_fx;
35
45
  constructor(opts: SessionFXOptions);
@@ -37,7 +47,7 @@ export declare class SessionFX implements SessionFXOperator {
37
47
  finishSignUp(opts: BootstrapOptions): Promise<OperationResultOf<UpsertResult>>;
38
48
  signUpAndBootstrap(opts: SignUpAndBootstrapOptions): Promise<OperationResultOf<UpsertResult>>;
39
49
  signIn(opts: SignInOptions): Promise<void>;
40
- signInWithEmail(opts: AuthEmailOptions): Promise<void>;
50
+ signInWithEmail(opts: SignInWithEmailOptions): Promise<void>;
41
51
  signOut(): Promise<void>;
42
52
  forgotPassword(opts: PLUsersForgotPassword): Promise<void>;
43
53
  sendSignInLink(opts: PLUsersSignInLink): Promise<void>;
@@ -3,12 +3,18 @@
3
3
  // Licensed under the Apache License, Version 2.0 (the "License"); you may
4
4
  // not use this file except in compliance with the License. You may obtain
5
5
  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ export const SignInMode = {
7
+ Email: "email",
8
+ Username: "username",
9
+ };
6
10
  export class SessionFX {
7
11
  auth;
12
+ connections;
8
13
  users;
9
14
  users_fx;
10
15
  constructor(opts) {
11
16
  this.auth = opts.auth;
17
+ this.connections = opts.connections;
12
18
  this.users = opts.users;
13
19
  this.users_fx = opts.users_fx;
14
20
  }
@@ -33,7 +39,7 @@ export class SessionFX {
33
39
  });
34
40
  }
35
41
  async signIn(opts) {
36
- const email = opts.mode === "email"
42
+ const email = opts.mode === SignInMode.Email
37
43
  ? opts.identifier
38
44
  : await this.users_fx.resolveUsername({
39
45
  username: opts.identifier,
@@ -41,10 +47,17 @@ export class SessionFX {
41
47
  await this.signInWithEmail({
42
48
  email,
43
49
  password: opts.password,
50
+ connection: opts.connection,
44
51
  });
45
52
  }
46
53
  async signInWithEmail(opts) {
47
- await this.auth.signIn(opts);
54
+ await this.auth.signIn({
55
+ email: opts.email,
56
+ password: opts.password,
57
+ });
58
+ await this.connections.insertOneConnection({
59
+ in: opts.connection,
60
+ });
48
61
  await this.users.sync();
49
62
  }
50
63
  async signOut() {
@@ -0,0 +1,70 @@
1
+ import { type Object as StorageObject, type PLStorageDownload, type RPStorageUpload } from "@fabriktor/schema";
2
+ import type { OperationResultOf, StorageCollection, StorageOperator, StorageUploadFile, UploadStorageOptions, OptionalField } from "@fabriktor/client";
3
+ export declare const StorageUploadModeration: {
4
+ readonly Clean: "clean";
5
+ readonly ContainsForbidden: "contains_forbidden";
6
+ readonly AllForbidden: "all_forbidden";
7
+ };
8
+ export type StorageUploadModeration = (typeof StorageUploadModeration)[keyof typeof StorageUploadModeration];
9
+ export declare const StorageDualUploadStatus: {
10
+ readonly ChatOnly: "chat_only";
11
+ readonly Complete: "complete";
12
+ readonly Partial: "partial";
13
+ };
14
+ export type StorageDualUploadStatus = (typeof StorageDualUploadStatus)[keyof typeof StorageDualUploadStatus];
15
+ export type StorageFXOptions = {
16
+ storage: StorageOperator;
17
+ };
18
+ export type UploadObjectsOptions = UploadStorageOptions & {
19
+ col: StorageCollection;
20
+ };
21
+ export type DownloadObjectsOptions = OptionalField<PLStorageDownload, "mime_prefix"> & {
22
+ col: StorageCollection;
23
+ };
24
+ export type UploadUserObjectsOptions = {
25
+ user_id: string;
26
+ uploader_id?: string;
27
+ full_quality?: boolean;
28
+ files: StorageUploadFile[];
29
+ };
30
+ export type UploadChatObjectsOptions = {
31
+ chat_id: string;
32
+ user_id?: string;
33
+ uploader_id?: string;
34
+ full_quality?: boolean;
35
+ files: StorageUploadFile[];
36
+ save_to_personal_box?: boolean;
37
+ };
38
+ export type StorageUploadResult = {
39
+ raw: OperationResultOf<RPStorageUpload>;
40
+ moderation: StorageUploadModeration;
41
+ contains_forbidden: boolean;
42
+ all_forbidden: boolean;
43
+ };
44
+ export type UploadChatObjectsResult = {
45
+ status: typeof StorageDualUploadStatus.ChatOnly;
46
+ chat: StorageUploadResult;
47
+ } | {
48
+ status: typeof StorageDualUploadStatus.Complete;
49
+ chat: StorageUploadResult;
50
+ personal_box: StorageUploadResult;
51
+ } | {
52
+ status: typeof StorageDualUploadStatus.Partial;
53
+ chat: StorageUploadResult;
54
+ personal_box_error: unknown;
55
+ };
56
+ export interface StorageFXOperator {
57
+ uploadObjects(opts: UploadObjectsOptions): Promise<StorageUploadResult>;
58
+ downloadObjects(opts: DownloadObjectsOptions): Promise<OperationResultOf<StorageObject[]>>;
59
+ uploadUserObjects(opts: UploadUserObjectsOptions): Promise<StorageUploadResult>;
60
+ uploadChatObjects(opts: UploadChatObjectsOptions): Promise<UploadChatObjectsResult>;
61
+ }
62
+ export declare class StorageFX implements StorageFXOperator {
63
+ private storage;
64
+ constructor(opts: StorageFXOptions);
65
+ uploadObjects(opts: UploadObjectsOptions): Promise<StorageUploadResult>;
66
+ downloadObjects(opts: DownloadObjectsOptions): Promise<OperationResultOf<StorageObject[]>>;
67
+ uploadUserObjects(opts: UploadUserObjectsOptions): Promise<StorageUploadResult>;
68
+ uploadChatObjects(opts: UploadChatObjectsOptions): Promise<UploadChatObjectsResult>;
69
+ }
70
+ export declare function newStorageFX(opts: StorageFXOptions): StorageFX;
@@ -0,0 +1,225 @@
1
+ // Copyright (C) Fabriktor, Inc. 2025-present.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+ // not use this file except in compliance with the License. You may obtain
5
+ // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ import { ColStorageChat, ColStorageUsers, } from "@fabriktor/schema";
7
+ import { errRequired, errValidation } from "../core/err.js";
8
+ const maxUploadFiles = 20;
9
+ const maxUploadBytes = 1024 * 1024 * 1024;
10
+ const mimePrefixAll = "";
11
+ const extensionHEIC = ".heic";
12
+ const extensionHEIF = ".heif";
13
+ const mimeImageHEIC = "image/heic";
14
+ const mimeImageHEIF = "image/heif";
15
+ const mimeImageHEICSequence = "image/heic-sequence";
16
+ const mimeImageHEIFSequence = "image/heif-sequence";
17
+ const msgOwnerIDRequired = "Owner ID is required.";
18
+ const msgChatIDRequired = "Chat ID is required.";
19
+ const msgUserIDRequired = "User ID is required.";
20
+ const msgUploadFilesRequired = "At least one file is required.";
21
+ const msgUploadTooManyFiles = "Upload supports at most 20 files.";
22
+ const msgUploadTooLarge = "Combined upload size cannot exceed 1 GB.";
23
+ const msgHEICUnsupported = "HEIC and HEIF files must be converted to JPEG or PNG before upload.";
24
+ export const StorageUploadModeration = {
25
+ Clean: "clean",
26
+ ContainsForbidden: "contains_forbidden",
27
+ AllForbidden: "all_forbidden",
28
+ };
29
+ export const StorageDualUploadStatus = {
30
+ ChatOnly: "chat_only",
31
+ Complete: "complete",
32
+ Partial: "partial",
33
+ };
34
+ export class StorageFX {
35
+ storage;
36
+ constructor(opts) {
37
+ this.storage = opts.storage;
38
+ }
39
+ async uploadObjects(opts) {
40
+ validateUpload(opts);
41
+ const out = await this.storage.upload(opts.col, {
42
+ owner_id: opts.owner_id,
43
+ uploader_id: opts.uploader_id,
44
+ full_quality: opts.full_quality,
45
+ files: opts.files,
46
+ });
47
+ return uploadResult(out);
48
+ }
49
+ async downloadObjects(opts) {
50
+ const owner_id = opts.owner_id.trim();
51
+ if (owner_id === "") {
52
+ throw errRequired({
53
+ field: "owner_id",
54
+ msg: msgOwnerIDRequired,
55
+ });
56
+ }
57
+ const { col, mime_prefix, ...input } = opts;
58
+ return await this.storage.download(col, {
59
+ ...input,
60
+ owner_id,
61
+ mime_prefix: mime_prefix?.trim() ?? mimePrefixAll,
62
+ });
63
+ }
64
+ async uploadUserObjects(opts) {
65
+ const user_id = opts.user_id.trim();
66
+ if (user_id === "") {
67
+ throw errRequired({
68
+ field: "user_id",
69
+ msg: msgUserIDRequired,
70
+ });
71
+ }
72
+ return await this.uploadObjects({
73
+ col: ColStorageUsers,
74
+ owner_id: user_id,
75
+ uploader_id: opts.uploader_id,
76
+ full_quality: opts.full_quality,
77
+ files: opts.files,
78
+ });
79
+ }
80
+ async uploadChatObjects(opts) {
81
+ const chat_id = opts.chat_id.trim();
82
+ if (chat_id === "") {
83
+ throw errRequired({
84
+ field: "chat_id",
85
+ msg: msgChatIDRequired,
86
+ });
87
+ }
88
+ let personal_box_user_id;
89
+ if (opts.save_to_personal_box) {
90
+ const user_id = opts.user_id?.trim() ?? "";
91
+ if (user_id === "") {
92
+ throw errRequired({
93
+ field: "user_id",
94
+ msg: msgUserIDRequired,
95
+ });
96
+ }
97
+ personal_box_user_id = user_id;
98
+ }
99
+ const chat = await this.uploadObjects({
100
+ col: ColStorageChat,
101
+ owner_id: chat_id,
102
+ uploader_id: opts.uploader_id,
103
+ full_quality: opts.full_quality,
104
+ files: opts.files,
105
+ });
106
+ if (personal_box_user_id === undefined) {
107
+ return {
108
+ status: StorageDualUploadStatus.ChatOnly,
109
+ chat,
110
+ };
111
+ }
112
+ try {
113
+ const personal_box = await this.uploadUserObjects({
114
+ user_id: personal_box_user_id,
115
+ uploader_id: opts.uploader_id,
116
+ full_quality: opts.full_quality,
117
+ files: opts.files,
118
+ });
119
+ return {
120
+ status: StorageDualUploadStatus.Complete,
121
+ chat,
122
+ personal_box,
123
+ };
124
+ }
125
+ catch (err) {
126
+ return {
127
+ status: StorageDualUploadStatus.Partial,
128
+ chat,
129
+ personal_box_error: err,
130
+ };
131
+ }
132
+ }
133
+ }
134
+ export function newStorageFX(opts) {
135
+ return new StorageFX(opts);
136
+ }
137
+ function validateUpload(opts) {
138
+ if (opts.owner_id.trim() === "") {
139
+ throw errRequired({
140
+ field: "owner_id",
141
+ msg: msgOwnerIDRequired,
142
+ });
143
+ }
144
+ if (opts.files.length === 0) {
145
+ throw errRequired({
146
+ field: "files",
147
+ msg: msgUploadFilesRequired,
148
+ });
149
+ }
150
+ if (opts.files.length > maxUploadFiles) {
151
+ throw errValidation({
152
+ field: "files",
153
+ msg: msgUploadTooManyFiles,
154
+ });
155
+ }
156
+ if (uploadSize(opts.files) > maxUploadBytes) {
157
+ throw errValidation({
158
+ field: "files",
159
+ msg: msgUploadTooLarge,
160
+ });
161
+ }
162
+ if (opts.files.some(isHEIC)) {
163
+ throw errValidation({
164
+ field: "files",
165
+ msg: msgHEICUnsupported,
166
+ });
167
+ }
168
+ }
169
+ function uploadSize(files) {
170
+ let out = 0;
171
+ for (const f of files) {
172
+ out += fileSize(f);
173
+ }
174
+ return out;
175
+ }
176
+ function fileSize(f) {
177
+ if ("blob" in f) {
178
+ return f.blob.size;
179
+ }
180
+ return f.size;
181
+ }
182
+ function isHEIC(f) {
183
+ const name = fileName(f).toLowerCase();
184
+ const mime = fileMIME(f).toLowerCase();
185
+ return (name.endsWith(extensionHEIC) ||
186
+ name.endsWith(extensionHEIF) ||
187
+ mime === mimeImageHEIC ||
188
+ mime === mimeImageHEIF ||
189
+ mime === mimeImageHEICSequence ||
190
+ mime === mimeImageHEIFSequence);
191
+ }
192
+ function fileName(f) {
193
+ if ("blob" in f) {
194
+ return f.filename;
195
+ }
196
+ return f.name;
197
+ }
198
+ function fileMIME(f) {
199
+ if ("blob" in f) {
200
+ return f.blob.type;
201
+ }
202
+ return f.type;
203
+ }
204
+ function uploadResult(raw) {
205
+ const contains_forbidden = raw.value?.contains_forbidden ?? false;
206
+ const all_forbidden = raw.value?.all_forbidden ?? false;
207
+ return {
208
+ raw,
209
+ moderation: uploadModeration({
210
+ contains_forbidden,
211
+ all_forbidden,
212
+ }),
213
+ contains_forbidden,
214
+ all_forbidden,
215
+ };
216
+ }
217
+ function uploadModeration(opts) {
218
+ if (opts.all_forbidden) {
219
+ return StorageUploadModeration.AllForbidden;
220
+ }
221
+ if (opts.contains_forbidden) {
222
+ return StorageUploadModeration.ContainsForbidden;
223
+ }
224
+ return StorageUploadModeration.Clean;
225
+ }
@@ -3,9 +3,13 @@
3
3
  // Licensed under the Apache License, Version 2.0 (the "License"); you may
4
4
  // not use this file except in compliance with the License. You may obtain
5
5
  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
- import { errPhoneCodeRequired, errPhoneRequired, errUsernameInvalid, errUsernameResolveFailed, } from "../core/err.js";
6
+ import { errRequired, errResolveFailed, errValidation, } from "../core/err.js";
7
7
  import { normalizePhone, normalizePhoneCode, validatePhone, validatePhoneCode, } from "./phone.js";
8
8
  import { normalizeUsername, validateUsernameFormat } from "./username.js";
9
+ const msgUnableToResolveUsername = "Unable to resolve username.";
10
+ const msgPhoneRequired = "Phone number is required.";
11
+ const msgPhoneCodeRequired = "Verification code is required.";
12
+ const resourceUsername = "username";
9
13
  export class UsersFX {
10
14
  users;
11
15
  tmp_usernames;
@@ -30,10 +34,7 @@ export class UsersFX {
30
34
  async verifyUsername(opts) {
31
35
  const check = validateUsernameFormat(opts.to_validate);
32
36
  if (!check.valid) {
33
- throw errUsernameInvalid({
34
- field: "to_validate",
35
- msg: check.error,
36
- });
37
+ throw errUsernameInvalid("to_validate", check.error);
37
38
  }
38
39
  return await this.tmp_usernames.verify({
39
40
  words: opts.words,
@@ -73,3 +74,28 @@ export class UsersFX {
73
74
  export function newUsersFX(opts) {
74
75
  return new UsersFX(opts);
75
76
  }
77
+ function errUsernameResolveFailed(field) {
78
+ return errResolveFailed({
79
+ resource: resourceUsername,
80
+ field,
81
+ msg: msgUnableToResolveUsername,
82
+ });
83
+ }
84
+ function errUsernameInvalid(field, msg) {
85
+ return errValidation({
86
+ field,
87
+ msg,
88
+ });
89
+ }
90
+ function errPhoneRequired(field) {
91
+ return errRequired({
92
+ field,
93
+ msg: msgPhoneRequired,
94
+ });
95
+ }
96
+ function errPhoneCodeRequired(field) {
97
+ return errRequired({
98
+ field,
99
+ msg: msgPhoneCodeRequired,
100
+ });
101
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabriktor/fx",
3
- "version": "0.0.31",
3
+ "version": "0.0.32",
4
4
  "description": "![Fabriktor Logo](./img/fabriktor-character.gif)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,7 +17,7 @@
17
17
  "typescript": "^6.0.3"
18
18
  },
19
19
  "dependencies": {
20
- "@fabriktor/client": "0.0.32",
21
- "@fabriktor/schema": "0.0.27"
20
+ "@fabriktor/client": "0.0.38",
21
+ "@fabriktor/schema": "0.0.29"
22
22
  }
23
23
  }