@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,200 @@
1
+ import { createHash, randomBytes } from 'crypto';
2
+ import { Session } from '../entities/session.entity.js';
3
+ /**
4
+ * Session lifecycle. Persists to Postgres as the source of truth; caches hot reads in
5
+ * Redis to cut per-request latency (R-11).
6
+ *
7
+ * Cookie format: `${sessionId}.${rawToken}`. We store the SHA-256 of rawToken as
8
+ * `tokenHash`. Lookups validate sessionId → row → timing-safe equal on the hash.
9
+ */
10
+ const SESSION_TTL_SECONDS = 60 * 60 * 24 * 30; // 30 days
11
+ const REDIS_KEY_PREFIX = 'session:';
12
+ export class SessionService {
13
+ emFactory;
14
+ redis;
15
+ constructor(emFactory, redis) {
16
+ this.emFactory = emFactory;
17
+ this.redis = redis;
18
+ }
19
+ async createSession(input) {
20
+ // command-coverage-ignore: session lifecycle — mints an auth session; auth
21
+ // infrastructure (owned by SessionService), not an audited domain write.
22
+ const em = this.emFactory();
23
+ const rawToken = randomBytes(32).toString('base64url');
24
+ const tokenHash = sha256Hex(rawToken);
25
+ const expiresAt = new Date(Date.now() + SESSION_TTL_SECONDS * 1_000);
26
+ const session = em.create(Session, {
27
+ tokenHash,
28
+ expiresAt,
29
+ // Feature 040 — stamp activity at creation so a freshly-authenticated
30
+ // customer shows up in the admin "online" view immediately.
31
+ lastSeenAt: new Date(),
32
+ ...(input.customerAccountId !== undefined ? { customerAccountId: input.customerAccountId } : {}),
33
+ ...(input.adminUserId !== undefined ? { adminUserId: input.adminUserId } : {}),
34
+ ...(input.impersonatorAdminUserId !== undefined
35
+ ? { impersonatorAdminUserId: input.impersonatorAdminUserId }
36
+ : {}),
37
+ ...(input.ipAddress !== undefined ? { ipAddress: input.ipAddress } : {}),
38
+ ...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
39
+ });
40
+ await em.persistAndFlush(session);
41
+ await this.cache(session);
42
+ return {
43
+ cookieValue: `${session.id}.${rawToken}`,
44
+ expiresAt,
45
+ session,
46
+ };
47
+ }
48
+ async loadSession(cookieValue) {
49
+ const dot = cookieValue.indexOf('.');
50
+ if (dot === -1)
51
+ return null;
52
+ const sessionId = cookieValue.slice(0, dot);
53
+ const rawToken = cookieValue.slice(dot + 1);
54
+ if (!sessionId || !rawToken)
55
+ return null;
56
+ const expectedHash = sha256Hex(rawToken);
57
+ const cached = await this.redis.get(REDIS_KEY_PREFIX + sessionId);
58
+ if (cached) {
59
+ const parsed = JSON.parse(cached);
60
+ if (!timingSafeEqual(parsed.tokenHash, expectedHash))
61
+ return null;
62
+ if (new Date(parsed.expiresAt).getTime() <= Date.now())
63
+ return null;
64
+ return { session: deserialise(parsed.session), kind: deriveKind(parsed.session) };
65
+ }
66
+ const em = this.emFactory();
67
+ const session = await em.findOne(Session, { id: sessionId });
68
+ if (!session)
69
+ return null;
70
+ if (!timingSafeEqual(session.tokenHash, expectedHash))
71
+ return null;
72
+ if (session.expiresAt.getTime() <= Date.now())
73
+ return null;
74
+ await this.cache(session);
75
+ return { session, kind: deriveKind(session) };
76
+ }
77
+ async destroySession(sessionId) {
78
+ // command-coverage-ignore: session lifecycle — logout/session revoke; auth
79
+ // infrastructure, not an audited domain write.
80
+ const em = this.emFactory();
81
+ const session = await em.findOne(Session, { id: sessionId });
82
+ if (session) {
83
+ await em.removeAndFlush(session);
84
+ }
85
+ await this.redis.del(REDIS_KEY_PREFIX + sessionId);
86
+ }
87
+ /**
88
+ * Revoke every active session for a Customer — used when an account is
89
+ * blocked or soft-deleted so it loses access promptly (feature 040,
90
+ * FR-016/SC-002). Clears both the Postgres rows and their Redis caches.
91
+ */
92
+ async destroyAllForCustomer(customerAccountId) {
93
+ // command-coverage-ignore: session lifecycle — revokes all sessions for a
94
+ // customer; auth infrastructure, not an audited domain write.
95
+ const em = this.emFactory();
96
+ const sessions = await em.find(Session, { customerAccountId });
97
+ if (sessions.length === 0)
98
+ return;
99
+ for (const session of sessions) {
100
+ await this.redis.del(REDIS_KEY_PREFIX + session.id);
101
+ }
102
+ await em.removeAndFlush(sessions);
103
+ }
104
+ /**
105
+ * Revoke every session an admin user holds — both the sessions they signed
106
+ * in with and the impersonations they started, which carry the admin id in
107
+ * `impersonatorAdminUserId` and a customer id in `customerAccountId`. Used
108
+ * when a peer resets their password (issue #252): a session that survives
109
+ * the reset is a credential the reset did not withdraw, which is exactly
110
+ * what the compromised-account case needs withdrawn.
111
+ */
112
+ async destroyAllForAdmin(adminUserId) {
113
+ // command-coverage-ignore: session lifecycle — revokes all sessions for an
114
+ // admin; auth infrastructure. The domain-state write that triggers it is
115
+ // audited by its own caller (`admin_user.change_password`).
116
+ const em = this.emFactory();
117
+ const sessions = await em.find(Session, {
118
+ $or: [{ adminUserId }, { impersonatorAdminUserId: adminUserId }],
119
+ });
120
+ if (sessions.length === 0)
121
+ return;
122
+ for (const session of sessions) {
123
+ await this.redis.del(REDIS_KEY_PREFIX + session.id);
124
+ }
125
+ await em.removeAndFlush(sessions);
126
+ }
127
+ /**
128
+ * Feature 040 — stamp activity, throttled to at most once per minute per
129
+ * session (Redis marker) to avoid write amplification on every request.
130
+ */
131
+ async touchLastSeen(sessionId) {
132
+ // command-coverage-ignore: session lifecycle — refreshes last-seen presence;
133
+ // high-volume auth bookkeeping, not an audited domain write.
134
+ const marker = `seen:${sessionId}`;
135
+ const set = await this.redis.set(marker, '1', 'EX', 60, 'NX');
136
+ if (set === null)
137
+ return; // touched within the last minute
138
+ const em = this.emFactory();
139
+ await em.nativeUpdate(Session, { id: sessionId }, { lastSeenAt: new Date() });
140
+ }
141
+ /**
142
+ * Feature 040 — distinct customer ids whose session was active within the
143
+ * freshness window, for the admin "online customers" view.
144
+ */
145
+ async listRecentlyActiveCustomers(windowMinutes) {
146
+ const em = this.emFactory();
147
+ const cutoff = new Date(Date.now() - windowMinutes * 60 * 1000);
148
+ const rows = await em.find(Session, { customerAccountId: { $ne: null }, lastSeenAt: { $gte: cutoff } }, { fields: ['customerAccountId'] });
149
+ return [...new Set(rows.map((r) => r.customerAccountId).filter((x) => !!x))];
150
+ }
151
+ async cache(session) {
152
+ const ttl = Math.max(1, Math.floor((session.expiresAt.getTime() - Date.now()) / 1_000));
153
+ const payload = {
154
+ tokenHash: session.tokenHash,
155
+ expiresAt: session.expiresAt.toISOString(),
156
+ session: serialise(session),
157
+ };
158
+ await this.redis.set(REDIS_KEY_PREFIX + session.id, JSON.stringify(payload), 'EX', ttl);
159
+ }
160
+ }
161
+ function serialise(session) {
162
+ return {
163
+ id: session.id,
164
+ tokenHash: session.tokenHash,
165
+ customerAccountId: session.customerAccountId ?? null,
166
+ adminUserId: session.adminUserId ?? null,
167
+ impersonatorAdminUserId: session.impersonatorAdminUserId ?? null,
168
+ expiresAt: session.expiresAt.toISOString(),
169
+ };
170
+ }
171
+ function deserialise(s) {
172
+ const session = new Session();
173
+ session.id = s.id;
174
+ session.tokenHash = s.tokenHash;
175
+ session.customerAccountId = s.customerAccountId;
176
+ session.adminUserId = s.adminUserId;
177
+ session.impersonatorAdminUserId = s.impersonatorAdminUserId;
178
+ session.expiresAt = new Date(s.expiresAt);
179
+ return session;
180
+ }
181
+ function deriveKind(session) {
182
+ if (session.impersonatorAdminUserId)
183
+ return 'impersonation';
184
+ if (session.adminUserId)
185
+ return 'admin';
186
+ return 'customer';
187
+ }
188
+ function sha256Hex(input) {
189
+ return createHash('sha256').update(input, 'utf8').digest('hex');
190
+ }
191
+ function timingSafeEqual(a, b) {
192
+ if (a.length !== b.length)
193
+ return false;
194
+ let diff = 0;
195
+ for (let i = 0; i < a.length; i++) {
196
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
197
+ }
198
+ return diff === 0;
199
+ }
200
+ //# sourceMappingURL=session-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-service.js","sourceRoot":"","sources":["../../../src/backend/services/session-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Auth module — manifest backfill (Module Lifecycle, feature 018).
3
+ *
4
+ * Predates the lifecycle system; this manifest is the static record
5
+ * required so the module participates in the registry. No install /
6
+ * uninstall hook today — the module's schema is owned by earlier
7
+ * platform-wide migrations.
8
+ */
9
+ export declare const manifest: {
10
+ id: string;
11
+ name: string;
12
+ version: string;
13
+ dependencies: string[];
14
+ description?: string | undefined;
15
+ acknowledgedDependencies?: {
16
+ moduleId: string;
17
+ port: string;
18
+ reason: string;
19
+ }[] | undefined;
20
+ nonBindingDependencies?: {
21
+ moduleId: string;
22
+ name: string;
23
+ kind: "contributes-to" | "degrades-without" | "refuses-without";
24
+ reason: string;
25
+ whenAbsent?: string | undefined;
26
+ }[] | undefined;
27
+ activation?: {
28
+ settingCode: string;
29
+ default: boolean;
30
+ } | {
31
+ nonDeactivatable: true;
32
+ reason: string;
33
+ } | undefined;
34
+ settings?: {
35
+ moduleCode: string;
36
+ groups: {
37
+ code: string;
38
+ name: string;
39
+ salesChannelCodes?: string[] | undefined;
40
+ isSystemProtected?: boolean | undefined;
41
+ }[];
42
+ settings: {
43
+ code: string;
44
+ name: string;
45
+ valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
46
+ defaultValue: unknown;
47
+ description?: string | undefined;
48
+ groupCode?: string | undefined;
49
+ previousDefaultValues?: unknown[] | undefined;
50
+ salesChannelCodes?: string[] | undefined;
51
+ enumOptions?: string[] | undefined;
52
+ configurationType?: string | undefined;
53
+ hidden?: boolean | undefined;
54
+ }[];
55
+ } | undefined;
56
+ i18n?: {
57
+ bundlesDir: string;
58
+ } | undefined;
59
+ docs?: false | {
60
+ dir: string;
61
+ } | undefined;
62
+ demo?: false | {
63
+ summary: string;
64
+ seed: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoSeedResult>;
65
+ reset: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoResetResult>;
66
+ after?: readonly string[] | undefined;
67
+ package?: string | undefined;
68
+ } | undefined;
69
+ actions?: {
70
+ id: string;
71
+ labelKey: string;
72
+ icon: "Plus" | "Sparkles" | "Settings" | "Search" | "Boxes" | "Layers" | "Menu" | "PlusCircle" | "PlusSquare" | "FilePlus" | "FolderPlus" | "Upload" | "FileUp" | "CloudUpload" | "Download" | "FileDown" | "FileText" | "BookOpen" | "Rss" | "Package" | "Tag" | "ShoppingCart" | "Receipt" | "CreditCard" | "Users" | "UserPlus" | "Inbox" | "ListChecks" | "ClipboardList" | "Image" | "Video" | "LayoutDashboard" | "PanelLeft" | "KeyRound" | "ShieldCheck" | "Edit" | "Archive" | "Box" | "Truck" | "CircleDollarSign" | "Activity" | "LineChart" | "Smartphone" | "Webhook" | "Scale" | "PlugZap" | "PercentDiamond" | "Newspaper" | "Languages" | "Eraser" | "Warehouse" | "TrendingDown" | "Bell" | "PackageOpen" | "Building2" | "Store" | "ClipboardCheck";
73
+ targetRoute: string;
74
+ keywords: string[];
75
+ weight: number;
76
+ descriptionKey?: string | undefined;
77
+ requiredPermission?: string | undefined;
78
+ }[] | undefined;
79
+ permissions?: {
80
+ code: string;
81
+ label: string;
82
+ module?: string | undefined;
83
+ description?: string | undefined;
84
+ requires?: string[] | undefined;
85
+ }[] | undefined;
86
+ transactionalEmails?: {
87
+ code: string;
88
+ name: string;
89
+ variables: {
90
+ key: string;
91
+ label: string;
92
+ sampleValue?: string | undefined;
93
+ description?: string | undefined;
94
+ }[];
95
+ description?: string | undefined;
96
+ group?: string | undefined;
97
+ }[] | undefined;
98
+ capabilities?: string[] | undefined;
99
+ exclusiveCapabilities?: {
100
+ key: string;
101
+ errorCode: string;
102
+ }[] | undefined;
103
+ errorCodes?: {
104
+ code: string;
105
+ tokens?: string[] | undefined;
106
+ }[] | undefined;
107
+ blocks?: {
108
+ name: string;
109
+ labelKey: string;
110
+ category: string;
111
+ contexts: ("invoice" | "email" | "cms" | "newsletter")[];
112
+ fields: Record<string, {
113
+ type: "number" | "object" | "array" | "text" | "textarea" | "select" | "radio" | "external" | "uuid" | "richtext";
114
+ label?: string | undefined;
115
+ required?: boolean | undefined;
116
+ options?: {
117
+ label: string;
118
+ value: string | number;
119
+ }[] | undefined;
120
+ refKind?: string | undefined;
121
+ }>;
122
+ descriptionKey?: string | undefined;
123
+ defaultProps?: Record<string, unknown> | undefined;
124
+ responsiveFields?: string[] | undefined;
125
+ previewIcon?: string | undefined;
126
+ weight?: number | undefined;
127
+ }[] | undefined;
128
+ blockCategories?: {
129
+ key: string;
130
+ titleKey: string;
131
+ contexts: ("invoice" | "email" | "cms" | "newsletter")[];
132
+ weight?: number | undefined;
133
+ visible?: boolean | undefined;
134
+ }[] | undefined;
135
+ env?: {
136
+ name: string;
137
+ describes: {
138
+ en: string;
139
+ pl: string;
140
+ };
141
+ requirement: {
142
+ kind: "required";
143
+ } | {
144
+ kind: "requiredWhen";
145
+ input: string;
146
+ equals: string;
147
+ } | {
148
+ kind: "optional";
149
+ without: {
150
+ en: string;
151
+ pl: string;
152
+ };
153
+ };
154
+ secret: boolean;
155
+ generable: boolean;
156
+ owner: {
157
+ kind: "platform";
158
+ } | {
159
+ kind: "application";
160
+ application: "admin" | "backend" | "storefront";
161
+ } | {
162
+ kind: "module";
163
+ moduleId: string;
164
+ };
165
+ consumers: ("admin" | "backend" | "storefront")[];
166
+ addressOf: "admin" | "backend" | "storefront" | null;
167
+ }[] | undefined;
168
+ };
169
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgEnB,CAAC"}
@@ -0,0 +1,71 @@
1
+ import { defineModuleManifest } from '@endora-commerce/contracts';
2
+ /**
3
+ * Auth module — manifest backfill (Module Lifecycle, feature 018).
4
+ *
5
+ * Predates the lifecycle system; this manifest is the static record
6
+ * required so the module participates in the registry. No install /
7
+ * uninstall hook today — the module's schema is owned by earlier
8
+ * platform-wide migrations.
9
+ */
10
+ export const manifest = defineModuleManifest({
11
+ id: 'auth',
12
+ docs: { dir: 'docs' },
13
+ name: 'Auth',
14
+ description: 'Authentication primitives — session cookies, bearer-token resolution, and request actor binding.',
15
+ version: '1.0.0',
16
+ // Feature 072, D-32 — `auth` owns the one `requireAdmin` implementation, and
17
+ // that guard checks a permission through `admin_roles`' PermissionService.
18
+ // The edge existed in the code long before it existed in the manifest; it is
19
+ // one of the 261 imported-but-undeclared dependencies F3 will finish fixing.
20
+ dependencies: ['admin_roles'],
21
+ /**
22
+ * The customer-organization lookup the request actor is bound with (issue
23
+ * #90). `customer_accounts` owns it and declares `auth`, so declaring it back
24
+ * closes a cycle — the mutual shape `acknowledgedDependencies` exists for.
25
+ * With `customer_accounts` absent the resolution fails closed, so a signed-in
26
+ * customer the platform cannot place in an Organization is refused rather than
27
+ * bound to none — which is the cost of the edge, and the sort of thing a
28
+ * reason can state without copying a fact the build re-derives (D-100).
29
+ *
30
+ * The read stayed invisible until `check-port-dependencies` learned to follow
31
+ * a module-local cradle alias, which is how a real port edge in the request
32
+ * hook of the one module every request passes through went undeclared.
33
+ */
34
+ acknowledgedDependencies: [
35
+ {
36
+ moduleId: 'customer_accounts',
37
+ port: 'customerOrgResolver',
38
+ reason: 'Binding request.actor for a signed-in customer resolves their Organization through ' +
39
+ 'customer_accounts, which declares auth — declaring it back closes a cycle. With ' +
40
+ 'customer_accounts absent the gated port refuses, so actor binding fails closed rather ' +
41
+ 'than treating a signed-in customer as belonging to no Organization.',
42
+ },
43
+ ],
44
+ // D-44 — real to the container, binding on no operator.
45
+ nonBindingDependencies: [
46
+ {
47
+ moduleId: 'api_keys',
48
+ name: 'apiKeyResolver',
49
+ kind: 'degrades-without',
50
+ whenAbsent: 'requests presenting an API key are not authenticated',
51
+ reason: 'The request hook binds `request.actor` from a bearer API key when one is presented. ' +
52
+ '`api_keys` declares this module, so the ordinary declaration closes a cycle, and ' +
53
+ 'acknowledging it would put `auth` — present in every deployment — among the ' +
54
+ 'dependents that refuse the flip, making an integration surface permanently ' +
55
+ 'unswitchable. The hook has a defined behaviour instead: it probes presence, and an ' +
56
+ 'API key presented to a deployment with the module off authenticates nobody, exactly ' +
57
+ 'as a request carrying no key does. Every session-authenticated request is untouched.',
58
+ },
59
+ ],
60
+ // Feature 072/073 (Constitution XVII) — `auth` resolves `request.actor` for
61
+ // every request and owns the `requireAdmin` port that gates 205 call sites in
62
+ // 60 modules. A deployment with it switched off has no admin surface and no
63
+ // customer session, which is not a smaller platform but a broken one. The
64
+ // orchestrator refuses to disable it, with no `--force`.
65
+ activation: {
66
+ nonDeactivatable: true,
67
+ reason: 'Resolves the actor for every request and owns the requireAdmin guard; switched off, the ' +
68
+ 'platform has no admin surface and no customer sessions.',
69
+ },
70
+ });
71
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAElE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,oBAAoB,CAAC;IAC3C,EAAE,EAAE,MAAM;IACV,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE;IACrB,IAAI,EAAE,MAAM;IACZ,WAAW,EACT,kGAAkG;IACpG,OAAO,EAAE,OAAO;IAChB,6EAA6E;IAC7E,2EAA2E;IAC3E,6EAA6E;IAC7E,6EAA6E;IAC7E,YAAY,EAAE,CAAC,aAAa,CAAC;IAC7B;;;;;;;;;;;;OAYG;IACH,wBAAwB,EAAE;QACxB;YACE,QAAQ,EAAE,mBAAmB;YAC7B,IAAI,EAAE,qBAAqB;YAC3B,MAAM,EACJ,qFAAqF;gBACrF,kFAAkF;gBAClF,wFAAwF;gBACxF,qEAAqE;SACxE;KACF;IACD,wDAAwD;IACxD,sBAAsB,EAAE;QACtB;YACE,QAAQ,EAAE,UAAU;YACpB,IAAI,EAAE,gBAAgB;YACtB,IAAI,EAAE,kBAAkB;YACxB,UAAU,EAAE,sDAAsD;YAClE,MAAM,EACJ,sFAAsF;gBACtF,mFAAmF;gBACnF,8EAA8E;gBAC9E,6EAA6E;gBAC7E,qFAAqF;gBACrF,sFAAsF;gBACtF,sFAAsF;SACzF;KACF;IACD,4EAA4E;IAC5E,8EAA8E;IAC9E,4EAA4E;IAC5E,0EAA0E;IAC1E,yDAAyD;IACzD,UAAU,EAAE;QACV,gBAAgB,EAAE,IAAI;QACtB,MAAM,EACJ,0FAA0F;YAC1F,yDAAyD;KAC5D;CACF,CAAC,CAAC"}
package/docs/auth.md ADDED
@@ -0,0 +1,42 @@
1
+ ---
2
+ title: auth
3
+ description: Customer + admin sessions, password hashing, TOTP
4
+ ---
5
+
6
+ # `auth`
7
+
8
+ Shared session, password-hashing, and TOTP primitives consumed by both
9
+ `customer_accounts` and `admin_users`. One of the two singular module
10
+ folders permitted by the naming conventions (alongside `example`).
11
+
12
+ ## What it owns
13
+
14
+ - **Sessions** — `session-service.ts` persists session rows in Postgres
15
+ with a Redis cache layer for hot reads; cookies signed via
16
+ `@fastify/cookie`.
17
+ - **Password hashing** — `password-hasher.ts` wraps argon2id; defaults are
18
+ tuned for the target hardware (see the **Hardware & system requirements**
19
+ section of `README.md` at the repository root).
20
+ - **TOTP** — the platform's `kernel/crypto/totp` wraps `otpauth` + a backup-code pool.
21
+ - **Fastify plugin** — `plugin.ts` parses the session cookie and attaches
22
+ `request.actor = { kind, id, ... }` plus `request.adminActor`.
23
+ - **Route guards** — `requireAdmin(permission?)`, `requireAdminAny(codes)` and
24
+ `requireCustomer`, provided as ports from `backend.ts` and resolved by every
25
+ module that gates a route. They were Fastify decorators on the plugin once;
26
+ they were turned into ports so production and the test harness run the same
27
+ implementation instead of one each.
28
+
29
+ ## No HTTP routes of its own
30
+
31
+ `auth` is a primitive module — login/logout/2FA endpoints belong to the
32
+ customer-facing `customer_accounts` and admin-facing `admin_users`
33
+ modules.
34
+
35
+ ## Extension points
36
+
37
+ - **Session storage** — `session-service` is constructed with `(em, redis)`;
38
+ alternative caches plug in here.
39
+ - **Custom actor kinds** — extend the `request.actor` discriminated union
40
+ and update the `requireX` pre-handlers; existing callers keep working
41
+ because the routes only use the `requireX` factory they already
42
+ consume.
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "@endora-commerce/mod-auth",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.100.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "Authentication primitives — session cookies, bearer-token resolution, and request actor binding.",
7
+ "license": "MIT",
8
+ "endora": {
9
+ "type": "module",
10
+ "id": "auth"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/endora-commerce/endora-commerce.git",
15
+ "directory": "packages/modules/auth"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/manifest.d.ts",
23
+ "default": "./dist/manifest.js"
24
+ },
25
+ "./backend": {
26
+ "types": "./dist/backend/index.d.ts",
27
+ "default": "./dist/backend/index.js"
28
+ },
29
+ "./package.json": "./package.json"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "docs"
34
+ ],
35
+ "engines": {
36
+ "node": ">=22.18.0"
37
+ },
38
+ "peerDependencies": {
39
+ "@mikro-orm/core": "^6",
40
+ "@mikro-orm/postgresql": "^6",
41
+ "fastify": "^5",
42
+ "fastify-plugin": "^5",
43
+ "ioredis": "^5",
44
+ "@endora-commerce/contracts": "0.100.0",
45
+ "@endora-commerce/platform": "0.100.0"
46
+ },
47
+ "devDependencies": {
48
+ "@mikro-orm/core": "^6.6.13",
49
+ "@mikro-orm/postgresql": "^6.6.13",
50
+ "@types/node": "^22.9.0",
51
+ "fastify": "^5.12.5",
52
+ "fastify-plugin": "^5.1.0",
53
+ "ioredis": "^5.10.1",
54
+ "typescript": "^5.9.3",
55
+ "@endora-commerce/contracts": "0.100.0",
56
+ "@endora-commerce/platform": "0.100.0"
57
+ },
58
+ "scripts": {
59
+ "build": "tsc -p tsconfig.build.json",
60
+ "typecheck": "tsc -p tsconfig.json",
61
+ "lint": "eslint src"
62
+ }
6
63
  }