@ours.network/notifications 0.3.1-nightly.3 → 0.3.1-nightly.4
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/README.md +5 -3
- package/dist/chat-target.d.ts +6 -7
- package/dist/chat-target.js +27 -8
- package/dist/store.d.ts +3 -4
- package/dist/store.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,11 +56,11 @@ Responses are never cached. User and producer credentials have separate scopes.
|
|
|
56
56
|
| GET `/api/v1/devices/status?id=...` | User | `{ "active": true }` |
|
|
57
57
|
| GET `/api/v1/inbox?limit=50&before=...` | User | Items, shared unread count, revision, next cursor |
|
|
58
58
|
| GET `/api/v1/summary` | User | Shared unread count, revision, per-chat source/identity/count, and `sections: {chats, tasks}` |
|
|
59
|
-
| POST `/api/v1/read-target` | User | `{ "url": "/fleet/chats
|
|
59
|
+
| POST `/api/v1/read-target` | User | `{ "url": "/fleet/chats/agents/agent", "revision": OBSERVED_REVISION }`, reads only this chat up to the revision captured before its history load |
|
|
60
60
|
| POST `/api/v1/read` | User | `{ "ids": ["notification-id"] }`, idempotent shared read |
|
|
61
61
|
| POST `/api/v1/delete` | User | `{ "ids": ["notification-id"] }` → `{ "removed": COUNT }`; persistently removes only this user’s entries and queued deliveries after the client has confirmed target absence |
|
|
62
|
-
| POST `/api/v1/delete-target` | Producer | `{ "url": "/fleet/chats
|
|
63
|
-
| POST `/api/v1/send` | Producer | `{ "eventId": "stable-event-id", "title": "Agent completed", "body": "Open the chat", "url": "/fleet/chats
|
|
62
|
+
| POST `/api/v1/delete-target` | Producer | `{ "url": "/fleet/chats/agents/agent" }` → `{ "removed": COUNT }`; only its credential’s user/source and exact canonical conversation, including filter/anchor variants |
|
|
63
|
+
| POST `/api/v1/send` | Producer | `{ "eventId": "stable-event-id", "title": "Agent completed", "body": "Open the chat", "url": "/fleet/chats/agents/agent" }` |
|
|
64
64
|
|
|
65
65
|
Inbox admission and queued delivery are one fsynced atomic snapshot. The service
|
|
66
66
|
deduplicates by user/source/event ID; replay must retain identical title/body/URL
|
|
@@ -171,3 +171,5 @@ an inbox item is deleted, preventing retry resurrection. Legacy inbox entries ar
|
|
|
171
171
|
admitted as receipts on load. The existing inbox/job caps remain; a separate cap of
|
|
172
172
|
100,000 receipts per account rejects new events explicitly rather than evicting
|
|
173
173
|
dedupe evidence. Upgrades must preserve the service's state file and scoped credentials.
|
|
174
|
+
|
|
175
|
+
Conversation notification links use canonical contact (`/fleet/chats/contacts/:id`), agent (`/fleet/chats/agents/:id`), and task room (`/fleet/tasks/:taskId/room` or `/fleet/tasks/:taskId/rooms/:key`) paths. Deploy producers, this service, and the app together. Summary rows retain `source`/`chat` and include `kind` (`contact`, `agent`, or `room`) to distinguish colliding IDs. Filters and message anchors do not change target identity. Old query-based conversation URLs are intentionally unsupported; stored payload URLs are neither converted nor deleted by this change. Existing navigation-target security restrictions still apply.
|
package/dist/chat-target.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export interface ChatTarget {
|
|
2
|
+
kind: 'contact' | 'agent' | 'room';
|
|
3
3
|
source: 'work' | 'messenger';
|
|
4
4
|
chat: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}): string;
|
|
5
|
+
}
|
|
6
|
+
/** Stable conversation identity, independent of list filters and message anchors. */
|
|
7
|
+
export declare function chatTarget(path: string): ChatTarget | null;
|
|
8
|
+
export declare function chatKey(value: ChatTarget): string;
|
package/dist/chat-target.js
CHANGED
|
@@ -1,15 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
import { target as navigationTarget } from './validation.js';
|
|
2
|
+
/** Stable conversation identity, independent of list filters and message anchors. */
|
|
2
3
|
export function chatTarget(path) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
if (
|
|
4
|
+
try {
|
|
5
|
+
navigationTarget(path);
|
|
6
|
+
const url = new URL(path, 'https://ours.invalid');
|
|
7
|
+
if (['source', 'chat', 'task', 'detail'].some(key => url.searchParams.has(key)))
|
|
7
8
|
return null;
|
|
8
|
-
|
|
9
|
+
// Match the original path: URL normalization must not turn malformed arity into a target.
|
|
10
|
+
const parts = path.split(/[?#]/, 1)[0].split('/').slice(1).map(decodeURIComponent);
|
|
11
|
+
if (parts.some(part => !part || /[\x00-\x1f\x7f]/.test(part) || part === '.' || part === '..'))
|
|
12
|
+
return null;
|
|
13
|
+
if (parts.length === 4 && parts[0] === 'fleet' && parts[1] === 'chats') {
|
|
14
|
+
if (parts[2] === 'contacts')
|
|
15
|
+
return { kind: 'contact', source: 'messenger', chat: parts[3] };
|
|
16
|
+
if (parts[2] === 'agents')
|
|
17
|
+
return { kind: 'agent', source: 'work', chat: parts[3] };
|
|
18
|
+
}
|
|
19
|
+
if (parts[0] === 'fleet' && parts[1] === 'tasks' && /^[A-Za-z0-9_-]{1,120}$/.test(parts[2] ?? '')) {
|
|
20
|
+
if (parts.length === 4 && parts[3] === 'room')
|
|
21
|
+
return { kind: 'room', source: 'work', chat: `room-${parts[2]}` };
|
|
22
|
+
if (parts.length === 5 && parts[3] === 'rooms' && /^[A-Za-z][A-Za-z0-9_-]*$/.test(parts[4])
|
|
23
|
+
&& !['constructor', 'prototype', '__proto__'].includes(parts[4])) {
|
|
24
|
+
return { kind: 'room', source: 'work', chat: `room-${parts[2]}:${parts[4]}` };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
9
27
|
}
|
|
28
|
+
catch { /* Unsupported persisted input is unknown, never a missing target. */ }
|
|
10
29
|
return null;
|
|
11
30
|
}
|
|
12
31
|
export function chatKey(value) {
|
|
13
|
-
const chat = value.
|
|
14
|
-
return JSON.stringify([value.source, chat]);
|
|
32
|
+
const chat = value.kind === 'contact' && /^[a-f0-9]{64}$/i.test(value.chat) ? value.chat.toUpperCase() : value.chat;
|
|
33
|
+
return JSON.stringify([value.kind, value.source, chat]);
|
|
15
34
|
}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ChatTarget } from './chat-target.js';
|
|
1
2
|
import type { PushSubscription } from 'web-push';
|
|
2
3
|
export interface Notification {
|
|
3
4
|
id: string;
|
|
@@ -51,11 +52,9 @@ export declare class Store {
|
|
|
51
52
|
summary(userId: string): {
|
|
52
53
|
unread: number;
|
|
53
54
|
revision: number;
|
|
54
|
-
chats: {
|
|
55
|
-
source: "work" | "messenger";
|
|
56
|
-
chat: string;
|
|
55
|
+
chats: (ChatTarget & {
|
|
57
56
|
count: number;
|
|
58
|
-
}[];
|
|
57
|
+
})[];
|
|
59
58
|
sections: {
|
|
60
59
|
chats: number;
|
|
61
60
|
tasks: number;
|
package/dist/store.js
CHANGED
|
@@ -114,7 +114,7 @@ export class Store {
|
|
|
114
114
|
if (/^\/fleet\/tasks(?:\/[^/]+)?\/?$/.test(url.pathname))
|
|
115
115
|
sections.tasks++;
|
|
116
116
|
else if (targetForSection) {
|
|
117
|
-
if (typeof n.taskId === 'string' || targetForSection.
|
|
117
|
+
if (typeof n.taskId === 'string' || targetForSection.kind === 'room')
|
|
118
118
|
sections.tasks++;
|
|
119
119
|
else if (targetForSection.source === 'messenger')
|
|
120
120
|
sections.chats++;
|
package/package.json
CHANGED