@pmate/chat-sdk 0.12.0-next.34 → 0.12.0-next.35
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.
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { StoredMessage, MessageScope } from "./messageStore.js";
|
|
2
|
+
export type MessageControl = {
|
|
3
|
+
scope: string;
|
|
4
|
+
thread: string;
|
|
5
|
+
target: string;
|
|
6
|
+
kind: "recall" | "reaction";
|
|
7
|
+
actor: string;
|
|
8
|
+
emoji: string;
|
|
9
|
+
id: string;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
revision: number;
|
|
12
|
+
active: boolean;
|
|
13
|
+
};
|
|
14
|
+
export type ControlKey = [string, string, string, string, string, string];
|
|
15
|
+
export declare const controlKey: (event: MessageControl) => ControlKey;
|
|
16
|
+
export declare function newerControl(old: MessageControl | undefined, next: MessageControl): MessageControl;
|
|
17
|
+
/** Body-free proofs retained independently of the visible history window. */
|
|
18
|
+
export declare function messageControls(message: StoredMessage, scope: MessageScope, key: string): MessageControl[];
|
|
19
|
+
export declare function applyMessageControls<T extends StoredMessage>(messages: T[], controls: readonly MessageControl[]): T[];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const controlKey = (event) => [event.scope, event.thread, event.target, event.kind, event.actor, event.emoji];
|
|
2
|
+
export function newerControl(old, next) {
|
|
3
|
+
return !old || next.revision > old.revision || next.revision === old.revision && (next.timestamp > old.timestamp || next.timestamp === old.timestamp && next.id > old.id) ? next : old;
|
|
4
|
+
}
|
|
5
|
+
const identifier = (value) => typeof value === "string" && value.length > 0 && value.length <= 256;
|
|
6
|
+
/** Body-free proofs retained independently of the visible history window. */
|
|
7
|
+
export function messageControls(message, scope, key) {
|
|
8
|
+
const row = message;
|
|
9
|
+
if (row.appId !== undefined && row.appId !== scope.appId || row.teamId !== undefined && row.teamId !== scope.teamId || !identifier(row.from))
|
|
10
|
+
return [];
|
|
11
|
+
const apps = row.body;
|
|
12
|
+
const app = apps?.content?.appId ?? apps?.payload?.appId;
|
|
13
|
+
if (app !== undefined && app !== scope.appId)
|
|
14
|
+
return [];
|
|
15
|
+
if (row.recalled || row.legacyDisplay?.recalled)
|
|
16
|
+
return [{ scope: key, thread: message.threadId, target: message.id, kind: "recall", actor: row.from, emoji: "", id: message.id, timestamp: message.timestamp, revision: 0, active: true }];
|
|
17
|
+
const body = row.body;
|
|
18
|
+
const content = body?.kind === "message" && body.content?.appId === scope.appId ? body.content : undefined;
|
|
19
|
+
const target = body?.kind === "recall" && row.appId === scope.appId ? body.targetMessageId : content?.recall?.messageId;
|
|
20
|
+
const base = { scope: key, thread: message.threadId, actor: row.from, id: message.id, timestamp: message.timestamp };
|
|
21
|
+
if (identifier(target))
|
|
22
|
+
return [{ ...base, target, kind: "recall", emoji: "", revision: 0, active: true }];
|
|
23
|
+
const reaction = content?.reaction;
|
|
24
|
+
if (body?.contentKind === "text" && reaction?.version === 1 && identifier(reaction.messageId) && identifier(reaction.emojiId) && Number.isSafeInteger(reaction.revision) && (reaction.revision ?? 0) >= 1 && typeof reaction.active === "boolean")
|
|
25
|
+
return [{ ...base, target: reaction.messageId, kind: "reaction", emoji: reaction.emojiId, revision: reaction.revision, active: reaction.active }];
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
export function applyMessageControls(messages, controls) {
|
|
29
|
+
const grouped = new Map();
|
|
30
|
+
for (const event of controls) {
|
|
31
|
+
const list = grouped.get(event.target) ?? [];
|
|
32
|
+
list.push(event);
|
|
33
|
+
grouped.set(event.target, list);
|
|
34
|
+
}
|
|
35
|
+
return messages.map(message => {
|
|
36
|
+
const facts = grouped.get(message.id);
|
|
37
|
+
if (!facts?.length)
|
|
38
|
+
return message;
|
|
39
|
+
const row = message;
|
|
40
|
+
const recalled = message.recalled || facts.some(event => event.kind === "recall" && event.actor === row.from);
|
|
41
|
+
const reactionEvents = facts.filter(event => event.kind === "reaction").map(event => ({ id: event.id, messageId: event.target, threadId: event.thread, from: event.actor, emojiId: event.emoji, active: event.active, revision: event.revision, timestamp: event.timestamp }));
|
|
42
|
+
return { ...message, ...(recalled ? { recalled: true } : {}), ...(reactionEvents.length ? { reactionEvents: [...(row.reactionEvents ?? []), ...reactionEvents] } : {}) };
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -33,12 +33,14 @@ export declare const messageScopeKey: (scope: MessageScope) => string;
|
|
|
33
33
|
/** Normalized browser cache. Data and coverage advance together in each commit.
|
|
34
34
|
* Merge runs inside the write transaction against the latest cross-tab state. */
|
|
35
35
|
export declare class IndexedDbMessageStore<T extends StoredMessage> {
|
|
36
|
+
private scope;
|
|
36
37
|
private merge;
|
|
37
38
|
private db;
|
|
38
39
|
private rows;
|
|
39
40
|
private legacy;
|
|
40
41
|
private coverage;
|
|
41
42
|
private accessRows;
|
|
43
|
+
private controls;
|
|
42
44
|
private key;
|
|
43
45
|
constructor(scope: MessageScope, merge: (previous: T | undefined, incoming: T) => T, options?: {
|
|
44
46
|
indexedDB?: IDBFactory;
|
|
@@ -69,6 +71,9 @@ export declare class IndexedDbMessageStore<T extends StoredMessage> {
|
|
|
69
71
|
authoritative?: boolean;
|
|
70
72
|
scopeVersion?: number;
|
|
71
73
|
}): Promise<number>;
|
|
74
|
+
private rememberControls;
|
|
75
|
+
private controlPage;
|
|
76
|
+
private finishPage;
|
|
72
77
|
windows(): Promise<CoverageRow[]>;
|
|
73
78
|
scopeAccess(): Promise<MessageScopeAccess>;
|
|
74
79
|
private scopedCoverage;
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { Dexie } from "dexie";
|
|
2
|
+
import { messageControls, controlKey, newerControl, applyMessageControls } from "./messageControls.js";
|
|
2
3
|
export const messageScopeKey = (scope) => JSON.stringify([scope.appId, scope.accountId, scope.profileId, scope.teamId]);
|
|
3
4
|
/** Normalized browser cache. Data and coverage advance together in each commit.
|
|
4
5
|
* Merge runs inside the write transaction against the latest cross-tab state. */
|
|
5
6
|
export class IndexedDbMessageStore {
|
|
7
|
+
scope;
|
|
6
8
|
merge;
|
|
7
9
|
db;
|
|
8
10
|
rows;
|
|
9
11
|
legacy;
|
|
10
12
|
coverage;
|
|
11
13
|
accessRows;
|
|
14
|
+
controls;
|
|
12
15
|
key;
|
|
13
16
|
constructor(scope, merge, options = {}) {
|
|
17
|
+
this.scope = scope;
|
|
14
18
|
this.merge = merge;
|
|
15
19
|
this.key = messageScopeKey(scope);
|
|
16
20
|
this.db = new Dexie(options.name ?? "pmate-chat-messages", { indexedDB: options.indexedDB ?? globalThis.indexedDB, IDBKeyRange: globalThis.IDBKeyRange });
|
|
17
21
|
this.db.version(1).stores({ messages: "[scope+thread+id],[scope+thread+time+id],[scope+thread],scope", coverage: "[scope+thread],scope" });
|
|
18
22
|
this.db.version(2).stores({ legacyMessages: "[scope+thread+id],[scope+thread+time+id],[scope+thread],scope" });
|
|
19
23
|
this.db.version(3).stores({ scopeAccess: "scope" });
|
|
24
|
+
this.db.version(4).stores({ messageControls: "[scope+thread+target+kind+actor+emoji],[scope+thread+target],[scope+thread],scope" });
|
|
25
|
+
this.controls = this.db.table("messageControls");
|
|
20
26
|
this.accessRows = this.db.table("scopeAccess");
|
|
21
27
|
this.legacy = this.db.table("legacyMessages");
|
|
22
28
|
this.rows = this.db.table("messages");
|
|
@@ -27,14 +33,15 @@ export class IndexedDbMessageStore {
|
|
|
27
33
|
async read(thread, limit = 100, before) {
|
|
28
34
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000)
|
|
29
35
|
throw new Error("INVALID_MESSAGE_LIMIT");
|
|
30
|
-
|
|
36
|
+
const page = await this.db.transaction("r", this.rows, this.coverage, this.accessRows, this.controls, async () => {
|
|
31
37
|
const access = await this.scopeAccess();
|
|
32
38
|
if (access.revoked)
|
|
33
39
|
return { messages: [], hasEarlier: false, coverage: { complete: false, revoked: true, resetVersion: 0, scopeVersion: access.version } };
|
|
34
40
|
const range = this.rows.where("[scope+thread+time+id]").between([this.key, thread, Dexie.minKey, Dexie.minKey], [this.key, thread, before?.timestamp ?? Dexie.maxKey, before?.id ?? Dexie.maxKey], true, !before);
|
|
35
41
|
const records = await range.reverse().limit(limit + 1).toArray();
|
|
36
|
-
return { messages: records.slice(0, limit).reverse().map(row => row.value), hasEarlier: records.length > limit, coverage: this.scopedCoverage(await this.coverage.get([this.key, thread]), access) };
|
|
42
|
+
return this.controlPage({ messages: records.slice(0, limit).reverse().map(row => row.value), hasEarlier: records.length > limit, coverage: this.scopedCoverage(await this.coverage.get([this.key, thread]), access) });
|
|
37
43
|
});
|
|
44
|
+
return this.finishPage(thread, page);
|
|
38
45
|
}
|
|
39
46
|
/** Migration records never claim coverage or overwrite authoritative rows. */
|
|
40
47
|
async importLegacy(thread, messages) {
|
|
@@ -48,13 +55,19 @@ export class IndexedDbMessageStore {
|
|
|
48
55
|
if (bytes > 1_048_576)
|
|
49
56
|
throw new Error("MESSAGE_BATCH_TOO_LARGE");
|
|
50
57
|
}
|
|
51
|
-
return this.db.transaction("rw", this.rows, this.legacy, this.coverage, this.accessRows, async () => {
|
|
58
|
+
return this.db.transaction("rw", [this.rows, this.legacy, this.coverage, this.accessRows, this.controls], async () => {
|
|
52
59
|
const access = await this.scopeAccess();
|
|
53
60
|
const coverage = await this.coverage.get([this.key, thread]);
|
|
54
61
|
if (access.revoked || coverage?.revoked)
|
|
55
62
|
throw new Error("MESSAGE_ACCESS_REVOKED");
|
|
56
63
|
if (access.version > 0)
|
|
57
64
|
throw new Error("MESSAGE_PROJECTION_CHANGED");
|
|
65
|
+
const proofs = messages.map(message => messageControls(message, this.scope, this.key));
|
|
66
|
+
const controlsOnly = messages.length > 0 && proofs.every((events, index) => events.length > 0 && !messages[index].recalled && !messages[index].legacyDisplay?.recalled);
|
|
67
|
+
if (controlsOnly) {
|
|
68
|
+
await this.rememberControls(proofs.flat());
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
58
71
|
if ((coverage?.resetVersion ?? 0) > 0)
|
|
59
72
|
throw new Error("MESSAGE_PROJECTION_CHANGED");
|
|
60
73
|
const ids = [...new Set(messages.map(message => message.id))], keys = ids.map(id => [this.key, thread, id]);
|
|
@@ -71,6 +84,7 @@ export class IndexedDbMessageStore {
|
|
|
71
84
|
if (JSON.stringify(old) !== JSON.stringify(value))
|
|
72
85
|
changed.set(message.id, { scope: this.key, thread, id: value.id, time: value.timestamp, value });
|
|
73
86
|
}
|
|
87
|
+
await this.rememberControls([...changed.values()].flatMap(row => messageControls(row.value, this.scope, this.key)));
|
|
74
88
|
if (changed.size)
|
|
75
89
|
await this.legacy.bulkPut([...changed.values()]);
|
|
76
90
|
return changed.size;
|
|
@@ -80,7 +94,7 @@ export class IndexedDbMessageStore {
|
|
|
80
94
|
async readFallback(thread, limit = 100, before) {
|
|
81
95
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000)
|
|
82
96
|
throw new Error("INVALID_MESSAGE_LIMIT");
|
|
83
|
-
|
|
97
|
+
const page = await this.db.transaction("r", [this.rows, this.legacy, this.coverage, this.accessRows, this.controls], async () => {
|
|
84
98
|
const access = await this.scopeAccess();
|
|
85
99
|
const coverage = access.revoked ? { complete: false, revoked: true, resetVersion: 0, scopeVersion: access.version } : this.scopedCoverage(await this.coverage.get([this.key, thread]), access);
|
|
86
100
|
if (coverage?.revoked)
|
|
@@ -88,8 +102,9 @@ export class IndexedDbMessageStore {
|
|
|
88
102
|
const read = (table) => table.where("[scope+thread+time+id]").between([this.key, thread, Dexie.minKey, Dexie.minKey], [this.key, thread, before?.timestamp ?? Dexie.maxKey, before?.id ?? Dexie.maxKey], true, !before).reverse().limit(limit + 1).toArray();
|
|
89
103
|
const [canonical, legacy] = await Promise.all([read(this.rows), read(this.legacy)]);
|
|
90
104
|
const rows = [...new Map([...legacy, ...canonical].map(row => [row.id, row])).values()].sort((a, b) => a.time - b.time || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
91
|
-
return { messages: rows.slice(-limit).map(row => row.value), hasEarlier: rows.length > limit, coverage };
|
|
105
|
+
return this.controlPage({ messages: rows.slice(-limit).map(row => row.value), hasEarlier: rows.length > limit, coverage });
|
|
92
106
|
});
|
|
107
|
+
return this.finishPage(thread, page);
|
|
93
108
|
}
|
|
94
109
|
async commit(thread, messages, coverage, guard) {
|
|
95
110
|
for (const message of messages)
|
|
@@ -97,7 +112,7 @@ export class IndexedDbMessageStore {
|
|
|
97
112
|
throw new Error("MESSAGE_SCOPE_MISMATCH");
|
|
98
113
|
if (messages.length > 1000)
|
|
99
114
|
throw new Error("MESSAGE_BATCH_TOO_LARGE");
|
|
100
|
-
return this.db.transaction("rw", this.rows, this.legacy, this.coverage, this.accessRows, async () => {
|
|
115
|
+
return this.db.transaction("rw", [this.rows, this.legacy, this.coverage, this.accessRows, this.controls], async () => {
|
|
101
116
|
const access = await this.scopeAccess();
|
|
102
117
|
if ((guard?.scopeVersion ?? 0) !== access.version)
|
|
103
118
|
throw new Error("MESSAGE_SCOPE_CHANGED");
|
|
@@ -126,6 +141,7 @@ export class IndexedDbMessageStore {
|
|
|
126
141
|
if (!canonicalIds.has(message.id) || JSON.stringify(old) !== JSON.stringify(value))
|
|
127
142
|
changed.set(message.id, { scope: this.key, thread, id: value.id, time: value.timestamp, value });
|
|
128
143
|
}
|
|
144
|
+
await this.rememberControls(messages.flatMap(message => messageControls(message, this.scope, this.key)));
|
|
129
145
|
if (changed.size)
|
|
130
146
|
await this.rows.bulkPut([...changed.values()]);
|
|
131
147
|
const replaced = legacy.flatMap(row => row ? [[this.key, thread, row.id]] : []);
|
|
@@ -139,6 +155,47 @@ export class IndexedDbMessageStore {
|
|
|
139
155
|
return changed.size;
|
|
140
156
|
});
|
|
141
157
|
}
|
|
158
|
+
async rememberControls(events) {
|
|
159
|
+
const latest = new Map();
|
|
160
|
+
for (const event of events) {
|
|
161
|
+
const key = JSON.stringify(controlKey(event));
|
|
162
|
+
latest.set(key, newerControl(latest.get(key), event));
|
|
163
|
+
}
|
|
164
|
+
const values = [...latest.values()];
|
|
165
|
+
for (let offset = 0; offset < values.length; offset += 128) {
|
|
166
|
+
const batch = values.slice(offset, offset + 128), previous = await this.controls.bulkGet(batch.map(controlKey));
|
|
167
|
+
const changed = batch.filter((event, index) => newerControl(previous[index], event) !== previous[index]);
|
|
168
|
+
if (changed.length)
|
|
169
|
+
await this.controls.bulkPut(changed);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async controlPage(page) {
|
|
173
|
+
const events = page.messages.flatMap(message => messageControls(message, this.scope, this.key));
|
|
174
|
+
const old = events.length ? await this.controls.bulkGet(events.map(controlKey)) : [];
|
|
175
|
+
const pendingControls = events.filter((event, index) => newerControl(old[index], event) !== old[index]);
|
|
176
|
+
const facts = await this.controls.where("[scope+thread+target]").anyOf(page.messages.map(message => [this.key, message.threadId, message.id])).toArray();
|
|
177
|
+
const latest = new Map();
|
|
178
|
+
for (const event of [...facts, ...events]) {
|
|
179
|
+
const key = JSON.stringify(controlKey(event));
|
|
180
|
+
latest.set(key, newerControl(latest.get(key), event));
|
|
181
|
+
}
|
|
182
|
+
return { ...page, messages: applyMessageControls(page.messages, [...latest.values()]), pendingControls };
|
|
183
|
+
}
|
|
184
|
+
async finishPage(thread, page) {
|
|
185
|
+
const { pendingControls, ...result } = page;
|
|
186
|
+
// v3 caches migrate only source proofs from already-read pages, with no body
|
|
187
|
+
// rewrite. Steady reads remain a single read-only IndexedDB transaction.
|
|
188
|
+
if (pendingControls?.length)
|
|
189
|
+
await this.db.transaction("rw", this.controls, this.coverage, this.accessRows, async () => {
|
|
190
|
+
const access = await this.scopeAccess(), current = await this.coverage.get([this.key, thread]);
|
|
191
|
+
if (access.revoked || access.version !== (page.coverage?.scopeVersion ?? 0))
|
|
192
|
+
throw new Error("MESSAGE_SCOPE_CHANGED");
|
|
193
|
+
if ((current?.resetVersion ?? 0) !== (page.coverage?.resetVersion ?? 0))
|
|
194
|
+
throw new Error("MESSAGE_PROJECTION_CHANGED");
|
|
195
|
+
await this.rememberControls(pendingControls);
|
|
196
|
+
});
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
142
199
|
async windows() {
|
|
143
200
|
return this.db.transaction("r", this.coverage, this.accessRows, async () => {
|
|
144
201
|
if ((await this.scopeAccess()).revoked)
|
|
@@ -156,6 +213,7 @@ export class IndexedDbMessageStore {
|
|
|
156
213
|
return access.version ? { ...coverage, complete: coverage?.complete ?? false, scopeVersion: access.version } : coverage;
|
|
157
214
|
}
|
|
158
215
|
async clearScopeRows() {
|
|
216
|
+
await this.controls.where("scope").equals(this.key).delete();
|
|
159
217
|
await this.rows.where("[scope+thread+id]").between([this.key, Dexie.minKey, Dexie.minKey], [this.key, Dexie.maxKey, Dexie.maxKey], true, true).delete();
|
|
160
218
|
await this.legacy.where("[scope+thread+id]").between([this.key, Dexie.minKey, Dexie.minKey], [this.key, Dexie.maxKey, Dexie.maxKey], true, true).delete();
|
|
161
219
|
await this.coverage.where("[scope+thread]").between([this.key, Dexie.minKey], [this.key, Dexie.maxKey], true, true).delete();
|
|
@@ -170,7 +228,7 @@ export class IndexedDbMessageStore {
|
|
|
170
228
|
await this.accessRows.put(next);
|
|
171
229
|
return next;
|
|
172
230
|
});
|
|
173
|
-
await this.db.transaction("rw", this.rows, this.legacy, this.coverage, this.accessRows, async () => {
|
|
231
|
+
await this.db.transaction("rw", [this.rows, this.legacy, this.coverage, this.accessRows, this.controls], async () => {
|
|
174
232
|
const current = await this.scopeAccess();
|
|
175
233
|
if (current.version === access.version && current.revoked)
|
|
176
234
|
await this.clearScopeRows();
|
|
@@ -179,7 +237,7 @@ export class IndexedDbMessageStore {
|
|
|
179
237
|
}
|
|
180
238
|
/** A durable epoch prevents another tab's pre-reset request from restoring rows. */
|
|
181
239
|
async resetThread(thread, revoked = false, scopeVersion = 0) {
|
|
182
|
-
return this.db.transaction("rw", this.rows, this.legacy, this.coverage, this.accessRows, async () => {
|
|
240
|
+
return this.db.transaction("rw", [this.rows, this.legacy, this.coverage, this.accessRows, this.controls], async () => {
|
|
183
241
|
const access = await this.scopeAccess();
|
|
184
242
|
if (access.version !== scopeVersion)
|
|
185
243
|
throw new Error("MESSAGE_SCOPE_CHANGED");
|
|
@@ -187,6 +245,8 @@ export class IndexedDbMessageStore {
|
|
|
187
245
|
throw new Error("MESSAGE_ACCESS_REVOKED");
|
|
188
246
|
const old = await this.coverage.get([this.key, thread]);
|
|
189
247
|
const resetVersion = (old?.resetVersion ?? 0) + 1;
|
|
248
|
+
if (revoked)
|
|
249
|
+
await this.controls.where("[scope+thread]").equals([this.key, thread]).delete();
|
|
190
250
|
await this.rows.where("[scope+thread]").equals([this.key, thread]).delete();
|
|
191
251
|
await this.legacy.where("[scope+thread]").equals([this.key, thread]).delete();
|
|
192
252
|
await this.coverage.put({ scope: this.key, thread, complete: false, resetVersion, revoked });
|
|
@@ -194,14 +254,16 @@ export class IndexedDbMessageStore {
|
|
|
194
254
|
});
|
|
195
255
|
}
|
|
196
256
|
async removeThread(thread) {
|
|
197
|
-
await this.db.transaction("rw", this.rows, this.legacy, this.coverage, this.accessRows, async () => {
|
|
257
|
+
await this.db.transaction("rw", [this.rows, this.legacy, this.coverage, this.accessRows, this.controls], async () => {
|
|
258
|
+
await this.controls.where("[scope+thread]").equals([this.key, thread]).delete();
|
|
198
259
|
await this.rows.where("[scope+thread]").equals([this.key, thread]).delete();
|
|
199
260
|
await this.legacy.where("[scope+thread]").equals([this.key, thread]).delete();
|
|
200
261
|
await this.coverage.delete([this.key, thread]);
|
|
201
262
|
});
|
|
202
263
|
}
|
|
203
264
|
async clear() {
|
|
204
|
-
await this.db.transaction("rw", this.rows, this.legacy, this.coverage, this.accessRows, async () => {
|
|
265
|
+
await this.db.transaction("rw", [this.rows, this.legacy, this.coverage, this.accessRows, this.controls], async () => {
|
|
266
|
+
await this.controls.where("scope").equals(this.key).delete();
|
|
205
267
|
await this.legacy.where("scope").equals(this.key).delete();
|
|
206
268
|
await this.rows.where("scope").equals(this.key).delete();
|
|
207
269
|
await this.coverage.where("scope").equals(this.key).delete();
|
package/package.json
CHANGED