1 import { api } from '@proton/pass/lib/api/api';
2 import { createPageIterator } from '@proton/pass/lib/api/utils';
3 import { PassCrypto } from '@proton/pass/lib/crypto';
5 AliasAndItemCreateRequest,
8 CustomAliasCreateRequest,
9 ImportItemBatchRequest,
14 ItemMoveIndividualToShareRequest,
15 ItemMoveMultipleToShareRequest,
17 ItemRevisionContentsResponse,
20 ItemUpdateFlagsRequest,
22 } from '@proton/pass/types';
23 import { truthy } from '@proton/pass/utils/fp/predicates';
24 import { logger } from '@proton/pass/utils/logger';
25 import { getEpoch } from '@proton/pass/utils/time/epoch';
26 import identity from '@proton/utils/identity';
28 import { serializeItemContent } from './item-proto.transformer';
29 import { parseItemRevision } from './item.parser';
30 import { batchByShareId, intoRevisionID } from './item.utils';
32 /* Item creation API request for all items
33 * except for alias items */
34 export const createItem = async (
35 createIntent: ItemCreateIntent<Exclude<ItemType, 'alias'>>
36 ): Promise<ItemRevisionContentsResponse> => {
37 const { shareId, ...item } = createIntent;
39 const content = serializeItemContent(item);
40 const data = await PassCrypto.createItem({ shareId, content });
42 const { Item } = await api({
43 url: `pass/v1/share/${shareId}/item`,
51 /* Specific alias item API request */
52 export const createAlias = async (createIntent: ItemCreateIntent<'alias'>): Promise<ItemRevisionContentsResponse> => {
53 const { shareId, ...item } = createIntent;
55 const content = serializeItemContent(item);
56 const encryptedItem = await PassCrypto.createItem({ shareId, content });
58 const data: CustomAliasCreateRequest = {
60 Prefix: item.extraData.prefix,
61 SignedSuffix: item.extraData.signedSuffix,
62 MailboxIDs: item.extraData.mailboxes.map(({ id }) => id),
65 const { Item } = await api({
66 url: `pass/v1/share/${shareId}/alias/custom`,
74 /* Specific item with alias API request: the first item
75 * returned will be the login item, followed by the alias */
76 export const createItemWithAlias = async (
77 createIntent: ItemCreateIntent<'login'> & { extraData: { withAlias: true } }
78 ): Promise<[ItemRevisionContentsResponse, ItemRevisionContentsResponse]> => {
79 const { shareId, ...item } = createIntent;
81 const loginItemContent = serializeItemContent(item);
82 const aliasItemContent = serializeItemContent(item.extraData.alias);
84 const encryptedLoginItem = await PassCrypto.createItem({ shareId, content: loginItemContent });
85 const encryptedAliasItem = await PassCrypto.createItem({ shareId, content: aliasItemContent });
87 const data: AliasAndItemCreateRequest = {
88 Item: encryptedLoginItem,
90 Prefix: item.extraData.alias.extraData.prefix,
91 SignedSuffix: item.extraData.alias.extraData.signedSuffix,
92 MailboxIDs: item.extraData.alias.extraData.mailboxes.map(({ id }) => id),
93 Item: encryptedAliasItem,
97 const { Bundle } = await api({
98 url: `pass/v1/share/${shareId}/item/with_alias`,
103 return [Bundle!.Item, Bundle!.Alias];
106 export const editItem = async (
107 editIntent: ItemEditIntent,
109 ): Promise<ItemRevisionContentsResponse> => {
110 const { shareId, itemId, ...item } = editIntent;
111 const content = serializeItemContent(item);
113 const latestItemKey = (
115 url: `pass/v1/share/${shareId}/item/${itemId}/key/latest`,
120 const data = await PassCrypto.updateItem({ shareId, content, lastRevision, latestItemKey });
122 const { Item } = await api({
123 url: `pass/v1/share/${shareId}/item/${itemId}`,
131 export const moveItem = async (
134 destinationShareId: string
135 ): Promise<ItemRevision> => {
136 const content = serializeItemContent(item.data);
137 const data = await PassCrypto.moveItem({ destinationShareId, content });
139 const encryptedItem = (
141 url: `pass/v1/share/${shareId}/item/${item.itemId}/share`,
147 return parseItemRevision(destinationShareId, encryptedItem);
150 export const moveItems = async (
151 items: ItemRevision[],
152 destinationShareId: string,
154 data: BatchItemRevisions & { movedItems: ItemRevision[]; destinationShareId: string },
158 ): Promise<ItemRevision[]> =>
161 batchByShareId(items, identity).map(async ({ shareId, items }) => {
162 const data: ItemMoveMultipleToShareRequest = {
163 ShareID: destinationShareId,
164 Items: await Promise.all(
165 items.map<Promise<ItemMoveIndividualToShareRequest>>(async (item) => {
166 const content = serializeItemContent(item.data);
170 Item: (await PassCrypto.moveItem({ destinationShareId, content })).Item,
171 /* TODO: add array of revisions to not lose history
172 * after moving an item to another vault */
179 const { Items = [] } = await api({ url: `pass/v1/share/${shareId}/item/share`, method: 'put', data });
180 const decryptedItems = Items!.map((encrypted) => parseItemRevision(destinationShareId, encrypted));
181 const movedItems = await Promise.all(decryptedItems);
183 onBatch?.({ batch: items, movedItems, shareId, destinationShareId }, (progress += movedItems.length));
189 export const trashItems = async (
190 items: ItemRevision[],
191 onBatch?: (data: BatchItemRevisionIDs, progress: number) => void,
196 batchByShareId(items, intoRevisionID).map(async ({ shareId, items: Items }) => {
197 const response = await api({
198 url: `pass/v1/share/${shareId}/item/trash`,
203 onBatch?.({ shareId, batch: Items }, (progress += Items.length));
207 ).flatMap(({ Items }) => Items ?? []);
209 export const restoreItems = async (
210 items: ItemRevision[],
211 onBatch?: (data: BatchItemRevisionIDs, progress: number) => void,
216 batchByShareId(items, intoRevisionID).map(async ({ shareId, items: Items }) => {
217 const response = await api({
218 url: `pass/v1/share/${shareId}/item/untrash`,
223 onBatch?.({ shareId, batch: Items }, (progress += Items.length));
227 ).flatMap(({ Items }) => Items ?? []);
229 export const deleteItems = async (
230 items: ItemRevision[],
231 onBatch?: (data: BatchItemRevisionIDs, progress: number) => void,
236 batchByShareId(items, intoRevisionID).map(async ({ shareId, items: Items }) => {
238 url: `pass/v1/share/${shareId}/item`,
243 onBatch?.({ shareId, batch: Items }, (progress += items.length));
249 export const updateItemLastUseTime = async (shareId: string, itemId: string) =>
252 url: `pass/v1/share/${shareId}/item/${itemId}/lastuse`,
254 data: { LastUseTime: getEpoch() },
258 export const requestAllItemsForShareId = async (
259 options: { shareId: string; OnlyAlias?: boolean },
260 onBatch?: (progress: number) => void
261 ): Promise<ItemRevisionContentsResponse[]> =>
264 request: async (Since) => {
265 const { Items } = await api({
266 url: `pass/v1/share/${options.shareId}/item`,
268 params: Since ? { Since, OnlyAlias: options.OnlyAlias } : {},
271 return { data: Items?.RevisionsData ?? [], cursor: Items?.LastToken };
275 /** Will not throw on decryption errors : this avoids blocking the
276 * user if one item is corrupted or is using a newer proto version */
277 export async function requestItemsForShareId(
279 onBatch?: (progress: number) => void
280 ): Promise<ItemRevision[]> {
281 const encryptedItems = await requestAllItemsForShareId({ shareId }, onBatch);
282 const items = await Promise.all(encryptedItems.map((item) => parseItemRevision(shareId, item).catch(() => null)));
283 return items.filter(truthy);
286 export const importItemsBatch = async (options: {
288 importIntents: ItemImportIntent[];
289 onSkippedItem?: (skipped: ItemImportIntent) => void;
290 }): Promise<ItemRevisionContentsResponse[]> => {
291 const { shareId, importIntents, onSkippedItem } = options;
292 const data: ImportItemBatchRequest = {
295 importIntents.map(async (importIntent): Promise<Maybe<ImportItemRequest>> => {
296 const { trashed, createTime, modifyTime, ...item } = importIntent;
300 Item: await PassCrypto.createItem({ shareId, content: serializeItemContent(item) }),
301 AliasEmail: item.type === 'alias' ? item.extraData.aliasEmail : null,
303 CreateTime: createTime ?? null,
304 ModifyTime: modifyTime ?? null,
307 logger.info(`[Import] could not import "${item.metadata.name}"`);
308 onSkippedItem?.(importIntent);
316 if (data.Items.length === 0) return [];
318 const result = await api({
319 url: `pass/v1/share/${shareId}/item/import/batch`,
324 if (result.Revisions?.RevisionsData === undefined) {
325 throw new Error(`Error while batch importing data`);
328 return result.Revisions.RevisionsData;
331 /** Update the item monitoring flag */
332 export const updateItemFlags = async (
335 data: ItemUpdateFlagsRequest
336 ): Promise<ItemRevisionContentsResponse> =>
337 (await api({ url: `pass/v1/share/${shareId}/item/${itemId}/flags`, method: 'put', data })).Item!;
339 export const pinItem = (shareId: string, itemId: string) =>
340 api({ url: `pass/v1/share/${shareId}/item/${itemId}/pin`, method: 'post' });
342 export const unpinItem = (shareId: string, itemId: string) =>
343 api({ url: `pass/v1/share/${shareId}/item/${itemId}/pin`, method: 'delete' });
345 export const getItemRevisions = async (
346 { shareId, itemId, pageSize, since }: ItemRevisionsIntent,
351 url: `pass/v1/share/${shareId}/item/${itemId}/revision`,
352 params: { PageSize: pageSize, ...(since ? { Since: since } : {}) },