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,
176 const { Items = [] } = await api({ url: `pass/v1/share/${shareId}/item/share`, method: 'put', data });
177 const decryptedItems = Items!.map((encrypted) => parseItemRevision(destinationShareId, encrypted));
178 const movedItems = await Promise.all(decryptedItems);
180 onBatch?.({ batch: items, movedItems, shareId, destinationShareId }, (progress += movedItems.length));
186 export const trashItems = async (
187 items: ItemRevision[],
188 onBatch?: (data: BatchItemRevisionIDs, progress: number) => void,
193 batchByShareId(items, intoRevisionID).map(async ({ shareId, items: Items }) => {
194 const response = await api({
195 url: `pass/v1/share/${shareId}/item/trash`,
200 onBatch?.({ shareId, batch: Items }, (progress += Items.length));
204 ).flatMap(({ Items }) => Items ?? []);
206 export const restoreItems = async (
207 items: ItemRevision[],
208 onBatch?: (data: BatchItemRevisionIDs, progress: number) => void,
213 batchByShareId(items, intoRevisionID).map(async ({ shareId, items: Items }) => {
214 const response = await api({
215 url: `pass/v1/share/${shareId}/item/untrash`,
220 onBatch?.({ shareId, batch: Items }, (progress += Items.length));
224 ).flatMap(({ Items }) => Items ?? []);
226 export const deleteItems = async (
227 items: ItemRevision[],
228 onBatch?: (data: BatchItemRevisionIDs, progress: number) => void,
233 batchByShareId(items, intoRevisionID).map(async ({ shareId, items: Items }) => {
235 url: `pass/v1/share/${shareId}/item`,
240 onBatch?.({ shareId, batch: Items }, (progress += items.length));
246 export const updateItemLastUseTime = async (shareId: string, itemId: string) =>
249 url: `pass/v1/share/${shareId}/item/${itemId}/lastuse`,
251 data: { LastUseTime: getEpoch() },
255 export const requestAllItemsForShareId = async (
256 options: { shareId: string; OnlyAlias?: boolean },
257 onBatch?: (progress: number) => void
258 ): Promise<ItemRevisionContentsResponse[]> =>
261 request: async (Since) => {
262 const { Items } = await api({
263 url: `pass/v1/share/${options.shareId}/item`,
265 params: Since ? { Since, OnlyAlias: options.OnlyAlias } : {},
268 return { data: Items?.RevisionsData ?? [], cursor: Items?.LastToken };
272 /** Will not throw on decryption errors : this avoids blocking the
273 * user if one item is corrupted or is using a newer proto version */
274 export async function requestItemsForShareId(
276 onBatch?: (progress: number) => void
277 ): Promise<ItemRevision[]> {
278 const encryptedItems = await requestAllItemsForShareId({ shareId }, onBatch);
279 const items = await Promise.all(encryptedItems.map((item) => parseItemRevision(shareId, item).catch(() => null)));
280 return items.filter(truthy);
283 export const importItemsBatch = async (options: {
285 importIntents: ItemImportIntent[];
286 onSkippedItem?: (skipped: ItemImportIntent) => void;
287 }): Promise<ItemRevisionContentsResponse[]> => {
288 const { shareId, importIntents, onSkippedItem } = options;
289 const data: ImportItemBatchRequest = {
292 importIntents.map(async (importIntent): Promise<Maybe<ImportItemRequest>> => {
293 const { trashed, createTime, modifyTime, ...item } = importIntent;
297 Item: await PassCrypto.createItem({ shareId, content: serializeItemContent(item) }),
298 AliasEmail: item.type === 'alias' ? item.extraData.aliasEmail : null,
300 CreateTime: createTime ?? null,
301 ModifyTime: modifyTime ?? null,
304 logger.info(`[Import] could not import "${item.metadata.name}"`);
305 onSkippedItem?.(importIntent);
313 if (data.Items.length === 0) return [];
315 const result = await api({
316 url: `pass/v1/share/${shareId}/item/import/batch`,
321 if (result.Revisions?.RevisionsData === undefined) {
322 throw new Error(`Error while batch importing data`);
325 return result.Revisions.RevisionsData;
328 /** Update the item monitoring flag */
329 export const updateItemFlags = async (
332 data: ItemUpdateFlagsRequest
333 ): Promise<ItemRevisionContentsResponse> =>
334 (await api({ url: `pass/v1/share/${shareId}/item/${itemId}/flags`, method: 'put', data })).Item!;
336 export const pinItem = (shareId: string, itemId: string) =>
337 api({ url: `pass/v1/share/${shareId}/item/${itemId}/pin`, method: 'post' });
339 export const unpinItem = (shareId: string, itemId: string) =>
340 api({ url: `pass/v1/share/${shareId}/item/${itemId}/pin`, method: 'delete' });
342 export const getItemRevisions = async (
343 { shareId, itemId, pageSize, since }: ItemRevisionsIntent,
348 url: `pass/v1/share/${shareId}/item/${itemId}/revision`,
349 params: { PageSize: pageSize, ...(since ? { Since: since } : {}) },