@xtremax/pigeon 0.0.0-stage → 0.3.1

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.
Files changed (104) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +68 -2
  3. package/bin/pigeon.mjs +92 -0
  4. package/dist/cjs/browser-stub.js +10 -0
  5. package/dist/cjs/client.js +112 -0
  6. package/dist/cjs/contract.js +5 -0
  7. package/dist/cjs/errors.js +48 -0
  8. package/dist/cjs/events.js +62 -0
  9. package/dist/cjs/idempotency.js +14 -0
  10. package/dist/cjs/inbox/browser-stub.js +10 -0
  11. package/dist/cjs/inbox/client.js +39 -0
  12. package/dist/cjs/inbox/errors.js +44 -0
  13. package/dist/cjs/inbox/handler.js +127 -0
  14. package/dist/cjs/inbox/index.js +15 -0
  15. package/dist/cjs/inbox/origin.js +22 -0
  16. package/dist/cjs/inbox/project.js +84 -0
  17. package/dist/cjs/inbox/validate.js +59 -0
  18. package/dist/cjs/inbox-react/format.js +72 -0
  19. package/dist/cjs/inbox-react/inbox-client.js +63 -0
  20. package/dist/cjs/inbox-react/inbox-context.js +13 -0
  21. package/dist/cjs/inbox-react/inbox-full-list.js +13 -0
  22. package/dist/cjs/inbox-react/inbox-list.js +37 -0
  23. package/dist/cjs/inbox-react/inbox-panel.js +13 -0
  24. package/dist/cjs/inbox-react/inbox-provider.js +157 -0
  25. package/dist/cjs/inbox-react/inbox-state.js +88 -0
  26. package/dist/cjs/inbox-react/index.js +25 -0
  27. package/dist/cjs/inbox-react/labels.js +38 -0
  28. package/dist/cjs/inbox-react/notification-bell.js +32 -0
  29. package/dist/cjs/inbox-react/notification-item.js +17 -0
  30. package/dist/cjs/inbox-react/parse.js +94 -0
  31. package/dist/cjs/inbox-react/preference-dialog.js +49 -0
  32. package/dist/cjs/inbox-react/types.js +4 -0
  33. package/dist/cjs/inbox-react/use-inbox-polling.js +94 -0
  34. package/dist/cjs/index.js +14 -0
  35. package/dist/cjs/package.json +1 -0
  36. package/dist/cjs/retry.js +14 -0
  37. package/dist/esm/browser-stub.js +6 -0
  38. package/dist/esm/client.js +107 -0
  39. package/dist/esm/contract.js +2 -0
  40. package/dist/esm/errors.js +42 -0
  41. package/dist/esm/events.js +57 -0
  42. package/dist/esm/idempotency.js +10 -0
  43. package/dist/esm/inbox/browser-stub.js +7 -0
  44. package/dist/esm/inbox/client.js +35 -0
  45. package/dist/esm/inbox/errors.js +38 -0
  46. package/dist/esm/inbox/handler.js +124 -0
  47. package/dist/esm/inbox/index.js +5 -0
  48. package/dist/esm/inbox/origin.js +19 -0
  49. package/dist/esm/inbox/project.js +76 -0
  50. package/dist/esm/inbox/validate.js +49 -0
  51. package/dist/esm/inbox-react/format.js +64 -0
  52. package/dist/esm/inbox-react/inbox-client.js +58 -0
  53. package/dist/esm/inbox-react/inbox-context.js +9 -0
  54. package/dist/esm/inbox-react/inbox-full-list.js +10 -0
  55. package/dist/esm/inbox-react/inbox-list.js +34 -0
  56. package/dist/esm/inbox-react/inbox-panel.js +10 -0
  57. package/dist/esm/inbox-react/inbox-provider.js +154 -0
  58. package/dist/esm/inbox-react/inbox-state.js +83 -0
  59. package/dist/esm/inbox-react/index.js +18 -0
  60. package/dist/esm/inbox-react/labels.js +34 -0
  61. package/dist/esm/inbox-react/notification-bell.js +29 -0
  62. package/dist/esm/inbox-react/notification-item.js +14 -0
  63. package/dist/esm/inbox-react/parse.js +85 -0
  64. package/dist/esm/inbox-react/preference-dialog.js +46 -0
  65. package/dist/esm/inbox-react/types.js +1 -0
  66. package/dist/esm/inbox-react/use-inbox-polling.js +88 -0
  67. package/dist/esm/index.js +4 -0
  68. package/dist/esm/retry.js +9 -0
  69. package/dist/inbox-react/styles.css +65 -0
  70. package/dist/types/browser-stub.d.ts +4 -0
  71. package/dist/types/client.d.ts +94 -0
  72. package/dist/types/contract.d.ts +182 -0
  73. package/dist/types/errors.d.ts +29 -0
  74. package/dist/types/events.d.ts +26 -0
  75. package/dist/types/idempotency.d.ts +4 -0
  76. package/dist/types/inbox/browser-stub.d.ts +3 -0
  77. package/dist/types/inbox/client.d.ts +34 -0
  78. package/dist/types/inbox/errors.d.ts +13 -0
  79. package/dist/types/inbox/handler.d.ts +25 -0
  80. package/dist/types/inbox/index.d.ts +6 -0
  81. package/dist/types/inbox/origin.d.ts +5 -0
  82. package/dist/types/inbox/project.d.ts +7 -0
  83. package/dist/types/inbox/validate.d.ts +25 -0
  84. package/dist/types/inbox-react/format.d.ts +13 -0
  85. package/dist/types/inbox-react/inbox-client.d.ts +24 -0
  86. package/dist/types/inbox-react/inbox-context.d.ts +29 -0
  87. package/dist/types/inbox-react/inbox-full-list.d.ts +7 -0
  88. package/dist/types/inbox-react/inbox-list.d.ts +12 -0
  89. package/dist/types/inbox-react/inbox-panel.d.ts +2 -0
  90. package/dist/types/inbox-react/inbox-provider.d.ts +18 -0
  91. package/dist/types/inbox-react/inbox-state.d.ts +70 -0
  92. package/dist/types/inbox-react/index.d.ts +19 -0
  93. package/dist/types/inbox-react/labels.d.ts +40 -0
  94. package/dist/types/inbox-react/notification-bell.d.ts +4 -0
  95. package/dist/types/inbox-react/notification-item.d.ts +9 -0
  96. package/dist/types/inbox-react/parse.d.ts +9 -0
  97. package/dist/types/inbox-react/preference-dialog.d.ts +5 -0
  98. package/dist/types/inbox-react/types.d.ts +55 -0
  99. package/dist/types/inbox-react/use-inbox-polling.d.ts +33 -0
  100. package/dist/types/index.d.ts +6 -0
  101. package/dist/types/retry.d.ts +13 -0
  102. package/package.json +34 -4
  103. package/skills/add-pigeon-notifications/SKILL.md +302 -0
  104. package/skills/add-pigeon-notifications/references/inbox-contract.instructions.md +208 -0
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createInboxHandler = createInboxHandler;
4
+ const client_1 = require("./client");
5
+ const errors_1 = require("./errors");
6
+ const origin_1 = require("./origin");
7
+ const project_1 = require("./project");
8
+ const validate_1 = require("./validate");
9
+ /** The six operations of IB-8, and nothing else. */
10
+ function match(method, pathname, base) {
11
+ if (pathname !== base && !pathname.startsWith(`${base}/`))
12
+ return null;
13
+ const rest = pathname.slice(base.length).replace(/\/+$/, '');
14
+ if (method === 'GET' && rest === '/summary')
15
+ return { name: 'summary', write: false };
16
+ if (method === 'GET' && rest === '/notifications')
17
+ return { name: 'list', write: false };
18
+ if (method === 'GET' && rest === '/preference')
19
+ return { name: 'getPreference', write: false };
20
+ if (method === 'PUT' && rest === '/preference')
21
+ return { name: 'setPreference', write: true };
22
+ if (method === 'POST' && rest === '/read-all')
23
+ return { name: 'readAll', write: true };
24
+ const read = method === 'POST' ? /^\/notifications\/([^/]+)\/read$/.exec(rest) : null;
25
+ if (read)
26
+ return { name: 'markRead', write: true, id: read[1] };
27
+ return null;
28
+ }
29
+ /**
30
+ * The host xApp's browser-facing inbox routes, in one tested function. Mount it on a catch-all route:
31
+ *
32
+ * const handler = createInboxHandler({ resolvePerson });
33
+ * export { handler as GET, handler as POST, handler as PUT };
34
+ *
35
+ * The person comes only from `resolvePerson`. The host stores nothing. Without `client`, the key and the address are
36
+ * read on the first request, so a missing one answers `unavailable` and never stops the host's build.
37
+ */
38
+ function createInboxHandler(options) {
39
+ const base = (options.basePath ?? '/api/communication/inbox').replace(/\/+$/, '');
40
+ const call = { retry: false, timeoutMs: options.timeoutMs ?? 8000 };
41
+ let made;
42
+ const clientOf = () => options.client ?? (made ??= (0, client_1.createInboxClient)());
43
+ return async function inboxHandler(request) {
44
+ try {
45
+ const url = new URL(request.url);
46
+ const op = match(request.method.toUpperCase(), url.pathname, base);
47
+ if (!op)
48
+ return (0, errors_1.hostError)('not_found');
49
+ if (op.write && !(0, origin_1.originAllowed)(request, options.allowedOrigins))
50
+ return (0, errors_1.hostError)('not_permitted');
51
+ const person = await options.resolvePerson(request);
52
+ if (typeof person !== 'string' || person.length === 0 || person.length > validate_1.INBOX_LIMITS.personMax)
53
+ return (0, errors_1.hostError)('not_signed_in');
54
+ const client = clientOf();
55
+ switch (op.name) {
56
+ case 'summary':
57
+ return answer((0, project_1.projectSummary)(await client.summary(person, call)));
58
+ case 'list': {
59
+ const limit = (0, validate_1.parseLimit)(url.searchParams.get('limit'));
60
+ if (!limit.ok)
61
+ return (0, errors_1.hostError)('invalid_input', limit.field);
62
+ const cursor = (0, validate_1.parseCursor)(url.searchParams.get('cursor'));
63
+ if (!cursor.ok)
64
+ return (0, errors_1.hostError)('invalid_input', cursor.field);
65
+ const unread = (0, validate_1.parseUnread)(url.searchParams.get('unread'));
66
+ if (!unread.ok)
67
+ return (0, errors_1.hostError)('invalid_input', unread.field);
68
+ return answer((0, project_1.projectPage)(await client.list(person, { limit: limit.value, cursor: cursor.value, unreadOnly: unread.value }, call)));
69
+ }
70
+ case 'markRead': {
71
+ let raw;
72
+ try {
73
+ raw = decodeURIComponent(op.id);
74
+ }
75
+ catch {
76
+ return (0, errors_1.hostError)('invalid_input', 'id');
77
+ }
78
+ const id = (0, validate_1.parseId)(raw);
79
+ if (!id.ok)
80
+ return (0, errors_1.hostError)('invalid_input', id.field);
81
+ return answer((0, project_1.projectMarkRead)(await client.markRead(person, id.value, call)));
82
+ }
83
+ case 'readAll': {
84
+ const body = (0, validate_1.parseBody)(await request.text());
85
+ if (!body.ok)
86
+ return (0, errors_1.hostError)('invalid_input', body.field);
87
+ const upTo = (0, validate_1.parseUpTo)(body.value.upTo);
88
+ if (!upTo.ok)
89
+ return (0, errors_1.hostError)('invalid_input', upTo.field);
90
+ return answer((0, project_1.projectReadAll)(await client.markAllRead(person, { upTo: upTo.value }, call)));
91
+ }
92
+ case 'getPreference':
93
+ return answer((0, project_1.projectPreference)(await client.getPreference(person, call)));
94
+ case 'setPreference': {
95
+ const body = (0, validate_1.parseBody)(await request.text());
96
+ if (!body.ok)
97
+ return (0, errors_1.hostError)('invalid_input', body.field);
98
+ const state = (0, validate_1.parseState)(body.value.state);
99
+ if (!state.ok)
100
+ return (0, errors_1.hostError)('invalid_input', state.field);
101
+ return answer((0, project_1.projectPreference)(await client.setPreference(person, state.value, call)));
102
+ }
103
+ }
104
+ }
105
+ catch (error) {
106
+ const code = (0, errors_1.hostCodeOf)(error);
107
+ if (code === 'unavailable')
108
+ report(error);
109
+ return (0, errors_1.hostError)(code);
110
+ }
111
+ };
112
+ function answer(data) {
113
+ if (data === null) {
114
+ report(new Error('The inbox answer did not have the documented shape'));
115
+ return (0, errors_1.hostError)('unavailable');
116
+ }
117
+ return (0, errors_1.hostData)(data);
118
+ }
119
+ function report(error) {
120
+ try {
121
+ options.onError?.(error);
122
+ }
123
+ catch {
124
+ // A failing logger must not change the answer.
125
+ }
126
+ }
127
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.INBOX_LIMITS = exports.originAllowed = exports.hostCodeOf = exports.HOST_ERRORS = exports.createInboxHandler = exports.personHeader = exports.createInboxClient = void 0;
4
+ var client_1 = require("./client");
5
+ Object.defineProperty(exports, "createInboxClient", { enumerable: true, get: function () { return client_1.createInboxClient; } });
6
+ Object.defineProperty(exports, "personHeader", { enumerable: true, get: function () { return client_1.personHeader; } });
7
+ var handler_1 = require("./handler");
8
+ Object.defineProperty(exports, "createInboxHandler", { enumerable: true, get: function () { return handler_1.createInboxHandler; } });
9
+ var errors_1 = require("./errors");
10
+ Object.defineProperty(exports, "HOST_ERRORS", { enumerable: true, get: function () { return errors_1.HOST_ERRORS; } });
11
+ Object.defineProperty(exports, "hostCodeOf", { enumerable: true, get: function () { return errors_1.hostCodeOf; } });
12
+ var origin_1 = require("./origin");
13
+ Object.defineProperty(exports, "originAllowed", { enumerable: true, get: function () { return origin_1.originAllowed; } });
14
+ var validate_1 = require("./validate");
15
+ Object.defineProperty(exports, "INBOX_LIMITS", { enumerable: true, get: function () { return validate_1.INBOX_LIMITS; } });
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.originAllowed = originAllowed;
4
+ /**
5
+ * A request that changes something must come from the host's own pages (14.5): the browser says `Sec-Fetch-Site: same-origin`,
6
+ * or the `Origin` header equals an allowed origin. Without `allowedOrigins`, the only allowed origin is the request's own.
7
+ */
8
+ function originAllowed(request, allowedOrigins) {
9
+ if (request.headers.get('sec-fetch-site') === 'same-origin')
10
+ return true;
11
+ const origin = request.headers.get('origin');
12
+ if (!origin)
13
+ return false;
14
+ let allowed;
15
+ try {
16
+ allowed = allowedOrigins && allowedOrigins.length > 0 ? allowedOrigins : [new URL(request.url).origin];
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ return allowed.includes(origin);
22
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.projectNotification = projectNotification;
4
+ exports.projectSummary = projectSummary;
5
+ exports.projectPage = projectPage;
6
+ exports.projectMarkRead = projectMarkRead;
7
+ exports.projectReadAll = projectReadAll;
8
+ exports.projectPreference = projectPreference;
9
+ /**
10
+ * Allowlist projection: only the documented fields of IB-2 leave the host, so nothing the Gateway adds later reaches a
11
+ * browser by accident. A value of the wrong shape gives `null`, which the handler answers as `unavailable`.
12
+ */
13
+ const obj = (v) => (typeof v === 'object' && v !== null && !Array.isArray(v) ? v : null);
14
+ const str = (v) => (typeof v === 'string' ? v : null);
15
+ const strOrNull = (v) => (v === null ? null : typeof v === 'string' ? v : undefined);
16
+ const count = (v) => (typeof v === 'number' && Number.isInteger(v) && v >= 0 ? v : null);
17
+ function projectNotification(v) {
18
+ const o = obj(v);
19
+ const source = obj(o?.source);
20
+ if (!o || !source)
21
+ return null;
22
+ const id = str(o.id);
23
+ const title = str(o.title);
24
+ const body = str(o.body);
25
+ const createdAt = str(o.createdAt);
26
+ const link = strOrNull(o.link);
27
+ const readAt = strOrNull(o.readAt);
28
+ const name = str(source.name);
29
+ if (id === null || title === null || body === null || createdAt === null || link === undefined || readAt === undefined || name === null)
30
+ return null;
31
+ if (o.kind !== 'notification' && o.kind !== 'announcement')
32
+ return null;
33
+ if (source.type !== 'xapp' && source.type !== 'tenant')
34
+ return null;
35
+ if (typeof o.read !== 'boolean')
36
+ return null;
37
+ return { id, kind: o.kind, title, body, link, source: { type: source.type, name }, createdAt, read: o.read, readAt };
38
+ }
39
+ function projectSummary(v) {
40
+ const o = obj(v);
41
+ if (!o)
42
+ return null;
43
+ const unreadCount = count(o.unreadCount);
44
+ const newestUnreadAt = strOrNull(o.newestUnreadAt);
45
+ const pollSeconds = count(o.pollSeconds);
46
+ return unreadCount === null || newestUnreadAt === undefined || pollSeconds === null ? null : { unreadCount, newestUnreadAt, pollSeconds };
47
+ }
48
+ function projectPage(v) {
49
+ const o = obj(v);
50
+ if (!o || !Array.isArray(o.notifications))
51
+ return null;
52
+ const notifications = [];
53
+ for (const raw of o.notifications) {
54
+ const n = projectNotification(raw);
55
+ if (!n)
56
+ return null;
57
+ notifications.push(n);
58
+ }
59
+ const nextCursor = strOrNull(o.nextCursor);
60
+ const unreadCount = count(o.unreadCount);
61
+ return nextCursor === undefined || unreadCount === null ? null : { notifications, nextCursor, unreadCount };
62
+ }
63
+ function projectMarkRead(v) {
64
+ const o = obj(v);
65
+ const notification = projectNotification(o?.notification);
66
+ const unreadCount = count(o?.unreadCount);
67
+ return notification && unreadCount !== null ? { notification, unreadCount } : null;
68
+ }
69
+ function projectReadAll(v) {
70
+ const o = obj(v);
71
+ const marked = count(o?.marked);
72
+ const remaining = count(o?.remaining);
73
+ const unreadCount = count(o?.unreadCount);
74
+ return marked === null || remaining === null || unreadCount === null ? null : { marked, remaining, unreadCount };
75
+ }
76
+ function projectPreference(v) {
77
+ const o = obj(v);
78
+ if (!o)
79
+ return null;
80
+ const changedAt = strOrNull(o.changedAt);
81
+ if (o.state !== 'opted_in' && o.state !== 'opted_out' && o.state !== 'not_set')
82
+ return null;
83
+ return changedAt === undefined || typeof o.delivers !== 'boolean' ? null : { state: o.state, changedAt, delivers: o.delivers };
84
+ }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.INBOX_LIMITS = void 0;
4
+ exports.parseLimit = parseLimit;
5
+ exports.parseCursor = parseCursor;
6
+ exports.parseUnread = parseUnread;
7
+ exports.parseId = parseId;
8
+ exports.parseUpTo = parseUpTo;
9
+ exports.parseState = parseState;
10
+ exports.parseBody = parseBody;
11
+ exports.INBOX_LIMITS = { pageMin: 1, pageMax: 100, cursorMax: 128, idMax: 64, upToMax: 40, bodyBytes: 1024, personMax: 128 };
12
+ const ok = (value) => ({ ok: true, value });
13
+ const bad = (field) => ({ ok: false, field });
14
+ function parseLimit(raw) {
15
+ if (raw === null)
16
+ return ok(undefined);
17
+ if (!/^\d{1,3}$/.test(raw))
18
+ return bad('limit');
19
+ const n = Number(raw);
20
+ return n >= exports.INBOX_LIMITS.pageMin && n <= exports.INBOX_LIMITS.pageMax ? ok(n) : bad('limit');
21
+ }
22
+ function parseCursor(raw) {
23
+ if (raw === null)
24
+ return ok(undefined);
25
+ return raw.length > 0 && raw.length <= exports.INBOX_LIMITS.cursorMax && /^[A-Za-z0-9_-]+$/.test(raw) ? ok(raw) : bad('cursor');
26
+ }
27
+ function parseUnread(raw) {
28
+ if (raw === null || raw === 'false')
29
+ return ok(false);
30
+ return raw === 'true' ? ok(true) : bad('unread');
31
+ }
32
+ function parseId(raw) {
33
+ return raw.length > 0 && raw.length <= exports.INBOX_LIMITS.idMax && /^[A-Za-z0-9_-]+$/.test(raw) ? ok(raw) : bad('id');
34
+ }
35
+ function parseUpTo(raw) {
36
+ if (raw === undefined || raw === null)
37
+ return ok(undefined);
38
+ if (typeof raw !== 'string' || raw.length > exports.INBOX_LIMITS.upToMax || !/^\d{4}-\d{2}-\d{2}T[\d:.]+(Z|[+-]\d{2}:\d{2})$/.test(raw))
39
+ return bad('upTo');
40
+ const t = Date.parse(raw);
41
+ return Number.isFinite(t) ? ok(new Date(t).toISOString()) : bad('upTo');
42
+ }
43
+ function parseState(raw) {
44
+ return raw === 'opted_in' || raw === 'opted_out' ? ok(raw) : bad('state');
45
+ }
46
+ /** A JSON object of at most 1024 bytes. An empty body is an empty object. */
47
+ function parseBody(text) {
48
+ if (new TextEncoder().encode(text).length > exports.INBOX_LIMITS.bodyBytes)
49
+ return bad('body');
50
+ if (text.trim() === '')
51
+ return ok({});
52
+ try {
53
+ const v = JSON.parse(text);
54
+ return typeof v === 'object' && v !== null && !Array.isArray(v) ? ok(v) : bad('body');
55
+ }
56
+ catch {
57
+ return bad('body');
58
+ }
59
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fill = fill;
4
+ exports.badgeText = badgeText;
5
+ exports.bellName = bellName;
6
+ exports.safeLink = safeLink;
7
+ exports.relativeTime = relativeTime;
8
+ exports.exactTime = exactTime;
9
+ /** Fills `{name}` placeholders. Values are text, never markup. */
10
+ function fill(template, values) {
11
+ return template.replace(/\{(\w+)\}/g, (whole, key) => (key in values ? String(values[key]) : whole));
12
+ }
13
+ /** Nothing for 0, the number for 1 to 99, "99+" above (12.1). */
14
+ function badgeText(count) {
15
+ if (!Number.isFinite(count) || count < 1)
16
+ return '';
17
+ return count > 99 ? '99+' : String(Math.floor(count));
18
+ }
19
+ /** The bell's accessible name: "{label}, {n} unread", or the label alone when nothing is unread (15.3). */
20
+ function bellName(labels, count) {
21
+ return count > 0 ? fill(labels.bellWithCount, { label: labels.bell, n: count }) : labels.bell;
22
+ }
23
+ const LINK_MAX = 2048;
24
+ /** Accepts exactly what the inbox stores: an absolute https URL without credentials, at most 2048 characters (12.15). */
25
+ function safeLink(link) {
26
+ if (!link || link.length > LINK_MAX)
27
+ return null;
28
+ try {
29
+ const u = new URL(link);
30
+ return u.protocol === 'https:' && !u.username && !u.password ? u.toString() : null;
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ const MINUTE = 60;
37
+ const HOUR = 3600;
38
+ const DAY = 86400;
39
+ /** "now", "5 minutes ago", "yesterday", then a short date after a week. A time in the future (clock skew) reads as now. */
40
+ function relativeTime(iso, now, locale) {
41
+ const then = Date.parse(iso);
42
+ if (!Number.isFinite(then))
43
+ return '';
44
+ const seconds = Math.max(0, Math.floor((now.getTime() - then) / 1000));
45
+ try {
46
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
47
+ if (seconds < MINUTE)
48
+ return rtf.format(0, 'second');
49
+ if (seconds < HOUR)
50
+ return rtf.format(-Math.floor(seconds / MINUTE), 'minute');
51
+ if (seconds < DAY)
52
+ return rtf.format(-Math.floor(seconds / HOUR), 'hour');
53
+ if (seconds < 7 * DAY)
54
+ return rtf.format(-Math.floor(seconds / DAY), 'day');
55
+ return new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(new Date(then));
56
+ }
57
+ catch {
58
+ return new Date(then).toISOString().slice(0, 10);
59
+ }
60
+ }
61
+ /** The full date and time, for the `title` of a relative time. */
62
+ function exactTime(iso, locale) {
63
+ const then = Date.parse(iso);
64
+ if (!Number.isFinite(then))
65
+ return '';
66
+ try {
67
+ return new Intl.DateTimeFormat(locale, { dateStyle: 'full', timeStyle: 'short' }).format(new Date(then));
68
+ }
69
+ catch {
70
+ return new Date(then).toISOString();
71
+ }
72
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_API_BASE = void 0;
4
+ exports.normalizeApiBase = normalizeApiBase;
5
+ exports.createInboxBrowserClient = createInboxBrowserClient;
6
+ const parse_1 = require("./parse");
7
+ exports.DEFAULT_API_BASE = '/api/communication/inbox';
8
+ /**
9
+ * The browser talks only to the host xApp's own origin (14.2). The base is a path on that origin: an absolute URL,
10
+ * a protocol-relative one or anything with a query is refused.
11
+ */
12
+ function normalizeApiBase(apiBase) {
13
+ const base = (apiBase ?? exports.DEFAULT_API_BASE).trim();
14
+ if (!base.startsWith('/') || base.startsWith('//') || /[\\?#\s]/.test(base) || base.includes('://'))
15
+ return null;
16
+ return base.replace(/\/+$/, '') || null;
17
+ }
18
+ /**
19
+ * Every call answers a result and never throws, so an outage cannot break the host page (14.9).
20
+ * The bell shows what this xApp's own sub-tenant sent the person, and the tenant's announcements: there is nothing to choose.
21
+ */
22
+ function createInboxBrowserClient(apiBase, fetchImpl) {
23
+ const base = normalizeApiBase(apiBase);
24
+ async function call(method, path, parse, body) {
25
+ if (base === null)
26
+ return { ok: false, code: 'unavailable' };
27
+ try {
28
+ const res = await (fetchImpl ?? fetch)(`${base}${path}`, {
29
+ method,
30
+ credentials: 'same-origin',
31
+ cache: 'no-store',
32
+ headers: { Accept: 'application/json', ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}) },
33
+ body: body === undefined ? undefined : JSON.stringify(body),
34
+ });
35
+ const json = await res.json().catch(() => null);
36
+ if (!res.ok)
37
+ return { ok: false, code: (0, parse_1.parseErrorCode)(res.status, json) };
38
+ const data = parse(json?.data);
39
+ return data === null ? { ok: false, code: 'unavailable' } : { ok: true, data };
40
+ }
41
+ catch {
42
+ return { ok: false, code: 'unavailable' };
43
+ }
44
+ }
45
+ return {
46
+ summary: () => call('GET', '/summary', parse_1.parseSummary),
47
+ list(query = {}) {
48
+ const q = new URLSearchParams();
49
+ if (query.limit !== undefined)
50
+ q.set('limit', String(query.limit));
51
+ if (query.cursor)
52
+ q.set('cursor', query.cursor);
53
+ if (query.unreadOnly)
54
+ q.set('unread', 'true');
55
+ const qs = q.toString();
56
+ return call('GET', `/notifications${qs ? `?${qs}` : ''}`, parse_1.parsePage);
57
+ },
58
+ markRead: (id) => call('POST', `/notifications/${encodeURIComponent(id)}/read`, parse_1.parseMarkRead, {}),
59
+ markAllRead: (upTo) => call('POST', '/read-all', parse_1.parseReadAll, upTo ? { upTo } : {}),
60
+ getPreference: () => call('GET', '/preference', parse_1.parsePreference),
61
+ setPreference: (state) => call('PUT', '/preference', parse_1.parsePreference, { state }),
62
+ };
63
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ 'use client';
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.InboxContext = void 0;
5
+ exports.useInbox = useInbox;
6
+ const react_1 = require("react");
7
+ exports.InboxContext = (0, react_1.createContext)(null);
8
+ function useInbox() {
9
+ const value = (0, react_1.useContext)(exports.InboxContext);
10
+ if (!value)
11
+ throw new Error('Wrap the inbox components in <InboxProvider>.');
12
+ return value;
13
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ 'use client';
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.InboxFullList = InboxFullList;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const inbox_list_1 = require("./inbox-list");
7
+ /**
8
+ * Every notification, a page at a time. The provider shows it in a dialog for "View all"; a host that passes
9
+ * `onViewAll` can place it on a page of its own instead.
10
+ */
11
+ function InboxFullList({ maxHeight = 'min(70vh, 640px)' }) {
12
+ return (0, jsx_runtime_1.jsx)(inbox_list_1.InboxListView, { paged: true, maxHeight: maxHeight });
13
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ 'use client';
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.InboxListView = InboxListView;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("react");
7
+ const core_1 = require("@mantine/core");
8
+ const format_1 = require("./format");
9
+ const inbox_context_1 = require("./inbox-context");
10
+ const inbox_state_1 = require("./inbox-state");
11
+ const notification_item_1 = require("./notification-item");
12
+ /** Tabs, "Mark all as read", the list and its loading, empty and error states. Shared by the panel and the full list. */
13
+ function InboxListView({ heading, maxHeight, paged = false }) {
14
+ const { state, labels, setTab, markAllRead, loadMore, retry, clearMessage, attachList } = (0, inbox_context_1.useInbox)();
15
+ (0, react_1.useEffect)(() => attachList(), [attachList]);
16
+ // Relative times move on while the list stays open.
17
+ const [now, setNow] = (0, react_1.useState)(() => new Date());
18
+ (0, react_1.useEffect)(() => {
19
+ const timer = window.setInterval(() => setNow(new Date()), 60_000);
20
+ return () => window.clearInterval(timer);
21
+ }, []);
22
+ let content;
23
+ if (!state.loaded && state.status === 'error') {
24
+ content = ((0, jsx_runtime_1.jsx)(core_1.Alert, { color: "red", variant: "light", m: "md", title: labels.loadError, children: (0, jsx_runtime_1.jsx)(core_1.Button, { variant: "default", size: "sm", mih: 44, onClick: retry, children: labels.retry }) }));
25
+ }
26
+ else if (!state.loaded) {
27
+ content = ((0, jsx_runtime_1.jsx)(core_1.Center, { mih: 160, role: "status", "aria-label": labels.loading, children: (0, jsx_runtime_1.jsx)(core_1.Loader, { size: "sm" }) }));
28
+ }
29
+ else if (state.items.length === 0) {
30
+ content = ((0, jsx_runtime_1.jsx)(core_1.Text, { c: "dimmed", size: "sm", ta: "center", px: "md", py: "xl", children: state.tab === 'unread' ? labels.emptyUnread : labels.emptyAll }));
31
+ }
32
+ else {
33
+ content = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("ul", { className: "xci-list", children: state.items.map((item) => ((0, jsx_runtime_1.jsx)("li", { className: "xci-entry", children: (0, jsx_runtime_1.jsx)(notification_item_1.NotificationItem, { item: item, now: now }) }, item.id))) }), paged && state.nextCursor && ((0, jsx_runtime_1.jsx)(core_1.Center, { p: "sm", children: (0, jsx_runtime_1.jsx)(core_1.Button, { variant: "default", size: "sm", mih: 44, loading: state.loadingMore, onClick: loadMore, children: labels.loadMore }) }))] }));
34
+ }
35
+ const list = ((0, jsx_runtime_1.jsx)(core_1.ScrollArea.Autosize, { mah: maxHeight, type: "auto", children: content }));
36
+ return ((0, jsx_runtime_1.jsxs)(core_1.Stack, { gap: 0, children: [(0, jsx_runtime_1.jsxs)(core_1.Group, { justify: "space-between", wrap: "nowrap", gap: "xs", ps: "md", pe: 4, pt: 4, children: [heading ?? (0, jsx_runtime_1.jsx)("span", {}), (0, jsx_runtime_1.jsx)(core_1.Button, { variant: "subtle", size: "compact-sm", mih: 44, px: "sm", disabled: !(0, inbox_state_1.canMarkAll)(state), onClick: markAllRead, children: labels.markAllRead })] }), (0, jsx_runtime_1.jsxs)(core_1.Tabs, { value: state.tab, onChange: (tab) => tab && setTab(tab), keepMounted: false, children: [(0, jsx_runtime_1.jsxs)(core_1.Tabs.List, { px: "xs", children: [(0, jsx_runtime_1.jsx)(core_1.Tabs.Tab, { value: "all", mih: 44, children: labels.tabAll }), (0, jsx_runtime_1.jsx)(core_1.Tabs.Tab, { value: "unread", mih: 44, children: state.unreadCount > 0 ? (0, format_1.fill)(labels.tabUnreadWithCount, { n: state.unreadCount }) : labels.tabUnread })] }), state.actionFailed && ((0, jsx_runtime_1.jsx)(core_1.Alert, { color: "red", variant: "light", m: "xs", role: "alert", withCloseButton: true, closeButtonLabel: labels.dismiss, onClose: clearMessage, children: labels.actionError })), (0, jsx_runtime_1.jsx)(core_1.Tabs.Panel, { value: "all", children: list }), (0, jsx_runtime_1.jsx)(core_1.Tabs.Panel, { value: "unread", children: list })] })] }));
37
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ 'use client';
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.InboxPanel = InboxPanel;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const core_1 = require("@mantine/core");
7
+ const inbox_context_1 = require("./inbox-context");
8
+ const inbox_list_1 = require("./inbox-list");
9
+ /** What the bell opens: the newest notifications, with the ways on to the full list and the preference. */
10
+ function InboxPanel() {
11
+ const { labels, viewAll, openPreferences } = (0, inbox_context_1.useInbox)();
12
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(inbox_list_1.InboxListView, { maxHeight: 400, heading: (0, jsx_runtime_1.jsx)(core_1.Title, { order: 2, size: "h5", children: labels.heading }) }), (0, jsx_runtime_1.jsx)(core_1.Divider, {}), (0, jsx_runtime_1.jsxs)(core_1.Group, { justify: "space-between", gap: 4, p: 4, children: [(0, jsx_runtime_1.jsx)(core_1.Button, { variant: "subtle", size: "compact-sm", mih: 44, px: "sm", onClick: viewAll, children: labels.viewAll }), (0, jsx_runtime_1.jsx)(core_1.Button, { variant: "subtle", size: "compact-sm", mih: 44, px: "sm", onClick: openPreferences, children: labels.preferences })] })] }));
13
+ }