@astram/guard 0.16.0 → 0.18.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.
Files changed (68) hide show
  1. package/dist/auth/account.controller.d.ts +3 -1
  2. package/dist/auth/account.controller.d.ts.map +1 -1
  3. package/dist/auth/account.controller.js +10 -6
  4. package/dist/auth/auth.controller.d.ts +3 -1
  5. package/dist/auth/auth.controller.d.ts.map +1 -1
  6. package/dist/auth/auth.controller.js +13 -11
  7. package/dist/auth/auth.module.d.ts +14 -2
  8. package/dist/auth/auth.module.d.ts.map +1 -1
  9. package/dist/auth/auth.module.js +54 -7
  10. package/dist/auth/confirm-email.controller.d.ts +3 -1
  11. package/dist/auth/confirm-email.controller.d.ts.map +1 -1
  12. package/dist/auth/confirm-email.controller.js +9 -5
  13. package/dist/auth/email-change-messages.d.ts +2 -0
  14. package/dist/auth/email-change-messages.d.ts.map +1 -1
  15. package/dist/auth/email-change-messages.js +2 -0
  16. package/dist/auth/password-reset-message.d.ts +7 -0
  17. package/dist/auth/password-reset-message.d.ts.map +1 -0
  18. package/dist/auth/password-reset-message.js +17 -0
  19. package/dist/auth/ports/email-sender.d.ts +7 -7
  20. package/dist/auth/ports/email-sender.d.ts.map +1 -1
  21. package/dist/auth/tokens.d.ts +14 -0
  22. package/dist/auth/tokens.d.ts.map +1 -1
  23. package/dist/auth/tokens.js +15 -1
  24. package/dist/authentication/argon2id-password-hasher.d.ts +3 -0
  25. package/dist/authentication/argon2id-password-hasher.d.ts.map +1 -1
  26. package/dist/authentication/argon2id-password-hasher.js +21 -0
  27. package/dist/authentication/credential-provisioner.d.ts +35 -0
  28. package/dist/authentication/credential-provisioner.d.ts.map +1 -0
  29. package/dist/authentication/credential-provisioner.js +16 -0
  30. package/dist/authentication/mariadb-credential-provisioner.d.ts +27 -0
  31. package/dist/authentication/mariadb-credential-provisioner.d.ts.map +1 -0
  32. package/dist/authentication/mariadb-credential-provisioner.js +95 -0
  33. package/dist/authz/operation-marker-check.d.ts +14 -0
  34. package/dist/authz/operation-marker-check.d.ts.map +1 -0
  35. package/dist/authz/operation-marker-check.js +25 -0
  36. package/dist/authz/wire-record-authorization.d.ts.map +1 -1
  37. package/dist/authz/wire-record-authorization.js +3 -2
  38. package/dist/config/message-config.d.ts +70 -0
  39. package/dist/config/message-config.d.ts.map +1 -0
  40. package/dist/config/message-config.js +249 -0
  41. package/dist/config/reserved-principal-ids.d.ts +14 -0
  42. package/dist/config/reserved-principal-ids.d.ts.map +1 -0
  43. package/dist/config/reserved-principal-ids.js +24 -0
  44. package/dist/config/root-ids-from-env.d.ts +2 -0
  45. package/dist/config/root-ids-from-env.d.ts.map +1 -0
  46. package/dist/config/root-ids-from-env.js +41 -0
  47. package/dist/index.d.ts +10 -2
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +24 -4
  50. package/dist/internal.d.ts +11 -0
  51. package/dist/internal.d.ts.map +1 -1
  52. package/dist/internal.js +15 -1
  53. package/dist/messaging/deliver-email.d.ts +10 -0
  54. package/dist/messaging/deliver-email.d.ts.map +1 -0
  55. package/dist/messaging/deliver-email.js +19 -0
  56. package/dist/messaging/email-delivery.d.ts +42 -0
  57. package/dist/messaging/email-delivery.d.ts.map +1 -0
  58. package/dist/messaging/email-delivery.js +117 -0
  59. package/dist/messaging/message-dispatcher.d.ts +66 -0
  60. package/dist/messaging/message-dispatcher.d.ts.map +1 -0
  61. package/dist/messaging/message-dispatcher.js +133 -0
  62. package/dist/messaging/recipient-mask.d.ts +7 -0
  63. package/dist/messaging/recipient-mask.d.ts.map +1 -0
  64. package/dist/messaging/recipient-mask.js +15 -0
  65. package/dist/module/shield.module.d.ts +5 -3
  66. package/dist/module/shield.module.d.ts.map +1 -1
  67. package/dist/module/shield.module.js +4 -0
  68. package/package.json +10 -5
@@ -0,0 +1,42 @@
1
+ import { type EmailProvider } from '@astram/core';
2
+ import type { FrameworkSchemas } from '@astram/core';
3
+ import type { Pool } from 'mariadb';
4
+ import type { EmailSender } from '../auth/ports/email-sender.js';
5
+ import { type EmailDeliveryConfig } from '../config/message-config.js';
6
+ import { type MessageAuditWriter } from './message-dispatcher.js';
7
+ /** `ShieldModule.forRoot({ emailSender })`. */
8
+ export type EmailSenderOption = EmailSender | 'disabled' | 'env';
9
+ /** What `@astram/messaging` must export for guard (spec P-2). */
10
+ export interface MessagingModule {
11
+ createEmailProvider(config: EmailDeliveryConfig): Promise<EmailProvider>;
12
+ }
13
+ export interface EmailDelivery {
14
+ /** `null` ⇒ mail disabled. */
15
+ readonly provider: EmailProvider | null;
16
+ /** The per-send bound the dispatcher applies. */
17
+ readonly timeoutMs: number;
18
+ }
19
+ export interface BuildEmailProviderDeps {
20
+ readonly env?: NodeJS.ProcessEnv;
21
+ /** Loader for the optional peer (default `require`). */
22
+ readonly load?: (id: string) => unknown;
23
+ readonly readFile?: (path: string) => string;
24
+ }
25
+ export declare function buildEmailProvider(option: EmailSenderOption | undefined, boundSender: EmailSender | null, deps: BuildEmailProviderDeps): Promise<EmailDelivery>;
26
+ /**
27
+ * The link-base boot rule applies only with a real sender (Phase 13 #1). `'env'` is real unless
28
+ * `EMAIL_PROVIDER=disabled`. Read at `forRoot` time, after `resolveBootSecrets()`.
29
+ */
30
+ export declare function isMailDisabled(option: EmailSenderOption | undefined, env: Readonly<Record<string, string | undefined>>): boolean;
31
+ /**
32
+ * The `EmailSender` bound under `SHIELD_EMAIL_SENDER` for `'env'`, so existing injectors keep the
33
+ * port they know. Each send is bounded; a failure rejects (callers already treat it best-effort).
34
+ */
35
+ export declare function senderFromProvider(provider: EmailProvider, timeoutMs: number): EmailSender;
36
+ /**
37
+ * `message.sent` / `message.failed` rows go to the framework audit log over guard's own pool
38
+ * (the runtime account holds append-only INSERT on the audit schema), independent of the
39
+ * optional app `auditSink` (Shield #6).
40
+ */
41
+ export declare function messageAuditWriter(pool: Pick<Pool, 'query'>, schemas: Pick<FrameworkSchemas, 'audit'>): MessageAuditWriter;
42
+ //# sourceMappingURL=email-delivery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-delivery.d.ts","sourceRoot":"","sources":["../../src/messaging/email-delivery.ts"],"names":[],"mappings":"AAcA,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAExE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAEpC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAEtF,+CAA+C;AAC/C,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,UAAU,GAAG,KAAK,CAAC;AAEjE,iEAAiE;AACjE,MAAM,WAAW,eAAe;IAC9B,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CAC1E;AAED,MAAM,WAAW,aAAa;IAC5B,8BAA8B;IAC9B,QAAQ,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAC;IACxC,iDAAiD;IACjD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,wDAAwD;IACxD,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAC9C;AAKD,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,iBAAiB,GAAG,SAAS,EACrC,WAAW,EAAE,WAAW,GAAG,IAAI,EAC/B,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,aAAa,CAAC,CA+BxB;AA+BD;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,iBAAiB,GAAG,SAAS,EACrC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GAChD,OAAO,CAIT;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CAM1F;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,EACzB,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC,GACvC,kBAAkB,CAKpB"}
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildEmailProvider = buildEmailProvider;
4
+ exports.isMailDisabled = isMailDisabled;
5
+ exports.senderFromProvider = senderFromProvider;
6
+ exports.messageAuditWriter = messageAuditWriter;
7
+ /**
8
+ * Email delivery binding (G-010, docs/features/message-delivery/SPEC.md R2, R8, R14). Turns the
9
+ * `ShieldModule.forRoot({ emailSender })` option into the provider the {@link MessageDispatcher}
10
+ * wraps:
11
+ * - `'env'` → {@link loadMessageConfig}, then `@astram/messaging` loaded as an OPTIONAL peer (only
12
+ * here, so apps that never choose `'env'` never load it) and its `createEmailProvider`; with
13
+ * `MESSAGE_BOOT_PROBE=1` the adapter's probe runs (no message is sent) and a failure refuses
14
+ * boot. `EMAIL_PROVIDER=disabled` binds nothing and loads nothing.
15
+ * - `'disabled'` → no provider (nothing delivered, nothing audited).
16
+ * - an app `EmailSender` → adapted as provider `custom`; omitted (development/test) → `log`.
17
+ */
18
+ const node_fs_1 = require("node:fs");
19
+ const common_1 = require("@nestjs/common");
20
+ const core_1 = require("@astram/core");
21
+ const engine_1 = require("@astram/engine");
22
+ const message_config_js_1 = require("../config/message-config.js");
23
+ const boot_secrets_js_1 = require("../secrets/boot-secrets.js");
24
+ const optional_peer_js_1 = require("../storage/optional-peer.js");
25
+ const message_dispatcher_js_1 = require("./message-dispatcher.js");
26
+ const MESSAGING_PACKAGE = '@astram/messaging';
27
+ const logger = new common_1.Logger('ShieldMessaging');
28
+ async function buildEmailProvider(option, boundSender, deps) {
29
+ const fallback = { timeoutMs: message_config_js_1.DEFAULT_MESSAGE_SEND_TIMEOUT_MS };
30
+ if (option === 'disabled')
31
+ return { provider: null, ...fallback };
32
+ if (option !== 'env') {
33
+ if (boundSender === null)
34
+ return { provider: null, ...fallback };
35
+ return { provider: (0, message_dispatcher_js_1.providerFromSender)(boundSender, option ? 'custom' : 'log'), ...fallback };
36
+ }
37
+ const env = deps.env ?? process.env;
38
+ const config = (0, message_config_js_1.loadMessageConfig)(env, {
39
+ resolvedFromSecretRef: boot_secrets_js_1.wasResolvedFromSecretRef,
40
+ readFile: deps.readFile ?? ((path) => (0, node_fs_1.readFileSync)(path, 'utf8')),
41
+ });
42
+ if (config.email.provider === 'disabled') {
43
+ logger.warn("emailSender: 'env' with EMAIL_PROVIDER=disabled — framework emails (password reset, " +
44
+ 'invites, notices) are NOT delivered.');
45
+ return { provider: null, ...fallback };
46
+ }
47
+ const messaging = (0, optional_peer_js_1.requireOptionalPeer)(MESSAGING_PACKAGE, "ShieldModule.forRoot({ emailSender: 'env' })", deps.load);
48
+ const provider = await messaging.createEmailProvider(config.email);
49
+ const timeoutMs = config.email.timeoutMs;
50
+ if (config.probe)
51
+ await probe(provider, timeoutMs);
52
+ logger.log(`Email delivery: ${provider.name}${config.probe ? ' (boot probe passed)' : ''}`);
53
+ return { provider, timeoutMs };
54
+ }
55
+ /** Reachability + credentials, no message (R8). Bounded; any failure refuses boot. */
56
+ async function probe(provider, timeoutMs) {
57
+ const fail = (detail) => new Error(`MESSAGE_BOOT_PROBE: the ${provider.name} check failed (${detail}); check EMAIL_PROVIDER ` +
58
+ 'and the provider host / credential variables');
59
+ if (!provider.probe)
60
+ return;
61
+ const signal = AbortSignal.timeout(timeoutMs);
62
+ let timer;
63
+ const expiry = new Promise((_, reject) => {
64
+ timer = setTimeout(() => reject(fail('timeout')), timeoutMs + 1_000);
65
+ timer.unref?.();
66
+ });
67
+ try {
68
+ await Promise.race([provider.probe({ signal }), expiry]);
69
+ }
70
+ catch (e) {
71
+ if (e instanceof core_1.MessageDeliveryError) {
72
+ const status = e.status !== undefined ? `, HTTP ${e.status}` : '';
73
+ throw fail(`${e.failureClass}${status}`);
74
+ }
75
+ if (signal.aborted)
76
+ throw fail('timeout');
77
+ if (e instanceof Error && e.message.startsWith('MESSAGE_BOOT_PROBE:'))
78
+ throw e;
79
+ throw fail('unreachable');
80
+ }
81
+ finally {
82
+ clearTimeout(timer);
83
+ }
84
+ }
85
+ /**
86
+ * The link-base boot rule applies only with a real sender (Phase 13 #1). `'env'` is real unless
87
+ * `EMAIL_PROVIDER=disabled`. Read at `forRoot` time, after `resolveBootSecrets()`.
88
+ */
89
+ function isMailDisabled(option, env) {
90
+ if (option === undefined || option === 'disabled')
91
+ return true;
92
+ if (option === 'env')
93
+ return env.EMAIL_PROVIDER?.trim() === 'disabled';
94
+ return false;
95
+ }
96
+ /**
97
+ * The `EmailSender` bound under `SHIELD_EMAIL_SENDER` for `'env'`, so existing injectors keep the
98
+ * port they know. Each send is bounded; a failure rejects (callers already treat it best-effort).
99
+ */
100
+ function senderFromProvider(provider, timeoutMs) {
101
+ return {
102
+ send: async (message) => {
103
+ await provider.send(message, { signal: AbortSignal.timeout(timeoutMs) });
104
+ },
105
+ };
106
+ }
107
+ /**
108
+ * `message.sent` / `message.failed` rows go to the framework audit log over guard's own pool
109
+ * (the runtime account holds append-only INSERT on the audit schema), independent of the
110
+ * optional app `auditSink` (Shield #6).
111
+ */
112
+ function messageAuditWriter(pool, schemas) {
113
+ const sink = new engine_1.MariaDbAuditSink({ schemas });
114
+ return {
115
+ record: (event) => sink.record(event, (sql, params) => pool.query(sql, params)),
116
+ };
117
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The one exit for framework messages (G-010, docs/features/message-delivery/SPEC.md R6, R9, R10;
3
+ * spec P-1). Every framework email — password reset, invite, email-change confirmation, security
4
+ * notices — goes through {@link MessageDispatcher.sendEmail}, which:
5
+ * - bounds the send (the provider gets an abort signal at `timeoutMs`; the call itself settles by
6
+ * `timeoutMs + graceMs` even if the provider ignores the signal);
7
+ * - records `message.sent` / `message.failed` with channel, provider, purpose, masked recipient
8
+ * and the provider's message id or the failure class — never the link, subject or body;
9
+ * - never throws, so a delivery failure never changes a public response.
10
+ *
11
+ * It wraps whatever is bound: an `@astram/messaging` adapter (`emailSender: 'env'`), an app's own
12
+ * `EmailSender` (provider `custom`) or the dev log sender (`log`). `'disabled'` binds no provider
13
+ * and audits nothing.
14
+ */
15
+ import { type AuditEvent, type DeliveryFailureClass, type EmailMessage, type EmailProvider, type MessagePurpose } from '@astram/core';
16
+ import type { EmailSender } from '../auth/ports/email-sender.js';
17
+ /** Who the message is about, for the audit row (from the session or the resolved credential). */
18
+ export interface MessageAuditContext {
19
+ readonly actorId: string;
20
+ readonly tenantId: string;
21
+ }
22
+ export type EmailDeliveryOutcome = {
23
+ readonly status: 'sent';
24
+ readonly provider: string;
25
+ readonly messageId?: string;
26
+ } | {
27
+ readonly status: 'failed';
28
+ readonly provider: string;
29
+ readonly failureClass: DeliveryFailureClass;
30
+ } | {
31
+ readonly status: 'disabled';
32
+ };
33
+ export interface MessageDispatcher {
34
+ /** `false` when mail is `'disabled'`: nothing can be delivered. */
35
+ readonly emailEnabled: boolean;
36
+ sendEmail(purpose: MessagePurpose, message: EmailMessage, context: MessageAuditContext): Promise<EmailDeliveryOutcome>;
37
+ }
38
+ /** Appends one audit event. Best-effort here: a failure is swallowed. */
39
+ export interface MessageAuditWriter {
40
+ record(event: AuditEvent): Promise<void>;
41
+ }
42
+ export interface MessageDispatcherOptions {
43
+ /** The per-send bound (`MESSAGE_SEND_TIMEOUT_MS`). */
44
+ readonly timeoutMs: number;
45
+ /** Extra time after the bound for a provider to honour the abort before the call settles. */
46
+ readonly graceMs?: number;
47
+ readonly now?: () => number;
48
+ }
49
+ export declare class GuardMessageDispatcher implements MessageDispatcher {
50
+ private readonly provider;
51
+ private readonly audit;
52
+ private readonly options;
53
+ readonly emailEnabled: boolean;
54
+ private readonly graceMs;
55
+ private readonly now;
56
+ constructor(provider: EmailProvider | null, audit: MessageAuditWriter | null, options: MessageDispatcherOptions);
57
+ sendEmail(purpose: MessagePurpose, message: EmailMessage, context: MessageAuditContext): Promise<EmailDeliveryOutcome>;
58
+ private bounded;
59
+ private record;
60
+ }
61
+ /**
62
+ * Adapt an app's `EmailSender` (or the dev log sender) to the provider port. The message passes
63
+ * through verbatim; the sender cannot be aborted, so the dispatcher's race settles the call.
64
+ */
65
+ export declare function providerFromSender(sender: EmailSender, name: string): EmailProvider;
66
+ //# sourceMappingURL=message-dispatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-dispatcher.d.ts","sourceRoot":"","sources":["../../src/messaging/message-dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAGjE,iGAAiG;AACjG,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACnF;IACE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;CAC7C,GACD;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;CAAE,CAAC;AAEpC,MAAM,WAAW,iBAAiB;IAChC,mEAAmE;IACnE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,SAAS,CACP,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAClC;AAED,yEAAyE;AACzE,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,wBAAwB;IACvC,sDAAsD;IACtD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,6FAA6F;IAC7F,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAMD,qBAAa,sBAAuB,YAAW,iBAAiB;IAM5D,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAP1B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAGhB,QAAQ,EAAE,aAAa,GAAG,IAAI,EAC9B,KAAK,EAAE,kBAAkB,GAAG,IAAI,EAChC,OAAO,EAAE,wBAAwB;IAO9C,SAAS,CACb,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,oBAAoB,CAAC;IA4BhC,OAAO,CAAC,OAAO;YAwBD,MAAM;CAiCrB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAQnF"}
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GuardMessageDispatcher = void 0;
4
+ exports.providerFromSender = providerFromSender;
5
+ /**
6
+ * The one exit for framework messages (G-010, docs/features/message-delivery/SPEC.md R6, R9, R10;
7
+ * spec P-1). Every framework email — password reset, invite, email-change confirmation, security
8
+ * notices — goes through {@link MessageDispatcher.sendEmail}, which:
9
+ * - bounds the send (the provider gets an abort signal at `timeoutMs`; the call itself settles by
10
+ * `timeoutMs + graceMs` even if the provider ignores the signal);
11
+ * - records `message.sent` / `message.failed` with channel, provider, purpose, masked recipient
12
+ * and the provider's message id or the failure class — never the link, subject or body;
13
+ * - never throws, so a delivery failure never changes a public response.
14
+ *
15
+ * It wraps whatever is bound: an `@astram/messaging` adapter (`emailSender: 'env'`), an app's own
16
+ * `EmailSender` (provider `custom`) or the dev log sender (`log`). `'disabled'` binds no provider
17
+ * and audits nothing.
18
+ */
19
+ const core_1 = require("@astram/core");
20
+ const email_address_js_1 = require("../auth/email-address.js");
21
+ const DEFAULT_GRACE_MS = 1_000;
22
+ class BoundExpired extends Error {
23
+ }
24
+ class GuardMessageDispatcher {
25
+ provider;
26
+ audit;
27
+ options;
28
+ emailEnabled;
29
+ graceMs;
30
+ now;
31
+ constructor(provider, audit, options) {
32
+ this.provider = provider;
33
+ this.audit = audit;
34
+ this.options = options;
35
+ this.emailEnabled = provider !== null;
36
+ this.graceMs = options.graceMs ?? DEFAULT_GRACE_MS;
37
+ this.now = options.now ?? Date.now;
38
+ }
39
+ async sendEmail(purpose, message, context) {
40
+ const provider = this.provider;
41
+ if (provider === null)
42
+ return { status: 'disabled' };
43
+ let outcome;
44
+ let status;
45
+ try {
46
+ const receipt = await this.bounded(provider, message);
47
+ outcome = {
48
+ status: 'sent',
49
+ provider: receipt.provider || provider.name,
50
+ ...(receipt.messageId ? { messageId: receipt.messageId } : {}),
51
+ };
52
+ }
53
+ catch (e) {
54
+ const failureClass = e instanceof BoundExpired
55
+ ? 'timeout'
56
+ : e instanceof core_1.MessageDeliveryError
57
+ ? e.failureClass
58
+ : 'transient';
59
+ if (e instanceof core_1.MessageDeliveryError)
60
+ status = e.status;
61
+ outcome = { status: 'failed', provider: provider.name, failureClass };
62
+ }
63
+ await this.record(purpose, message.to, context, outcome, status);
64
+ return outcome;
65
+ }
66
+ bounded(provider, message) {
67
+ const signal = AbortSignal.timeout(this.options.timeoutMs);
68
+ let timer;
69
+ const expiry = new Promise((_, reject) => {
70
+ timer = setTimeout(() => reject(new BoundExpired()), this.options.timeoutMs + this.graceMs);
71
+ timer.unref?.();
72
+ });
73
+ // Started inside a promise so a provider that throws synchronously still settles the race
74
+ // (never an orphaned expiry rejection, and the timer below is always cleared).
75
+ const send = Promise.resolve()
76
+ .then(() => provider.send(message, { signal }))
77
+ .catch((e) => {
78
+ // A provider that stopped on our abort reports it however it likes: it is the bound.
79
+ if (signal.aborted &&
80
+ !(e instanceof core_1.MessageDeliveryError && e.failureClass !== 'timeout')) {
81
+ throw new BoundExpired();
82
+ }
83
+ throw e;
84
+ });
85
+ return Promise.race([send, expiry]).finally(() => clearTimeout(timer));
86
+ }
87
+ async record(purpose, to, context, outcome, status) {
88
+ if (this.audit === null || outcome.status === 'disabled')
89
+ return;
90
+ const attributes = {
91
+ channel: 'email',
92
+ provider: outcome.provider,
93
+ purpose,
94
+ recipient: (0, email_address_js_1.maskEmail)(to),
95
+ };
96
+ if (outcome.status === 'sent') {
97
+ if (outcome.messageId)
98
+ attributes.messageId = outcome.messageId;
99
+ }
100
+ else {
101
+ attributes.failureClass = outcome.failureClass;
102
+ if (status !== undefined)
103
+ attributes.status = status;
104
+ }
105
+ try {
106
+ await this.audit.record({
107
+ type: purpose === 'invite' ? 'admin_action' : 'authentication',
108
+ occurredAtMs: this.now(),
109
+ actorId: context.actorId,
110
+ tenantId: context.tenantId,
111
+ action: { key: outcome.status === 'sent' ? 'message.sent' : 'message.failed' },
112
+ attributes,
113
+ });
114
+ }
115
+ catch {
116
+ // Best-effort: an audit outage never fails a delivery the user already got (or did not).
117
+ }
118
+ }
119
+ }
120
+ exports.GuardMessageDispatcher = GuardMessageDispatcher;
121
+ /**
122
+ * Adapt an app's `EmailSender` (or the dev log sender) to the provider port. The message passes
123
+ * through verbatim; the sender cannot be aborted, so the dispatcher's race settles the call.
124
+ */
125
+ function providerFromSender(sender, name) {
126
+ return {
127
+ name,
128
+ send: async (message) => {
129
+ await sender.send(message);
130
+ return { provider: name };
131
+ },
132
+ };
133
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The masked form of a phone number for audit rows (G-010 R9): `+`, the first two digits, one `*`
3
+ * per hidden digit, the last two digits (`+919876543210` → `+91********10`). A value that is not
4
+ * E.164 is never echoed (`***`). Emails use `maskEmail`.
5
+ */
6
+ export declare function maskPhone(value: string): string;
7
+ //# sourceMappingURL=recipient-mask.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipient-mask.d.ts","sourceRoot":"","sources":["../../src/messaging/recipient-mask.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAI/C"}
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.maskPhone = maskPhone;
4
+ const core_1 = require("@astram/core");
5
+ /**
6
+ * The masked form of a phone number for audit rows (G-010 R9): `+`, the first two digits, one `*`
7
+ * per hidden digit, the last two digits (`+919876543210` → `+91********10`). A value that is not
8
+ * E.164 is never echoed (`***`). Emails use `maskEmail`.
9
+ */
10
+ function maskPhone(value) {
11
+ if (!(0, core_1.isE164PhoneNumber)(value))
12
+ return '***';
13
+ const digits = value.slice(1);
14
+ return `+${digits.slice(0, 2)}${'*'.repeat(digits.length - 4)}${digits.slice(-2)}`;
15
+ }
@@ -115,8 +115,10 @@ export interface ShieldModuleOptions {
115
115
  * sender, or `'disabled'` (no-op, forgot-password still 204). REQUIRED under
116
116
  * `auth: true` unless the raw `NODE_ENV` is `development`/`test` (where the dev
117
117
  * `LogEmailSender` is the default); otherwise boot fails (security hardening #6).
118
+ * `'env'` (G-010) reads the provider from `EMAIL_PROVIDER` and the `EMAIL_*` / provider
119
+ * variables (needs the `@astram/messaging` package; runbook §6).
118
120
  */
119
- readonly emailSender?: EmailSender | 'disabled';
121
+ readonly emailSender?: EmailSender | 'disabled' | 'env';
120
122
  readonly recoveryCodeStore?: RecoveryCodeStore;
121
123
  readonly profileWriter?: ProfileWriter;
122
124
  readonly sessionSweeper?: SessionSweeper;
@@ -248,8 +250,8 @@ export interface ShieldModuleAsyncOptions {
248
250
  */
249
251
  readonly credentialWriter?: CredentialWriter;
250
252
  readonly resetTokenStore?: ResetTokenStore;
251
- /** See {@link ShieldModuleOptions.emailSender} (a sender, or `'disabled'`). */
252
- readonly emailSender?: EmailSender | 'disabled';
253
+ /** See {@link ShieldModuleOptions.emailSender} (a sender, `'disabled'` or `'env'`). */
254
+ readonly emailSender?: EmailSender | 'disabled' | 'env';
253
255
  readonly recoveryCodeStore?: RecoveryCodeStore;
254
256
  readonly profileWriter?: ProfileWriter;
255
257
  readonly sessionSweeper?: SessionSweeper;
@@ -1 +1 @@
1
- {"version":3,"file":"shield.module.d.ts","sourceRoot":"","sources":["../../src/module/shield.module.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"shield.module.d.ts","sourceRoot":"","sources":["../../src/module/shield.module.ts"],"names":[],"mappings":"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"}
@@ -49,6 +49,7 @@ const boot_module_js_1 = require("../boot/boot.module.js");
49
49
  const migration_ledger_check_js_1 = require("../boot/migration-ledger-check.js");
50
50
  const config_module_js_1 = require("../config/config.module.js");
51
51
  const app_config_js_1 = require("../config/app-config.js");
52
+ const reserved_principal_ids_js_1 = require("../config/reserved-principal-ids.js");
52
53
  /**
53
54
  * `FRAMEWORK_SCHEMAS` from `APP_CONFIG` (configurable-framework-db, ADR-0037): resolved once
54
55
  * at boot, the same frozen names every framework store and module receives.
@@ -226,6 +227,8 @@ function loadEnvAppConfig() {
226
227
  let ShieldModule = ShieldModule_1 = class ShieldModule {
227
228
  /** Register Shield with static options (the common path). */
228
229
  static forRoot(options = {}) {
230
+ // G-009 N4: a reserved operation principal is never a root, however `rootIds` was built.
231
+ (0, reserved_principal_ids_js_1.assertNoReservedRootId)(options.rootIds);
229
232
  const config = options.config ?? loadEnvAppConfig();
230
233
  // ADR-0035: with auth on and no explicit projector, sessions cache the facts the framework-built
231
234
  // authority decides on (attached when the authority is built; inactive until then).
@@ -337,6 +340,7 @@ let ShieldModule = ShieldModule_1 = class ShieldModule {
337
340
  * unchanged whether config is static or computed.
338
341
  */
339
342
  static forRootAsync(options) {
343
+ (0, reserved_principal_ids_js_1.assertNoReservedRootId)(options.rootIds); // G-009 N4 (the static allowlist; see forRoot)
340
344
  const optionsToken = Symbol('SHIELD_MODULE_OPTIONS');
341
345
  const asyncOptionsProvider = {
342
346
  provide: optionsToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astram/guard",
3
- "version": "0.16.0",
3
+ "version": "0.18.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,17 +36,21 @@
36
36
  "argon2": "^0.41.1",
37
37
  "ioredis": "^5.4.1",
38
38
  "mariadb": "^3.5.4",
39
- "@astram/core": "1.8.0",
40
- "@astram/engine": "0.9.0"
39
+ "@astram/core": "1.9.0",
40
+ "@astram/engine": "0.10.1"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@nestjs/common": "^11",
44
44
  "@nestjs/core": "^11",
45
45
  "@nestjs/platform-express": "^11",
46
46
  "reflect-metadata": "^0.2",
47
- "rxjs": "^7"
47
+ "rxjs": "^7",
48
+ "@astram/messaging": "^0.1.1"
48
49
  },
49
50
  "peerDependenciesMeta": {
51
+ "@astram/messaging": {
52
+ "optional": true
53
+ },
50
54
  "@nestjs/platform-express": {
51
55
  "optional": true
52
56
  }
@@ -63,7 +67,8 @@
63
67
  "supertest": "^7.0.0",
64
68
  "typescript": "^5.7.0",
65
69
  "unplugin-swc": "^1.5.1",
66
- "vitest": "^2.1.8"
70
+ "vitest": "^2.1.8",
71
+ "@astram/messaging": "0.1.1"
67
72
  },
68
73
  "scripts": {
69
74
  "build": "tsc -p tsconfig.build.json",