@endora-commerce/mod-auth 0.0.0-stage → 0.100.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 (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -2
  3. package/dist/backend/entities/session.entity.d.ts +31 -0
  4. package/dist/backend/entities/session.entity.d.ts.map +1 -0
  5. package/dist/backend/entities/session.entity.js +95 -0
  6. package/dist/backend/entities/session.entity.js.map +1 -0
  7. package/dist/backend/index.d.ts +120 -0
  8. package/dist/backend/index.d.ts.map +1 -0
  9. package/dist/backend/index.js +194 -0
  10. package/dist/backend/index.js.map +1 -0
  11. package/dist/backend/plugin.d.ts +41 -0
  12. package/dist/backend/plugin.d.ts.map +1 -0
  13. package/dist/backend/plugin.js +164 -0
  14. package/dist/backend/plugin.js.map +1 -0
  15. package/dist/backend/require-admin.d.ts +35 -0
  16. package/dist/backend/require-admin.d.ts.map +1 -0
  17. package/dist/backend/require-admin.js +50 -0
  18. package/dist/backend/require-admin.js.map +1 -0
  19. package/dist/backend/require-customer.d.ts +4 -0
  20. package/dist/backend/require-customer.d.ts.map +1 -0
  21. package/dist/backend/require-customer.js +16 -0
  22. package/dist/backend/require-customer.js.map +1 -0
  23. package/dist/backend/services/session-port.d.ts +30 -0
  24. package/dist/backend/services/session-port.d.ts.map +1 -0
  25. package/dist/backend/services/session-port.js +93 -0
  26. package/dist/backend/services/session-port.js.map +1 -0
  27. package/dist/backend/services/session-service.d.ts +56 -0
  28. package/dist/backend/services/session-service.d.ts.map +1 -0
  29. package/dist/backend/services/session-service.js +200 -0
  30. package/dist/backend/services/session-service.js.map +1 -0
  31. package/dist/manifest.d.ts +169 -0
  32. package/dist/manifest.d.ts.map +1 -0
  33. package/dist/manifest.js +71 -0
  34. package/dist/manifest.js.map +1 -0
  35. package/docs/auth.md +42 -0
  36. package/package.json +60 -3
@@ -0,0 +1,164 @@
1
+ import fastifyPlugin from 'fastify-plugin';
2
+ import { SESSION_COOKIE_NAME, ADMIN_SESSION_COOKIE_NAME, } from '@endora-commerce/contracts';
3
+ // The `declare module 'fastify'` block that puts `actor` and `adminActor` on
4
+ // `FastifyRequest` is the platform's since T118b
5
+ // (`packages/platform/src/http/request-actor.ts`), and this side-effect import
6
+ // is what puts it in *this* module's program — the writes below are the reason
7
+ // this file needs it, and they are the only writes in the tree.
8
+ import '@endora-commerce/platform/http';
9
+ /*
10
+ * Customer (storefront) sessions live in `b2b_session`; admin (Admin UI)
11
+ * sessions live in a **separate** `b2b_admin_session` cookie. Using two distinct
12
+ * cookie names lets a customer stay signed in on the storefront while an admin is
13
+ * signed in on the Admin UI in the same browser — on a shared host (e.g. all
14
+ * `localhost` ports) a single cookie name would clobber the other on every login.
15
+ *
16
+ * The two spellings moved to `@endora-commerce/contracts` in feature 075's Phase
17
+ * P. This file re-exported them for the consumers Phase C had not reached; that
18
+ * re-export is gone with the move to a package (T040b) because **there were
19
+ * none left** — every one of the five modules that set or clear the cookie
20
+ * already names the contracts package, which is what packaging them forced.
21
+ * Keeping it would also have put a `@endora-commerce/contracts` re-export on
22
+ * this package's `./backend` barrel, which `module-package-entity-surface.test.ts`
23
+ * refuses under D-168: it cannot follow the specifier, so whether a named entity
24
+ * class comes through it is unknown rather than false.
25
+ */
26
+ /**
27
+ * If the request carries a valid admin session (resolved into `request.adminActor`
28
+ * by the auth plugin) but its ambient `request.actor` is not already an admin,
29
+ * promote the admin candidate. Admin guards call this so an admin route works
30
+ * even when a customer session is simultaneously present on the same request.
31
+ */
32
+ export function promoteAdminActor(request) {
33
+ const r = request;
34
+ if (request.actor.kind !== 'admin' && r.adminActor) {
35
+ request.actor = r.adminActor;
36
+ }
37
+ }
38
+ async function authPluginImpl(app, opts) {
39
+ const cookieName = opts.cookieName ?? SESSION_COOKIE_NAME;
40
+ const adminCookieName = opts.adminCookieName ?? ADMIN_SESSION_COOKIE_NAME;
41
+ // Fastify 5 forbids reference-type defaults on decorateRequest (they'd be
42
+ // shared across requests). Use a getter / setter pair backed by a
43
+ // per-request symbol so each request gets its own slot; the onRequest
44
+ // hook below seeds the value.
45
+ const actorSlot = Symbol('b2b-auth.actor');
46
+ app.decorateRequest('actor', {
47
+ getter() {
48
+ const self = this;
49
+ return self[actorSlot] ?? { kind: 'anonymous' };
50
+ },
51
+ setter(value) {
52
+ this[actorSlot] = value;
53
+ },
54
+ });
55
+ // Admin session candidate, resolved from the admin cookie independently of the
56
+ // customer actor so both can coexist on one request.
57
+ const adminActorSlot = Symbol('b2b-auth.adminActor');
58
+ app.decorateRequest('adminActor', {
59
+ getter() {
60
+ const self = this;
61
+ return self[adminActorSlot] ?? null;
62
+ },
63
+ setter(value) {
64
+ this[adminActorSlot] = value;
65
+ },
66
+ });
67
+ app.addHook('onRequest', async (request) => {
68
+ // Initialise to anonymous; downstream branches may overwrite with a
69
+ // typed customer / admin / api_key actor.
70
+ request.actor = { kind: 'anonymous' };
71
+ request.adminActor = null;
72
+ // 1. API key (Bearer) beats cookie — integrations pass Authorization: Bearer ...
73
+ const authHeader = request.headers.authorization;
74
+ if (authHeader?.startsWith('Bearer ') && opts.apiKeyResolver) {
75
+ const token = authHeader.slice('Bearer '.length).trim();
76
+ const resolved = await opts.apiKeyResolver(token);
77
+ if (resolved) {
78
+ request.actor = { kind: 'api_key', ...resolved };
79
+ return;
80
+ }
81
+ // Unknown bearer token — leave as anonymous; route-level guards will reject.
82
+ return;
83
+ }
84
+ const cookies = request.cookies;
85
+ // 2a. Admin cookie → admin session candidate (kept separate so it can
86
+ // coexist with a customer session in the same browser).
87
+ const adminRaw = cookies?.[adminCookieName];
88
+ if (adminRaw) {
89
+ const resolvedAdmin = await opts.sessionService.loadSession(adminRaw);
90
+ if (resolvedAdmin?.kind === 'admin' && resolvedAdmin.session.adminUserId) {
91
+ request.adminActor = {
92
+ kind: 'admin',
93
+ adminUserId: resolvedAdmin.session.adminUserId,
94
+ };
95
+ }
96
+ }
97
+ // 2b. Customer cookie → the ambient actor for public + customer routes.
98
+ const raw = cookies?.[cookieName];
99
+ if (raw) {
100
+ const resolved = await opts.sessionService.loadSession(raw);
101
+ if (resolved) {
102
+ const session = resolved.session;
103
+ if (resolved.kind === 'impersonation' && session.customerAccountId) {
104
+ const orgId = opts.customerOrgResolver
105
+ ? (await opts.customerOrgResolver(session.customerAccountId)) ?? ''
106
+ : '';
107
+ request.actor = {
108
+ kind: 'customer',
109
+ customerAccountId: session.customerAccountId,
110
+ organizationId: orgId,
111
+ impersonatorAdminUserId: session.impersonatorAdminUserId ?? null,
112
+ };
113
+ }
114
+ else if (resolved.kind === 'customer' && session.customerAccountId) {
115
+ const orgId = opts.customerOrgResolver
116
+ ? (await opts.customerOrgResolver(session.customerAccountId)) ?? ''
117
+ : '';
118
+ request.actor = {
119
+ kind: 'customer',
120
+ customerAccountId: session.customerAccountId,
121
+ organizationId: orgId,
122
+ impersonatorAdminUserId: null,
123
+ };
124
+ }
125
+ // An admin-kind session in the customer cookie (legacy) is ignored;
126
+ // admins now authenticate via the dedicated admin cookie.
127
+ // Feature 040 — keep presence fresh: stamp `lastSeenAt` on every active
128
+ // customer request (throttled to once/minute inside the service via a
129
+ // Redis marker). Without this, the admin "online customers" view only
130
+ // reflects login time, so an actively-browsing buyer ages out of the
131
+ // freshness window and disappears. Fire-and-forget: never block or fail
132
+ // the request on a presence write.
133
+ if ((resolved.kind === 'customer' || resolved.kind === 'impersonation') &&
134
+ session.customerAccountId) {
135
+ void opts.sessionService.touchLastSeen(session.id).catch(() => undefined);
136
+ }
137
+ }
138
+ }
139
+ // 3. If no customer/anonymous identity was established but an admin session
140
+ // exists, expose it as the ambient actor too, so admin flows that read
141
+ // `request.actor` without first running an admin guard still resolve.
142
+ if (request.actor.kind === 'anonymous' && request.adminActor) {
143
+ request.actor = request.adminActor;
144
+ }
145
+ });
146
+ }
147
+ export const authPlugin = fastifyPlugin(authPluginImpl, {
148
+ name: 'b2b-auth',
149
+ fastify: '5.x',
150
+ });
151
+ /*
152
+ * Three `FastifyInstance` decorators lived here — `requireCustomer`,
153
+ * `requireAdmin`, `requireApiKey` — and were deleted with this module's
154
+ * conversion (feature 072, T078). **Nothing in `src/` or `test/` called any of
155
+ * them.**
156
+ *
157
+ * Worth recording rather than deleting silently, because the `requireAdmin` one
158
+ * read as a live authorisation hole: its body accepted any admin regardless of
159
+ * the permission code, under a comment promising the real check "in T188". It
160
+ * was not a hole — the guard every route actually uses is `createRequireAdmin`
161
+ * in `require-admin.ts`, which checks `permissionService.hasPermission`. The
162
+ * decorator was dead code that looked dangerous, which is its own kind of cost.
163
+ */
164
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/backend/plugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { AdminPermissionChecker, RequireAdminAnyFactory, RequireAdminFactory } from '@endora-commerce/platform/kernel';
2
+ /**
3
+ * The admin guard (feature 072, D-32). **One implementation**, shared by
4
+ * production and the test harness.
5
+ *
6
+ * Before this file there were two. Production read `request.actor`, called
7
+ * `promoteAdminActor` and checked `permissionService.hasPermission`
8
+ * (`composition.ts`); the harness read `request.testActor` and took
9
+ * `permissionService` as **optional**, so omitting it silently disabled every
10
+ * permission check across 205 call sites in 60 modules. The harness happens to
11
+ * pass a `PermissionService` at all 28 of its wiring sites, so the checks did
12
+ * run — but nothing made them, and the two guards still differed in what they
13
+ * read and in whether an admin session riding alongside a customer session was
14
+ * promoted.
15
+ *
16
+ * `auth` owns this because promotion needs the auth plugin's per-request
17
+ * decorations, and the permission check needs `admin_roles` — which `auth`'s
18
+ * manifest now declares as a dependency. The kernel owns only the type.
19
+ */
20
+ export interface RequireAdminDeps {
21
+ /** `admin_roles`' `PermissionService`, narrowed to what the guard uses. */
22
+ readonly permissionService: AdminPermissionChecker;
23
+ }
24
+ /** Gate a route on an admin session, optionally holding one permission code. */
25
+ export declare function createRequireAdmin({ permissionService }: RequireAdminDeps): RequireAdminFactory;
26
+ /**
27
+ * Gate a route on an admin session holding **any** of the listed codes.
28
+ *
29
+ * Lives here rather than in `http/` for the same reason as the single-code
30
+ * factory: it needs `promoteAdminActor` and a permission checker, so keeping it
31
+ * under `http/` made the HTTP layer import two domain modules — two of the five
32
+ * infrastructure→module edges this feature removes.
33
+ */
34
+ export declare function createRequireAdminAny({ permissionService, }: RequireAdminDeps): RequireAdminAnyFactory;
35
+ //# sourceMappingURL=require-admin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-admin.d.ts","sourceRoot":"","sources":["../../src/backend/require-admin.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACpB,MAAM,kCAAkC,CAAC;AAG1C;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,2EAA2E;IAC3E,QAAQ,CAAC,iBAAiB,EAAE,sBAAsB,CAAC;CACpD;AAkBD,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,EAAE,iBAAiB,EAAE,EAAE,gBAAgB,GAAG,mBAAmB,CAa/F;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,iBAAiB,GAClB,EAAE,gBAAgB,GAAG,sBAAsB,CAgB3C"}
@@ -0,0 +1,50 @@
1
+ import { ERROR_CODES } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ import { promoteAdminActor } from './plugin.js';
4
+ function resolveAdminActor(request) {
5
+ // `promoteAdminActor` dereferences `request.actor`. In production it is a
6
+ // decorated getter that always answers; a composition root that mounts a
7
+ // route without an actor resolver would otherwise turn a 401 into a 500.
8
+ const carrier = request;
9
+ if (carrier.actor !== undefined)
10
+ promoteAdminActor(request);
11
+ const actor = carrier.actor;
12
+ return actor?.kind === 'admin' ? actor : null;
13
+ }
14
+ /** Gate a route on an admin session, optionally holding one permission code. */
15
+ export function createRequireAdmin({ permissionService }) {
16
+ return (permission) => async (request) => {
17
+ const actor = resolveAdminActor(request);
18
+ if (!actor) {
19
+ throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'Admin session required.');
20
+ }
21
+ if (!permission)
22
+ return;
23
+ const ok = await permissionService.hasPermission(actor.adminUserId, permission);
24
+ if (!ok) {
25
+ throw new HttpError(403, ERROR_CODES.FORBIDDEN, `Missing permission: ${permission}.`);
26
+ }
27
+ };
28
+ }
29
+ /**
30
+ * Gate a route on an admin session holding **any** of the listed codes.
31
+ *
32
+ * Lives here rather than in `http/` for the same reason as the single-code
33
+ * factory: it needs `promoteAdminActor` and a permission checker, so keeping it
34
+ * under `http/` made the HTTP layer import two domain modules — two of the five
35
+ * infrastructure→module edges this feature removes.
36
+ */
37
+ export function createRequireAdminAny({ permissionService, }) {
38
+ return (codes) => async (request) => {
39
+ const actor = resolveAdminActor(request);
40
+ if (!actor) {
41
+ throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'Admin session required.');
42
+ }
43
+ for (const code of codes) {
44
+ if (await permissionService.hasPermission(actor.adminUserId, code))
45
+ return;
46
+ }
47
+ throw new HttpError(403, ERROR_CODES.FORBIDDEN, `Missing permission: one of ${codes.join(', ')}.`);
48
+ };
49
+ }
50
+ //# sourceMappingURL=require-admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-admin.js","sourceRoot":"","sources":["../../src/backend/require-admin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAM3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAgChD,SAAS,iBAAiB,CAAC,OAAuB;IAChD,0EAA0E;IAC1E,yEAAyE;IACzE,yEAAyE;IACzE,MAAM,OAAO,GAAG,OAAuD,CAAC;IACxE,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS;QAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAC5B,OAAO,KAAK,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAChD,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,kBAAkB,CAAC,EAAE,iBAAiB,EAAoB;IACxE,OAAO,CAAC,UAAmB,EAAE,EAAE,CAC7B,KAAK,EAAE,OAAO,EAAiB,EAAE;QAC/B,MAAM,KAAK,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,YAAY,EAAE,yBAAyB,CAAC,CAAC;QAChF,CAAC;QACD,IAAI,CAAC,UAAU;YAAE,OAAO;QACxB,MAAM,EAAE,GAAG,MAAM,iBAAiB,CAAC,aAAa,CAAC,KAAK,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;QAChF,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,SAAS,EAAE,uBAAuB,UAAU,GAAG,CAAC,CAAC;QACxF,CAAC;IACH,CAAC,CAAC;AACN,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,EACpC,iBAAiB,GACA;IACjB,OAAO,CAAC,KAAK,EAAE,EAAE,CACf,KAAK,EAAE,OAAO,EAAiB,EAAE;QAC/B,MAAM,KAAK,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,YAAY,EAAE,yBAAyB,CAAC,CAAC;QAChF,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,MAAM,iBAAiB,CAAC,aAAa,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC;gBAAE,OAAO;QAC7E,CAAC;QACD,MAAM,IAAI,SAAS,CACjB,GAAG,EACH,WAAW,CAAC,SAAS,EACrB,8BAA8B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAClD,CAAC;IACJ,CAAC,CAAC;AACN,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { RequireCustomerGuard } from '@endora-commerce/platform/kernel';
2
+ /** Gate a route on an authenticated Customer session. */
3
+ export declare function createRequireCustomer(): RequireCustomerGuard;
4
+ //# sourceMappingURL=require-customer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-customer.d.ts","sourceRoot":"","sources":["../../src/backend/require-customer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AAwC7E,yDAAyD;AACzD,wBAAgB,qBAAqB,IAAI,oBAAoB,CAW5D"}
@@ -0,0 +1,16 @@
1
+ import { ERROR_CODES } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ /** Gate a route on an authenticated Customer session. */
4
+ export function createRequireCustomer() {
5
+ return async (request) => {
6
+ // In production `actor` is a decorated getter that always answers. A
7
+ // composition root that mounts a route without an actor resolver would
8
+ // otherwise turn a 401 into a 500 — the same guard `createRequireAdmin`
9
+ // puts in front of `promoteAdminActor`.
10
+ const actor = request.actor;
11
+ if (actor?.kind !== 'customer') {
12
+ throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'Customer session required.');
13
+ }
14
+ };
15
+ }
16
+ //# sourceMappingURL=require-customer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-customer.js","sourceRoot":"","sources":["../../src/backend/require-customer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAyC3D,yDAAyD;AACzD,MAAM,UAAU,qBAAqB;IACnC,OAAO,KAAK,EAAE,OAAuB,EAAiB,EAAE;QACtD,qEAAqE;QACrE,uEAAuE;QACvE,wEAAwE;QACxE,wCAAwC;QACxC,MAAM,KAAK,GAAI,OAA2D,CAAC,KAAK,CAAC;QACjF,IAAI,KAAK,EAAE,IAAI,KAAK,UAAU,EAAE,CAAC;YAC/B,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,YAAY,EAAE,4BAA4B,CAAC,CAAC;QACnF,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,30 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { AuthCustomerLastSeen, AuthSessionKind, AuthSessionPort, AuthSessionReadPort, AuthSessionRecord } from '@endora-commerce/contracts';
3
+ import { Session } from '../entities/session.entity.js';
4
+ import type { SessionService } from './session-service.js';
5
+ /**
6
+ * The published face of `auth`'s session machinery (feature 075, Phase P).
7
+ *
8
+ * `SessionService` is not itself the port, and the difference is one field:
9
+ * its `createSession` / `loadSession` hand back the `Session` **entity**, and
10
+ * an entity crossing a module boundary is the problem this feature exists to
11
+ * remove — publishing a port that hands one over would only rename it
12
+ * (contracts/port-publication.md §1.2). So the adapter below maps the row into
13
+ * `AuthSessionRecord`, which drops `tokenHash`: no consumer reads it, and it
14
+ * is the one field that must not travel.
15
+ *
16
+ * The adapter is additive. `sessionService` keeps its registration for the
17
+ * consumers Phase C has not reached yet.
18
+ */
19
+ export declare function toAuthSessionRecord(session: Session, kind: AuthSessionKind): AuthSessionRecord;
20
+ export declare function createAuthSessionPort(sessionService: SessionService): AuthSessionPort;
21
+ /**
22
+ * The reporting half. `customers`' online-customers view is the only consumer,
23
+ * and it asks exactly one question: when was each of these accounts last seen?
24
+ */
25
+ export declare class AuthSessionReadService implements AuthSessionReadPort {
26
+ private readonly emFactory;
27
+ constructor(emFactory: () => EntityManager);
28
+ lastSeenByCustomerAccount(customerAccountIds: readonly string[], since: Date): Promise<AuthCustomerLastSeen[]>;
29
+ }
30
+ //# sourceMappingURL=session-port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-port.d.ts","sourceRoot":"","sources":["../../../src/backend/services/session-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EACV,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;;;;;;;;;;;GAaG;AAEH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,GAAG,iBAAiB,CAc9F;AASD,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,cAAc,GAAG,eAAe,CA0BrF;AAED;;;GAGG;AACH,qBAAa,sBAAuB,YAAW,mBAAmB;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,yBAAyB,CAC7B,kBAAkB,EAAE,SAAS,MAAM,EAAE,EACrC,KAAK,EAAE,IAAI,GACV,OAAO,CAAC,oBAAoB,EAAE,CAAC;CAmBnC"}
@@ -0,0 +1,93 @@
1
+ import { Session } from '../entities/session.entity.js';
2
+ /**
3
+ * The published face of `auth`'s session machinery (feature 075, Phase P).
4
+ *
5
+ * `SessionService` is not itself the port, and the difference is one field:
6
+ * its `createSession` / `loadSession` hand back the `Session` **entity**, and
7
+ * an entity crossing a module boundary is the problem this feature exists to
8
+ * remove — publishing a port that hands one over would only rename it
9
+ * (contracts/port-publication.md §1.2). So the adapter below maps the row into
10
+ * `AuthSessionRecord`, which drops `tokenHash`: no consumer reads it, and it
11
+ * is the one field that must not travel.
12
+ *
13
+ * The adapter is additive. `sessionService` keeps its registration for the
14
+ * consumers Phase C has not reached yet.
15
+ */
16
+ export function toAuthSessionRecord(session, kind) {
17
+ return {
18
+ id: session.id,
19
+ kind,
20
+ customerAccountId: session.customerAccountId ?? null,
21
+ adminUserId: session.adminUserId ?? null,
22
+ impersonatorAdminUserId: session.impersonatorAdminUserId ?? null,
23
+ expiresAt: session.expiresAt,
24
+ createdAt: session.createdAt,
25
+ updatedAt: session.updatedAt,
26
+ lastSeenAt: session.lastSeenAt ?? null,
27
+ ipAddress: session.ipAddress ?? null,
28
+ userAgent: session.userAgent ?? null,
29
+ };
30
+ }
31
+ /** Same derivation `SessionService` applies internally, over the record's fields. */
32
+ function kindOf(session) {
33
+ if (session.impersonatorAdminUserId)
34
+ return 'impersonation';
35
+ if (session.adminUserId)
36
+ return 'admin';
37
+ return 'customer';
38
+ }
39
+ export function createAuthSessionPort(sessionService) {
40
+ return {
41
+ async createSession(input) {
42
+ const created = await sessionService.createSession(input);
43
+ return {
44
+ cookieValue: created.cookieValue,
45
+ expiresAt: created.expiresAt,
46
+ session: toAuthSessionRecord(created.session, kindOf(created.session)),
47
+ };
48
+ },
49
+ async loadSession(cookieValue) {
50
+ const resolved = await sessionService.loadSession(cookieValue);
51
+ if (!resolved)
52
+ return null;
53
+ return {
54
+ kind: resolved.kind,
55
+ session: toAuthSessionRecord(resolved.session, resolved.kind),
56
+ };
57
+ },
58
+ destroySession: (sessionId) => sessionService.destroySession(sessionId),
59
+ destroyAllForCustomer: (customerAccountId) => sessionService.destroyAllForCustomer(customerAccountId),
60
+ destroyAllForAdmin: (adminUserId) => sessionService.destroyAllForAdmin(adminUserId),
61
+ touchLastSeen: (sessionId) => sessionService.touchLastSeen(sessionId),
62
+ listRecentlyActiveCustomers: (windowMinutes) => sessionService.listRecentlyActiveCustomers(windowMinutes),
63
+ };
64
+ }
65
+ /**
66
+ * The reporting half. `customers`' online-customers view is the only consumer,
67
+ * and it asks exactly one question: when was each of these accounts last seen?
68
+ */
69
+ export class AuthSessionReadService {
70
+ emFactory;
71
+ constructor(emFactory) {
72
+ this.emFactory = emFactory;
73
+ }
74
+ async lastSeenByCustomerAccount(customerAccountIds, since) {
75
+ if (customerAccountIds.length === 0)
76
+ return [];
77
+ const em = this.emFactory();
78
+ const rows = await em.find(Session, { customerAccountId: { $in: [...customerAccountIds] }, lastSeenAt: { $gte: since } }, { fields: ['customerAccountId', 'lastSeenAt'] });
79
+ const newest = new Map();
80
+ for (const row of rows) {
81
+ if (!row.customerAccountId || !row.lastSeenAt)
82
+ continue;
83
+ const previous = newest.get(row.customerAccountId);
84
+ if (!previous || row.lastSeenAt > previous)
85
+ newest.set(row.customerAccountId, row.lastSeenAt);
86
+ }
87
+ return [...newest].map(([customerAccountId, lastSeenAt]) => ({
88
+ customerAccountId,
89
+ lastSeenAt,
90
+ }));
91
+ }
92
+ }
93
+ //# sourceMappingURL=session-port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-port.js","sourceRoot":"","sources":["../../../src/backend/services/session-port.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { Redis } from 'ioredis';
3
+ import { Session } from '../entities/session.entity.js';
4
+ export type SessionKind = 'customer' | 'admin' | 'impersonation';
5
+ export interface CreateSessionInput {
6
+ kind: SessionKind;
7
+ customerAccountId?: string;
8
+ adminUserId?: string;
9
+ impersonatorAdminUserId?: string;
10
+ ipAddress?: string;
11
+ userAgent?: string;
12
+ }
13
+ export interface SessionCookiePayload {
14
+ cookieValue: string;
15
+ expiresAt: Date;
16
+ session: Session;
17
+ }
18
+ export interface ResolvedSession {
19
+ session: Session;
20
+ kind: SessionKind;
21
+ }
22
+ export declare class SessionService {
23
+ private readonly emFactory;
24
+ private readonly redis;
25
+ constructor(emFactory: () => EntityManager, redis: Redis);
26
+ createSession(input: CreateSessionInput): Promise<SessionCookiePayload>;
27
+ loadSession(cookieValue: string): Promise<ResolvedSession | null>;
28
+ destroySession(sessionId: string): Promise<void>;
29
+ /**
30
+ * Revoke every active session for a Customer — used when an account is
31
+ * blocked or soft-deleted so it loses access promptly (feature 040,
32
+ * FR-016/SC-002). Clears both the Postgres rows and their Redis caches.
33
+ */
34
+ destroyAllForCustomer(customerAccountId: string): Promise<void>;
35
+ /**
36
+ * Revoke every session an admin user holds — both the sessions they signed
37
+ * in with and the impersonations they started, which carry the admin id in
38
+ * `impersonatorAdminUserId` and a customer id in `customerAccountId`. Used
39
+ * when a peer resets their password (issue #252): a session that survives
40
+ * the reset is a credential the reset did not withdraw, which is exactly
41
+ * what the compromised-account case needs withdrawn.
42
+ */
43
+ destroyAllForAdmin(adminUserId: string): Promise<void>;
44
+ /**
45
+ * Feature 040 — stamp activity, throttled to at most once per minute per
46
+ * session (Redis marker) to avoid write amplification on every request.
47
+ */
48
+ touchLastSeen(sessionId: string): Promise<void>;
49
+ /**
50
+ * Feature 040 — distinct customer ids whose session was active within the
51
+ * freshness window, for the admin "online customers" view.
52
+ */
53
+ listRecentlyActiveCustomers(windowMinutes: number): Promise<string[]>;
54
+ private cache;
55
+ }
56
+ //# sourceMappingURL=session-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/session-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAaxD,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,OAAO,GAAG,eAAe,CAAC;AAEjE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,WAAW,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,qBAAa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADL,SAAS,EAAE,MAAM,aAAa,EAC9B,KAAK,EAAE,KAAK;IAGzB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAgCvE,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IA2BjE,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWtD;;;;OAIG;IACG,qBAAqB,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYrE;;;;;;;OAOG;IACG,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAe5D;;;OAGG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUrD;;;OAGG;IACG,2BAA2B,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;YAW7D,KAAK;CASpB"}