@astram/guard 0.23.0 → 0.25.0
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/dist/auth/auth.module.d.ts +7 -0
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +43 -1
- package/dist/auth/tokens.d.ts +13 -0
- package/dist/auth/tokens.d.ts.map +1 -1
- package/dist/auth/tokens.js +14 -1
- package/dist/config/notification-config.d.ts +20 -0
- package/dist/config/notification-config.d.ts.map +1 -0
- package/dist/config/notification-config.js +33 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +49 -36
- package/dist/module/shield.module.d.ts +12 -0
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +11 -0
- package/dist/notifications/mariadb-notification-store.d.ts +38 -0
- package/dist/notifications/mariadb-notification-store.d.ts.map +1 -0
- package/dist/notifications/mariadb-notification-store.js +151 -0
- package/dist/notifications/notification-retention.job.d.ts +52 -0
- package/dist/notifications/notification-retention.job.d.ts.map +1 -0
- package/dist/notifications/notification-retention.job.js +97 -0
- package/dist/notifications/notification.service.d.ts +71 -0
- package/dist/notifications/notification.service.d.ts.map +1 -0
- package/dist/notifications/notification.service.js +252 -0
- package/dist/notifications/notifications.controller.d.ts +57 -0
- package/dist/notifications/notifications.controller.d.ts.map +1 -0
- package/dist/notifications/notifications.controller.js +257 -0
- package/dist/notifications/ports.d.ts +112 -0
- package/dist/notifications/ports.d.ts.map +1 -0
- package/dist/notifications/ports.js +63 -0
- package/package.json +5 -5
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification store contract (G-027 step 2, notification-store SPEC §5A, §5B, §10).
|
|
3
|
+
*
|
|
4
|
+
* One per-user, tenant-walled inbox in `<ns>_identity.user_notification` (identity migration
|
|
5
|
+
* 108). Every per-user statement is scoped by BOTH the user and the tenant (the session's active
|
|
6
|
+
* company): a notice of another user or company is indistinguishable from a missing one (core 50
|
|
7
|
+
* #4, #7; Shield #4). Notices never grant anything and never touch catalog, grants or `decide()`.
|
|
8
|
+
*
|
|
9
|
+
* Errors carry the FIELD, kind or variable name only, never a submitted value (core 50 #10).
|
|
10
|
+
*/
|
|
11
|
+
/** The two inbox categories (SPEC §10). The category always comes from the kind registration. */
|
|
12
|
+
export declare const NOTIFICATION_CATEGORIES: readonly ["announcement", "personal"];
|
|
13
|
+
export type NotificationCategory = (typeof NOTIFICATION_CATEGORIES)[number];
|
|
14
|
+
/** One `notificationKinds` registration (`ShieldModule.forRoot({ notificationKinds })`). */
|
|
15
|
+
export interface NotificationKindRegistration {
|
|
16
|
+
readonly kind: string;
|
|
17
|
+
readonly category: NotificationCategory;
|
|
18
|
+
}
|
|
19
|
+
/** A stored notice as the store returns it (no tenant id, user id or dedupe key). */
|
|
20
|
+
export interface NotificationRow {
|
|
21
|
+
readonly uuid: string;
|
|
22
|
+
readonly category: NotificationCategory;
|
|
23
|
+
readonly kind: string;
|
|
24
|
+
readonly title: string;
|
|
25
|
+
readonly body: string | null;
|
|
26
|
+
readonly link: string | null;
|
|
27
|
+
readonly createdAtMs: number;
|
|
28
|
+
/** `null` ⇒ unread. */
|
|
29
|
+
readonly readAtMs: number | null;
|
|
30
|
+
}
|
|
31
|
+
/** A row to insert; every field already validated and server-stamped by the service. */
|
|
32
|
+
export interface NotificationInsert {
|
|
33
|
+
readonly uuid: string;
|
|
34
|
+
readonly tenantId: string;
|
|
35
|
+
readonly userId: string;
|
|
36
|
+
readonly category: NotificationCategory;
|
|
37
|
+
readonly kind: string;
|
|
38
|
+
readonly title: string;
|
|
39
|
+
readonly body: string | null;
|
|
40
|
+
readonly link: string | null;
|
|
41
|
+
readonly dedupeKey: string | null;
|
|
42
|
+
readonly createdAtMs: number;
|
|
43
|
+
}
|
|
44
|
+
export interface NotificationListQuery {
|
|
45
|
+
/** Absent ⇒ both categories. */
|
|
46
|
+
readonly category?: NotificationCategory;
|
|
47
|
+
readonly unreadOnly: boolean;
|
|
48
|
+
/** ≥ 1. */
|
|
49
|
+
readonly page: number;
|
|
50
|
+
/** 1–100. */
|
|
51
|
+
readonly limit: number;
|
|
52
|
+
}
|
|
53
|
+
export interface NotificationListResult {
|
|
54
|
+
readonly total: number;
|
|
55
|
+
readonly rows: readonly NotificationRow[];
|
|
56
|
+
}
|
|
57
|
+
export interface NotificationUnreadCounts {
|
|
58
|
+
readonly announcement: number;
|
|
59
|
+
readonly personal: number;
|
|
60
|
+
}
|
|
61
|
+
/** Retention in days (`loadNotificationConfig`): read notices, and every notice. */
|
|
62
|
+
export interface NotificationRetentionPolicy {
|
|
63
|
+
readonly readDays: number;
|
|
64
|
+
readonly maxDays: number;
|
|
65
|
+
}
|
|
66
|
+
/** A query on the caller's open transaction connection (as `MfaResetQuery`). */
|
|
67
|
+
export type NotificationQuery = (sql: string, params?: unknown[]) => Promise<unknown>;
|
|
68
|
+
export interface NotificationWriteOptions {
|
|
69
|
+
/** Run on the caller's open transaction (step 3: notices commit or roll back with the grant). */
|
|
70
|
+
readonly query?: NotificationQuery;
|
|
71
|
+
}
|
|
72
|
+
/** The persistence port; `MariaDbNotificationStore` is the framework default. */
|
|
73
|
+
export interface NotificationStore {
|
|
74
|
+
list(userId: string, tenantId: string, query: NotificationListQuery): Promise<NotificationListResult>;
|
|
75
|
+
countUnread(userId: string, tenantId: string): Promise<NotificationUnreadCounts>;
|
|
76
|
+
/** `true` when the notice exists for this user in this tenant (read already or now). */
|
|
77
|
+
markRead(userId: string, tenantId: string, uuid: string, nowMs: number): Promise<boolean>;
|
|
78
|
+
/** Number of notices changed from unread to read. */
|
|
79
|
+
markAllRead(userId: string, tenantId: string, category: NotificationCategory | undefined, nowMs: number): Promise<number>;
|
|
80
|
+
/** Inserted count; a repeated dedupe key is a no-op, not an error. */
|
|
81
|
+
insertMany(rows: readonly NotificationInsert[], opts?: NotificationWriteOptions): Promise<number>;
|
|
82
|
+
/** Deleted count (at most `maxRows`). */
|
|
83
|
+
purge(nowMs: number, policy: NotificationRetentionPolicy, maxRows: number): Promise<number>;
|
|
84
|
+
}
|
|
85
|
+
/** Kind pattern (SPEC §10): dotted lower-case segments, at least two. */
|
|
86
|
+
export declare const NOTIFICATION_KIND_PATTERN: RegExp;
|
|
87
|
+
export declare const NOTIFICATION_KIND_MAX_LENGTH = 64;
|
|
88
|
+
/**
|
|
89
|
+
* A kind is not registered (`post`), or a `notificationKinds` registration is invalid (boot).
|
|
90
|
+
* The message names the kind only when it is well-formed, so no arbitrary input is echoed.
|
|
91
|
+
*/
|
|
92
|
+
export declare class NotificationKindError extends Error {
|
|
93
|
+
readonly name = "NotificationKindError";
|
|
94
|
+
constructor(kind: unknown, reason?: string, printed?: string);
|
|
95
|
+
/**
|
|
96
|
+
* Boot-time refusal of an app's `notificationKinds` entry: names the kind as registered (a
|
|
97
|
+
* developer constant, quoted and cut to 80 characters) so the bad entry can be found.
|
|
98
|
+
*/
|
|
99
|
+
static registration(kind: unknown, reason: string): NotificationKindError;
|
|
100
|
+
}
|
|
101
|
+
/** A `post` input field broke the text, link, dedupe or recipient rules (SPEC §10, §11). */
|
|
102
|
+
export declare class NotificationValidationError extends Error {
|
|
103
|
+
readonly field: string;
|
|
104
|
+
readonly name = "NotificationValidationError";
|
|
105
|
+
constructor(field: string, reason: string);
|
|
106
|
+
}
|
|
107
|
+
/** The store could not answer (DB error); the driver message is never carried. */
|
|
108
|
+
export declare class NotificationUnavailableError extends Error {
|
|
109
|
+
readonly name = "NotificationUnavailableError";
|
|
110
|
+
constructor();
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.d.ts","sourceRoot":"","sources":["../../src/notifications/ports.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Notification store contract (G-027 step 2, notification-store SPEC §5A, §5B, §10).
|
|
4
|
+
*
|
|
5
|
+
* One per-user, tenant-walled inbox in `<ns>_identity.user_notification` (identity migration
|
|
6
|
+
* 108). Every per-user statement is scoped by BOTH the user and the tenant (the session's active
|
|
7
|
+
* company): a notice of another user or company is indistinguishable from a missing one (core 50
|
|
8
|
+
* #4, #7; Shield #4). Notices never grant anything and never touch catalog, grants or `decide()`.
|
|
9
|
+
*
|
|
10
|
+
* Errors carry the FIELD, kind or variable name only, never a submitted value (core 50 #10).
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.NotificationUnavailableError = exports.NotificationValidationError = exports.NotificationKindError = exports.NOTIFICATION_KIND_MAX_LENGTH = exports.NOTIFICATION_KIND_PATTERN = exports.NOTIFICATION_CATEGORIES = void 0;
|
|
14
|
+
/** The two inbox categories (SPEC §10). The category always comes from the kind registration. */
|
|
15
|
+
exports.NOTIFICATION_CATEGORIES = ['announcement', 'personal'];
|
|
16
|
+
/** Kind pattern (SPEC §10): dotted lower-case segments, at least two. */
|
|
17
|
+
exports.NOTIFICATION_KIND_PATTERN = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/;
|
|
18
|
+
exports.NOTIFICATION_KIND_MAX_LENGTH = 64;
|
|
19
|
+
/** The kind when it is well-formed (safe to print), else a placeholder. */
|
|
20
|
+
function printableKind(kind) {
|
|
21
|
+
return typeof kind === 'string' &&
|
|
22
|
+
kind.length <= exports.NOTIFICATION_KIND_MAX_LENGTH &&
|
|
23
|
+
exports.NOTIFICATION_KIND_PATTERN.test(kind)
|
|
24
|
+
? kind
|
|
25
|
+
: '(malformed kind)';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A kind is not registered (`post`), or a `notificationKinds` registration is invalid (boot).
|
|
29
|
+
* The message names the kind only when it is well-formed, so no arbitrary input is echoed.
|
|
30
|
+
*/
|
|
31
|
+
class NotificationKindError extends Error {
|
|
32
|
+
name = 'NotificationKindError';
|
|
33
|
+
constructor(kind, reason = 'is not registered', printed) {
|
|
34
|
+
super(`notification kind ${printed ?? printableKind(kind)} ${reason}`);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Boot-time refusal of an app's `notificationKinds` entry: names the kind as registered (a
|
|
38
|
+
* developer constant, quoted and cut to 80 characters) so the bad entry can be found.
|
|
39
|
+
*/
|
|
40
|
+
static registration(kind, reason) {
|
|
41
|
+
const quoted = typeof kind === 'string' ? JSON.stringify(kind.slice(0, 80)) : `(${typeof kind})`;
|
|
42
|
+
return new NotificationKindError(kind, reason, quoted);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.NotificationKindError = NotificationKindError;
|
|
46
|
+
/** A `post` input field broke the text, link, dedupe or recipient rules (SPEC §10, §11). */
|
|
47
|
+
class NotificationValidationError extends Error {
|
|
48
|
+
field;
|
|
49
|
+
name = 'NotificationValidationError';
|
|
50
|
+
constructor(field, reason) {
|
|
51
|
+
super(`notification ${field} ${reason}`);
|
|
52
|
+
this.field = field;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.NotificationValidationError = NotificationValidationError;
|
|
56
|
+
/** The store could not answer (DB error); the driver message is never carried. */
|
|
57
|
+
class NotificationUnavailableError extends Error {
|
|
58
|
+
name = 'NotificationUnavailableError';
|
|
59
|
+
constructor() {
|
|
60
|
+
super('notifications are unavailable');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.NotificationUnavailableError = NotificationUnavailableError;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astram/guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Astram framework — the enforcement guard + real-world adapters (the 'hands'). One global deny-by-default gate every request passes through (ADR-0002), classification decorators, boot-pool assertion (ADR-0018), the forRoot() registration seam, and the DB/Redis/crypto adapters that connect the sealed core to a real backend. Depends on @astram/core (ADR-0003, ADR-0023).",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"argon2": "^0.41.1",
|
|
37
37
|
"ioredis": "^5.4.1",
|
|
38
38
|
"mariadb": "^3.5.4",
|
|
39
|
-
"@astram/
|
|
40
|
-
"@astram/
|
|
39
|
+
"@astram/core": "1.13.0",
|
|
40
|
+
"@astram/engine": "0.16.1"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@nestjs/common": "^11",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@nestjs/platform-express": "^11",
|
|
46
46
|
"reflect-metadata": "^0.2",
|
|
47
47
|
"rxjs": "^7",
|
|
48
|
-
"@astram/messaging": "^0.1.
|
|
48
|
+
"@astram/messaging": "^0.1.10"
|
|
49
49
|
},
|
|
50
50
|
"peerDependenciesMeta": {
|
|
51
51
|
"@astram/messaging": {
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"typescript": "^5.7.0",
|
|
69
69
|
"unplugin-swc": "^1.5.1",
|
|
70
70
|
"vitest": "^2.1.8",
|
|
71
|
-
"@astram/messaging": "0.1.
|
|
71
|
+
"@astram/messaging": "0.1.10"
|
|
72
72
|
},
|
|
73
73
|
"repository": {
|
|
74
74
|
"type": "git",
|