@open-mercato/shared 0.4.2-canary-e6bf6a353e → 0.4.2-canary-49d47ff90e

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.
@@ -0,0 +1,18 @@
1
+ import { createRequestContainer } from "../di/container.js";
2
+ import { getAuthFromRequest } from "../auth/server.js";
3
+ import { resolveTranslations } from "../i18n/server.js";
4
+ async function resolveRequestContext(req) {
5
+ const container = await createRequestContainer();
6
+ const auth = await getAuthFromRequest(req);
7
+ const { translate } = await resolveTranslations();
8
+ const ctx = {
9
+ container,
10
+ auth,
11
+ translate
12
+ };
13
+ return { ctx };
14
+ }
15
+ export {
16
+ resolveRequestContext
17
+ };
18
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/api/context.ts"],
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport { createRequestContainer } from '../di/container'\nimport { getAuthFromRequest, type AuthContext } from '../auth/server'\nimport { resolveTranslations } from '../i18n/server'\n\nexport type RequestContext = {\n container: AwilixContainer\n auth: AuthContext\n organizationScope?: unknown\n selectedOrganizationId?: string | null\n organizationIds?: string[] | null\n translate: (key: string, fallback?: string) => string\n}\n\nexport type ResolveRequestContextResult = {\n ctx: RequestContext\n}\n\n/**\n * Resolves the request context for API routes.\n * This includes container, auth, and translations.\n * For organization-scoped routes, use resolveOrganizationScopeForRequest separately.\n */\nexport async function resolveRequestContext(req: Request): Promise<ResolveRequestContextResult> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n const ctx: RequestContext = {\n container,\n auth,\n translate,\n }\n\n return { ctx }\n}\n"],
5
+ "mappings": "AACA,SAAS,8BAA8B;AACvC,SAAS,0BAA4C;AACrD,SAAS,2BAA2B;AAoBpC,eAAsB,sBAAsB,KAAoD;AAC9F,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAEhD,QAAM,MAAsB;AAAA,IAC1B;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO,EAAE,IAAI;AACf;",
6
+ "names": []
7
+ }
@@ -0,0 +1,111 @@
1
+ import { z } from "zod";
2
+ import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
3
+ const DEFAULT_POLICY = {
4
+ minLength: 6,
5
+ requireDigit: true,
6
+ requireUppercase: true,
7
+ requireSpecial: true
8
+ };
9
+ const ENV_KEYS = {
10
+ minLength: "OM_PASSWORD_MIN_LENGTH",
11
+ requireDigit: "OM_PASSWORD_REQUIRE_DIGIT",
12
+ requireUppercase: "OM_PASSWORD_REQUIRE_UPPERCASE",
13
+ requireSpecial: "OM_PASSWORD_REQUIRE_SPECIAL"
14
+ };
15
+ const PUBLIC_PREFIX = "NEXT_PUBLIC_";
16
+ function readEnvValue(env, key) {
17
+ const rawKey = ENV_KEYS[key];
18
+ const publicKey = `${PUBLIC_PREFIX}${rawKey}`;
19
+ const rawValue = env[rawKey];
20
+ if (typeof rawValue === "string" && rawValue.trim().length > 0) return rawValue;
21
+ const publicValue = env[publicKey];
22
+ if (typeof publicValue === "string" && publicValue.trim().length > 0) return publicValue;
23
+ return void 0;
24
+ }
25
+ function parsePositiveInt(raw, fallback, min = 1) {
26
+ if (!raw) return fallback;
27
+ const parsed = Number.parseInt(raw, 10);
28
+ if (Number.isNaN(parsed)) return fallback;
29
+ return Math.max(min, parsed);
30
+ }
31
+ function getPasswordPolicy(env = process.env) {
32
+ const minLength = parsePositiveInt(
33
+ readEnvValue(env, "minLength"),
34
+ DEFAULT_POLICY.minLength,
35
+ 1
36
+ );
37
+ return {
38
+ minLength,
39
+ requireDigit: parseBooleanWithDefault(
40
+ readEnvValue(env, "requireDigit"),
41
+ DEFAULT_POLICY.requireDigit
42
+ ),
43
+ requireUppercase: parseBooleanWithDefault(
44
+ readEnvValue(env, "requireUppercase"),
45
+ DEFAULT_POLICY.requireUppercase
46
+ ),
47
+ requireSpecial: parseBooleanWithDefault(
48
+ readEnvValue(env, "requireSpecial"),
49
+ DEFAULT_POLICY.requireSpecial
50
+ )
51
+ };
52
+ }
53
+ function getPasswordRequirements(policy = getPasswordPolicy()) {
54
+ const requirements = [{ id: "minLength", value: policy.minLength }];
55
+ if (policy.requireDigit) requirements.push({ id: "digit" });
56
+ if (policy.requireUppercase) requirements.push({ id: "uppercase" });
57
+ if (policy.requireSpecial) requirements.push({ id: "special" });
58
+ return requirements;
59
+ }
60
+ function formatPasswordRequirements(policy, translate, keyPrefix = "auth.password.requirements") {
61
+ const items = getPasswordRequirements(policy).map((requirement) => {
62
+ switch (requirement.id) {
63
+ case "minLength":
64
+ return translate(
65
+ `${keyPrefix}.minLength`,
66
+ "At least {min} characters",
67
+ { min: requirement.value ?? policy.minLength }
68
+ );
69
+ case "digit":
70
+ return translate(`${keyPrefix}.digit`, "One number");
71
+ case "uppercase":
72
+ return translate(`${keyPrefix}.uppercase`, "One uppercase letter");
73
+ case "special":
74
+ return translate(`${keyPrefix}.special`, "One special character");
75
+ default:
76
+ return "";
77
+ }
78
+ }).filter((value) => value && value.trim().length > 0);
79
+ if (!items.length) return "";
80
+ const separator = translate(`${keyPrefix}.separator`, ", ");
81
+ return items.join(separator);
82
+ }
83
+ function validatePassword(password, policy = getPasswordPolicy()) {
84
+ const violations = [];
85
+ if (password.length < policy.minLength) violations.push("minLength");
86
+ if (policy.requireDigit && !/[0-9]/.test(password)) violations.push("digit");
87
+ if (policy.requireUppercase && !/[A-Z]/.test(password)) violations.push("uppercase");
88
+ if (policy.requireSpecial && !/[^A-Za-z0-9]/.test(password)) violations.push("special");
89
+ return { ok: violations.length === 0, violations };
90
+ }
91
+ function buildPasswordSchema(options) {
92
+ const policy = options?.policy ?? getPasswordPolicy();
93
+ const maxLength = options?.maxLength;
94
+ const message = options?.message ?? "Password does not meet the requirements.";
95
+ let schema = z.string().min(policy.minLength, message);
96
+ if (typeof maxLength === "number") schema = schema.max(maxLength, message);
97
+ return schema.superRefine((value, ctx) => {
98
+ const result = validatePassword(value, policy);
99
+ if (!result.ok) {
100
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message });
101
+ }
102
+ });
103
+ }
104
+ export {
105
+ buildPasswordSchema,
106
+ formatPasswordRequirements,
107
+ getPasswordPolicy,
108
+ getPasswordRequirements,
109
+ validatePassword
110
+ };
111
+ //# sourceMappingURL=passwordPolicy.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/auth/passwordPolicy.ts"],
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\n\nexport type PasswordPolicy = {\n minLength: number\n requireDigit: boolean\n requireUppercase: boolean\n requireSpecial: boolean\n}\n\nexport type PasswordRequirementId = 'minLength' | 'digit' | 'uppercase' | 'special'\n\nexport type PasswordRequirement = {\n id: PasswordRequirementId\n value?: number\n}\n\nexport type PasswordValidationResult = {\n ok: boolean\n violations: PasswordRequirementId[]\n}\n\nexport type PasswordRequirementFormatter = (\n key: string,\n fallback: string,\n params?: Record<string, string | number>,\n) => string\n\nconst DEFAULT_POLICY: PasswordPolicy = {\n minLength: 6,\n requireDigit: true,\n requireUppercase: true,\n requireSpecial: true,\n}\n\nconst ENV_KEYS = {\n minLength: 'OM_PASSWORD_MIN_LENGTH',\n requireDigit: 'OM_PASSWORD_REQUIRE_DIGIT',\n requireUppercase: 'OM_PASSWORD_REQUIRE_UPPERCASE',\n requireSpecial: 'OM_PASSWORD_REQUIRE_SPECIAL',\n} as const\n\nconst PUBLIC_PREFIX = 'NEXT_PUBLIC_'\n\nfunction readEnvValue(env: NodeJS.ProcessEnv, key: keyof typeof ENV_KEYS): string | undefined {\n const rawKey = ENV_KEYS[key]\n const publicKey = `${PUBLIC_PREFIX}${rawKey}`\n const rawValue = env[rawKey]\n if (typeof rawValue === 'string' && rawValue.trim().length > 0) return rawValue\n const publicValue = env[publicKey]\n if (typeof publicValue === 'string' && publicValue.trim().length > 0) return publicValue\n return undefined\n}\n\nfunction parsePositiveInt(raw: string | undefined, fallback: number, min = 1): number {\n if (!raw) return fallback\n const parsed = Number.parseInt(raw, 10)\n if (Number.isNaN(parsed)) return fallback\n return Math.max(min, parsed)\n}\n\nexport function getPasswordPolicy(env: NodeJS.ProcessEnv = process.env): PasswordPolicy {\n const minLength = parsePositiveInt(\n readEnvValue(env, 'minLength'),\n DEFAULT_POLICY.minLength,\n 1,\n )\n return {\n minLength,\n requireDigit: parseBooleanWithDefault(\n readEnvValue(env, 'requireDigit'),\n DEFAULT_POLICY.requireDigit,\n ),\n requireUppercase: parseBooleanWithDefault(\n readEnvValue(env, 'requireUppercase'),\n DEFAULT_POLICY.requireUppercase,\n ),\n requireSpecial: parseBooleanWithDefault(\n readEnvValue(env, 'requireSpecial'),\n DEFAULT_POLICY.requireSpecial,\n ),\n }\n}\n\nexport function getPasswordRequirements(policy: PasswordPolicy = getPasswordPolicy()): PasswordRequirement[] {\n const requirements: PasswordRequirement[] = [{ id: 'minLength', value: policy.minLength }]\n if (policy.requireDigit) requirements.push({ id: 'digit' })\n if (policy.requireUppercase) requirements.push({ id: 'uppercase' })\n if (policy.requireSpecial) requirements.push({ id: 'special' })\n return requirements\n}\n\nexport function formatPasswordRequirements(\n policy: PasswordPolicy,\n translate: PasswordRequirementFormatter,\n keyPrefix = 'auth.password.requirements',\n): string {\n const items = getPasswordRequirements(policy).map((requirement) => {\n switch (requirement.id) {\n case 'minLength':\n return translate(\n `${keyPrefix}.minLength`,\n 'At least {min} characters',\n { min: requirement.value ?? policy.minLength },\n )\n case 'digit':\n return translate(`${keyPrefix}.digit`, 'One number')\n case 'uppercase':\n return translate(`${keyPrefix}.uppercase`, 'One uppercase letter')\n case 'special':\n return translate(`${keyPrefix}.special`, 'One special character')\n default:\n return ''\n }\n }).filter((value) => value && value.trim().length > 0)\n\n if (!items.length) return ''\n const separator = translate(`${keyPrefix}.separator`, ', ')\n return items.join(separator)\n}\n\nexport function validatePassword(\n password: string,\n policy: PasswordPolicy = getPasswordPolicy(),\n): PasswordValidationResult {\n const violations: PasswordRequirementId[] = []\n if (password.length < policy.minLength) violations.push('minLength')\n if (policy.requireDigit && !/[0-9]/.test(password)) violations.push('digit')\n if (policy.requireUppercase && !/[A-Z]/.test(password)) violations.push('uppercase')\n if (policy.requireSpecial && !/[^A-Za-z0-9]/.test(password)) violations.push('special')\n return { ok: violations.length === 0, violations }\n}\n\nexport function buildPasswordSchema(options?: {\n policy?: PasswordPolicy\n maxLength?: number\n message?: string\n}): z.ZodType<string> {\n const policy = options?.policy ?? getPasswordPolicy()\n const maxLength = options?.maxLength\n const message = options?.message ?? 'Password does not meet the requirements.'\n let schema = z.string().min(policy.minLength, message)\n if (typeof maxLength === 'number') schema = schema.max(maxLength, message)\n return schema.superRefine((value, ctx) => {\n const result = validatePassword(value, policy)\n if (!result.ok) {\n ctx.addIssue({ code: z.ZodIssueCode.custom, message })\n }\n })\n}\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,+BAA+B;AA2BxC,MAAM,iBAAiC;AAAA,EACrC,WAAW;AAAA,EACX,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,gBAAgB;AAClB;AAEA,MAAM,WAAW;AAAA,EACf,WAAW;AAAA,EACX,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,gBAAgB;AAClB;AAEA,MAAM,gBAAgB;AAEtB,SAAS,aAAa,KAAwB,KAAgD;AAC5F,QAAM,SAAS,SAAS,GAAG;AAC3B,QAAM,YAAY,GAAG,aAAa,GAAG,MAAM;AAC3C,QAAM,WAAW,IAAI,MAAM;AAC3B,MAAI,OAAO,aAAa,YAAY,SAAS,KAAK,EAAE,SAAS,EAAG,QAAO;AACvE,QAAM,cAAc,IAAI,SAAS;AACjC,MAAI,OAAO,gBAAgB,YAAY,YAAY,KAAK,EAAE,SAAS,EAAG,QAAO;AAC7E,SAAO;AACT;AAEA,SAAS,iBAAiB,KAAyB,UAAkB,MAAM,GAAW;AACpF,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,SAAS,OAAO,SAAS,KAAK,EAAE;AACtC,MAAI,OAAO,MAAM,MAAM,EAAG,QAAO;AACjC,SAAO,KAAK,IAAI,KAAK,MAAM;AAC7B;AAEO,SAAS,kBAAkB,MAAyB,QAAQ,KAAqB;AACtF,QAAM,YAAY;AAAA,IAChB,aAAa,KAAK,WAAW;AAAA,IAC7B,eAAe;AAAA,IACf;AAAA,EACF;AACA,SAAO;AAAA,IACL;AAAA,IACA,cAAc;AAAA,MACZ,aAAa,KAAK,cAAc;AAAA,MAChC,eAAe;AAAA,IACjB;AAAA,IACA,kBAAkB;AAAA,MAChB,aAAa,KAAK,kBAAkB;AAAA,MACpC,eAAe;AAAA,IACjB;AAAA,IACA,gBAAgB;AAAA,MACd,aAAa,KAAK,gBAAgB;AAAA,MAClC,eAAe;AAAA,IACjB;AAAA,EACF;AACF;AAEO,SAAS,wBAAwB,SAAyB,kBAAkB,GAA0B;AAC3G,QAAM,eAAsC,CAAC,EAAE,IAAI,aAAa,OAAO,OAAO,UAAU,CAAC;AACzF,MAAI,OAAO,aAAc,cAAa,KAAK,EAAE,IAAI,QAAQ,CAAC;AAC1D,MAAI,OAAO,iBAAkB,cAAa,KAAK,EAAE,IAAI,YAAY,CAAC;AAClE,MAAI,OAAO,eAAgB,cAAa,KAAK,EAAE,IAAI,UAAU,CAAC;AAC9D,SAAO;AACT;AAEO,SAAS,2BACd,QACA,WACA,YAAY,8BACJ;AACR,QAAM,QAAQ,wBAAwB,MAAM,EAAE,IAAI,CAAC,gBAAgB;AACjE,YAAQ,YAAY,IAAI;AAAA,MACtB,KAAK;AACH,eAAO;AAAA,UACL,GAAG,SAAS;AAAA,UACZ;AAAA,UACA,EAAE,KAAK,YAAY,SAAS,OAAO,UAAU;AAAA,QAC/C;AAAA,MACF,KAAK;AACH,eAAO,UAAU,GAAG,SAAS,UAAU,YAAY;AAAA,MACrD,KAAK;AACH,eAAO,UAAU,GAAG,SAAS,cAAc,sBAAsB;AAAA,MACnE,KAAK;AACH,eAAO,UAAU,GAAG,SAAS,YAAY,uBAAuB;AAAA,MAClE;AACE,eAAO;AAAA,IACX;AAAA,EACF,CAAC,EAAE,OAAO,CAAC,UAAU,SAAS,MAAM,KAAK,EAAE,SAAS,CAAC;AAErD,MAAI,CAAC,MAAM,OAAQ,QAAO;AAC1B,QAAM,YAAY,UAAU,GAAG,SAAS,cAAc,IAAI;AAC1D,SAAO,MAAM,KAAK,SAAS;AAC7B;AAEO,SAAS,iBACd,UACA,SAAyB,kBAAkB,GACjB;AAC1B,QAAM,aAAsC,CAAC;AAC7C,MAAI,SAAS,SAAS,OAAO,UAAW,YAAW,KAAK,WAAW;AACnE,MAAI,OAAO,gBAAgB,CAAC,QAAQ,KAAK,QAAQ,EAAG,YAAW,KAAK,OAAO;AAC3E,MAAI,OAAO,oBAAoB,CAAC,QAAQ,KAAK,QAAQ,EAAG,YAAW,KAAK,WAAW;AACnF,MAAI,OAAO,kBAAkB,CAAC,eAAe,KAAK,QAAQ,EAAG,YAAW,KAAK,SAAS;AACtF,SAAO,EAAE,IAAI,WAAW,WAAW,GAAG,WAAW;AACnD;AAEO,SAAS,oBAAoB,SAId;AACpB,QAAM,SAAS,SAAS,UAAU,kBAAkB;AACpD,QAAM,YAAY,SAAS;AAC3B,QAAM,UAAU,SAAS,WAAW;AACpC,MAAI,SAAS,EAAE,OAAO,EAAE,IAAI,OAAO,WAAW,OAAO;AACrD,MAAI,OAAO,cAAc,SAAU,UAAS,OAAO,IAAI,WAAW,OAAO;AACzE,SAAO,OAAO,YAAY,CAAC,OAAO,QAAQ;AACxC,UAAM,SAAS,iBAAiB,OAAO,MAAM;AAC7C,QAAI,CAAC,OAAO,IAAI;AACd,UAAI,SAAS,EAAE,MAAM,EAAE,aAAa,QAAQ,QAAQ,CAAC;AAAA,IACvD;AAAA,EACF,CAAC;AACH;",
6
+ "names": []
7
+ }
@@ -90,6 +90,7 @@ async function resolveApiKeyAuth(secret) {
90
90
  await em.persistAndFlush(record);
91
91
  } catch {
92
92
  }
93
+ const actualUserId = record.sessionUserId ?? record.createdBy ?? null;
93
94
  return {
94
95
  sub: `api_key:${record.id}`,
95
96
  tenantId: record.tenantId ?? null,
@@ -97,7 +98,8 @@ async function resolveApiKeyAuth(secret) {
97
98
  roles: roleNames,
98
99
  isApiKey: true,
99
100
  keyId: record.id,
100
- keyName: record.name
101
+ keyName: record.name,
102
+ ...actualUserId ? { userId: actualUserId } : {}
101
103
  };
102
104
  } catch {
103
105
  return null;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/auth/server.ts"],
4
- "sourcesContent": ["import { cookies } from 'next/headers'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { verifyJwt } from './jwt'\n\nconst TENANT_COOKIE_NAME = 'om_selected_tenant'\nconst ORGANIZATION_COOKIE_NAME = 'om_selected_org'\nconst ALL_ORGANIZATIONS_COOKIE_VALUE = '__all__'\nconst SUPERADMIN_ROLE = 'superadmin'\n\nexport type AuthContext = {\n sub: string\n tenantId: string | null\n orgId: string | null\n email?: string\n roles?: string[]\n isApiKey?: boolean\n keyId?: string\n keyName?: string\n [k: string]: unknown\n} | null\n\ntype CookieOverride = { applied: boolean; value: string | null }\n\nfunction decodeCookieValue(raw: string | undefined): string | null {\n if (raw === undefined) return null\n try {\n const decoded = decodeURIComponent(raw)\n return decoded ?? null\n } catch {\n return raw ?? null\n }\n}\n\nfunction readCookieFromHeader(header: string | null | undefined, name: string): string | undefined {\n if (!header) return undefined\n const parts = header.split(';')\n for (const part of parts) {\n const trimmed = part.trim()\n if (trimmed.startsWith(`${name}=`)) {\n return trimmed.slice(name.length + 1)\n }\n }\n return undefined\n}\n\nfunction resolveTenantOverride(raw: string | undefined): CookieOverride {\n if (raw === undefined) return { applied: false, value: null }\n const decoded = decodeCookieValue(raw)\n if (!decoded) return { applied: true, value: null }\n const trimmed = decoded.trim()\n if (!trimmed) return { applied: true, value: null }\n return { applied: true, value: trimmed }\n}\n\nfunction resolveOrganizationOverride(raw: string | undefined): CookieOverride {\n if (raw === undefined) return { applied: false, value: null }\n const decoded = decodeCookieValue(raw)\n if (!decoded || decoded === ALL_ORGANIZATIONS_COOKIE_VALUE) {\n return { applied: true, value: null }\n }\n const trimmed = decoded.trim()\n if (!trimmed || trimmed === ALL_ORGANIZATIONS_COOKIE_VALUE) {\n return { applied: true, value: null }\n }\n return { applied: true, value: trimmed }\n}\n\nfunction isSuperAdminAuth(auth: AuthContext | null | undefined): boolean {\n if (!auth) return false\n if ((auth as Record<string, unknown>).isSuperAdmin === true) return true\n const roles = Array.isArray(auth?.roles) ? auth.roles : []\n return roles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)\n}\n\nfunction applySuperAdminScope(\n auth: AuthContext,\n tenantCookie: string | undefined,\n orgCookie: string | undefined\n): AuthContext {\n if (!auth || !isSuperAdminAuth(auth)) return auth\n\n const tenantOverride = resolveTenantOverride(tenantCookie)\n const orgOverride = resolveOrganizationOverride(orgCookie)\n if (!tenantOverride.applied && !orgOverride.applied) return auth\n\n type MutableAuthContext = Exclude<AuthContext, null> & {\n actorTenantId?: string | null\n actorOrgId?: string | null\n }\n const baseAuth = auth as Exclude<AuthContext, null>\n const next: MutableAuthContext = { ...baseAuth }\n if (tenantOverride.applied) {\n if (!('actorTenantId' in next)) next.actorTenantId = auth?.tenantId ?? null\n next.tenantId = tenantOverride.value\n }\n if (orgOverride.applied) {\n if (!('actorOrgId' in next)) next.actorOrgId = auth?.orgId ?? null\n next.orgId = orgOverride.value\n }\n next.isSuperAdmin = true\n const existingRoles = Array.isArray(next.roles) ? next.roles : []\n if (!existingRoles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)) {\n next.roles = [...existingRoles, 'superadmin']\n }\n return next\n}\n\nasync function resolveApiKeyAuth(secret: string): Promise<AuthContext> {\n if (!secret) return null\n try {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const { findApiKeyBySecret } = await import('@open-mercato/core/modules/api_keys/services/apiKeyService')\n const { Role } = await import('@open-mercato/core/modules/auth/data/entities')\n\n const record = await findApiKeyBySecret(em, secret)\n if (!record) return null\n\n const roleIds = Array.isArray(record.rolesJson)\n ? record.rolesJson.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : []\n const roles = roleIds.length\n ? await em.find(Role, { id: { $in: roleIds } })\n : []\n const roleNames = roles.map((role) => role.name).filter((name): name is string => typeof name === 'string' && name.length > 0)\n\n try {\n record.lastUsedAt = new Date()\n await em.persistAndFlush(record)\n } catch {\n // best-effort update; ignore write failures\n }\n\n return {\n sub: `api_key:${record.id}`,\n tenantId: record.tenantId ?? null,\n orgId: record.organizationId ?? null,\n roles: roleNames,\n isApiKey: true,\n keyId: record.id,\n keyName: record.name,\n }\n } catch {\n return null\n }\n}\n\nfunction extractApiKey(req: Request): string | null {\n const header = (req.headers.get('x-api-key') || '').trim()\n if (header) return header\n const authHeader = (req.headers.get('authorization') || '').trim()\n if (authHeader.toLowerCase().startsWith('apikey ')) {\n return authHeader.slice(7).trim()\n }\n return null\n}\n\nexport async function getAuthFromCookies(): Promise<AuthContext> {\n const cookieStore = await cookies()\n const token = cookieStore.get('auth_token')?.value\n if (!token) return null\n try {\n const payload = verifyJwt(token) as AuthContext\n if (!payload) return null\n const tenantCookie = cookieStore.get(TENANT_COOKIE_NAME)?.value\n const orgCookie = cookieStore.get(ORGANIZATION_COOKIE_NAME)?.value\n return applySuperAdminScope(payload, tenantCookie, orgCookie)\n } catch {\n return null\n }\n}\n\nexport async function getAuthFromRequest(req: Request): Promise<AuthContext> {\n const cookieHeader = req.headers.get('cookie') || ''\n const tenantCookie = readCookieFromHeader(cookieHeader, TENANT_COOKIE_NAME)\n const orgCookie = readCookieFromHeader(cookieHeader, ORGANIZATION_COOKIE_NAME)\n const authHeader = (req.headers.get('authorization') || '').trim()\n let token: string | undefined\n if (authHeader.toLowerCase().startsWith('bearer ')) token = authHeader.slice(7).trim()\n if (!token) {\n const match = cookieHeader.match(/(?:^|;\\s*)auth_token=([^;]+)/)\n if (match) token = decodeURIComponent(match[1])\n }\n if (token) {\n try {\n const payload = verifyJwt(token) as AuthContext\n if (payload) return applySuperAdminScope(payload, tenantCookie, orgCookie)\n } catch {\n // fall back to API key detection\n }\n }\n\n const apiKey = extractApiKey(req)\n if (!apiKey) return null\n const apiAuth = await resolveApiKeyAuth(apiKey)\n if (!apiAuth) return null\n return applySuperAdminScope(apiAuth, tenantCookie, orgCookie)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { cookies } from 'next/headers'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { verifyJwt } from './jwt'\n\nconst TENANT_COOKIE_NAME = 'om_selected_tenant'\nconst ORGANIZATION_COOKIE_NAME = 'om_selected_org'\nconst ALL_ORGANIZATIONS_COOKIE_VALUE = '__all__'\nconst SUPERADMIN_ROLE = 'superadmin'\n\nexport type AuthContext = {\n sub: string\n tenantId: string | null\n orgId: string | null\n email?: string\n roles?: string[]\n isApiKey?: boolean\n userId?: string\n keyId?: string\n keyName?: string\n [k: string]: unknown\n} | null\n\ntype CookieOverride = { applied: boolean; value: string | null }\n\nfunction decodeCookieValue(raw: string | undefined): string | null {\n if (raw === undefined) return null\n try {\n const decoded = decodeURIComponent(raw)\n return decoded ?? null\n } catch {\n return raw ?? null\n }\n}\n\nfunction readCookieFromHeader(header: string | null | undefined, name: string): string | undefined {\n if (!header) return undefined\n const parts = header.split(';')\n for (const part of parts) {\n const trimmed = part.trim()\n if (trimmed.startsWith(`${name}=`)) {\n return trimmed.slice(name.length + 1)\n }\n }\n return undefined\n}\n\nfunction resolveTenantOverride(raw: string | undefined): CookieOverride {\n if (raw === undefined) return { applied: false, value: null }\n const decoded = decodeCookieValue(raw)\n if (!decoded) return { applied: true, value: null }\n const trimmed = decoded.trim()\n if (!trimmed) return { applied: true, value: null }\n return { applied: true, value: trimmed }\n}\n\nfunction resolveOrganizationOverride(raw: string | undefined): CookieOverride {\n if (raw === undefined) return { applied: false, value: null }\n const decoded = decodeCookieValue(raw)\n if (!decoded || decoded === ALL_ORGANIZATIONS_COOKIE_VALUE) {\n return { applied: true, value: null }\n }\n const trimmed = decoded.trim()\n if (!trimmed || trimmed === ALL_ORGANIZATIONS_COOKIE_VALUE) {\n return { applied: true, value: null }\n }\n return { applied: true, value: trimmed }\n}\n\nfunction isSuperAdminAuth(auth: AuthContext | null | undefined): boolean {\n if (!auth) return false\n if ((auth as Record<string, unknown>).isSuperAdmin === true) return true\n const roles = Array.isArray(auth?.roles) ? auth.roles : []\n return roles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)\n}\n\nfunction applySuperAdminScope(\n auth: AuthContext,\n tenantCookie: string | undefined,\n orgCookie: string | undefined\n): AuthContext {\n if (!auth || !isSuperAdminAuth(auth)) return auth\n\n const tenantOverride = resolveTenantOverride(tenantCookie)\n const orgOverride = resolveOrganizationOverride(orgCookie)\n if (!tenantOverride.applied && !orgOverride.applied) return auth\n\n type MutableAuthContext = Exclude<AuthContext, null> & {\n actorTenantId?: string | null\n actorOrgId?: string | null\n }\n const baseAuth = auth as Exclude<AuthContext, null>\n const next: MutableAuthContext = { ...baseAuth }\n if (tenantOverride.applied) {\n if (!('actorTenantId' in next)) next.actorTenantId = auth?.tenantId ?? null\n next.tenantId = tenantOverride.value\n }\n if (orgOverride.applied) {\n if (!('actorOrgId' in next)) next.actorOrgId = auth?.orgId ?? null\n next.orgId = orgOverride.value\n }\n next.isSuperAdmin = true\n const existingRoles = Array.isArray(next.roles) ? next.roles : []\n if (!existingRoles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)) {\n next.roles = [...existingRoles, 'superadmin']\n }\n return next\n}\n\nasync function resolveApiKeyAuth(secret: string): Promise<AuthContext> {\n if (!secret) return null\n try {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const { findApiKeyBySecret } = await import('@open-mercato/core/modules/api_keys/services/apiKeyService')\n const { Role } = await import('@open-mercato/core/modules/auth/data/entities')\n\n const record = await findApiKeyBySecret(em, secret)\n if (!record) return null\n\n const roleIds = Array.isArray(record.rolesJson)\n ? record.rolesJson.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : []\n const roles = roleIds.length\n ? await em.find(Role, { id: { $in: roleIds } })\n : []\n const roleNames = roles.map((role) => role.name).filter((name): name is string => typeof name === 'string' && name.length > 0)\n\n try {\n record.lastUsedAt = new Date()\n await em.persistAndFlush(record)\n } catch {\n // best-effort update; ignore write failures\n }\n\n // For session keys, use sessionUserId; for regular keys, use createdBy\n const actualUserId = record.sessionUserId ?? record.createdBy ?? null\n\n return {\n sub: `api_key:${record.id}`,\n tenantId: record.tenantId ?? null,\n orgId: record.organizationId ?? null,\n roles: roleNames,\n isApiKey: true,\n keyId: record.id,\n keyName: record.name,\n ...(actualUserId ? { userId: actualUserId } : {}),\n }\n } catch {\n return null\n }\n}\n\nfunction extractApiKey(req: Request): string | null {\n const header = (req.headers.get('x-api-key') || '').trim()\n if (header) return header\n const authHeader = (req.headers.get('authorization') || '').trim()\n if (authHeader.toLowerCase().startsWith('apikey ')) {\n return authHeader.slice(7).trim()\n }\n return null\n}\n\nexport async function getAuthFromCookies(): Promise<AuthContext> {\n const cookieStore = await cookies()\n const token = cookieStore.get('auth_token')?.value\n if (!token) return null\n try {\n const payload = verifyJwt(token) as AuthContext\n if (!payload) return null\n const tenantCookie = cookieStore.get(TENANT_COOKIE_NAME)?.value\n const orgCookie = cookieStore.get(ORGANIZATION_COOKIE_NAME)?.value\n return applySuperAdminScope(payload, tenantCookie, orgCookie)\n } catch {\n return null\n }\n}\n\nexport async function getAuthFromRequest(req: Request): Promise<AuthContext> {\n const cookieHeader = req.headers.get('cookie') || ''\n const tenantCookie = readCookieFromHeader(cookieHeader, TENANT_COOKIE_NAME)\n const orgCookie = readCookieFromHeader(cookieHeader, ORGANIZATION_COOKIE_NAME)\n const authHeader = (req.headers.get('authorization') || '').trim()\n let token: string | undefined\n if (authHeader.toLowerCase().startsWith('bearer ')) token = authHeader.slice(7).trim()\n if (!token) {\n const match = cookieHeader.match(/(?:^|;\\s*)auth_token=([^;]+)/)\n if (match) token = decodeURIComponent(match[1])\n }\n if (token) {\n try {\n const payload = verifyJwt(token) as AuthContext\n if (payload) return applySuperAdminScope(payload, tenantCookie, orgCookie)\n } catch {\n // fall back to API key detection\n }\n }\n\n const apiKey = extractApiKey(req)\n if (!apiKey) return null\n const apiAuth = await resolveApiKeyAuth(apiKey)\n if (!apiAuth) return null\n return applySuperAdminScope(apiAuth, tenantCookie, orgCookie)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,10 +1,17 @@
1
1
  import { Resend } from "resend";
2
- async function sendEmail({ to, subject, react, from }) {
2
+ async function sendEmail({ to, subject, react, from, replyTo }) {
3
3
  const apiKey = process.env.RESEND_API_KEY;
4
4
  if (!apiKey) throw new Error("RESEND_API_KEY is not set");
5
5
  const resend = new Resend(apiKey);
6
6
  const fromAddr = from || process.env.EMAIL_FROM || "no-reply@localhost";
7
- await resend.emails.send({ to, subject, from: fromAddr, react });
7
+ const payload = {
8
+ to,
9
+ subject,
10
+ from: fromAddr,
11
+ react,
12
+ ...replyTo ? { reply_to: replyTo } : {}
13
+ };
14
+ await resend.emails.send(payload);
8
15
  }
9
16
  export {
10
17
  sendEmail
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/email/send.ts"],
4
- "sourcesContent": ["import { Resend } from 'resend'\nimport React from 'react'\n\nexport type SendEmailOptions = {\n to: string\n subject: string\n react: React.ReactElement\n from?: string\n}\n\nexport async function sendEmail({ to, subject, react, from }: SendEmailOptions) {\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) throw new Error('RESEND_API_KEY is not set')\n const resend = new Resend(apiKey)\n const fromAddr = from || process.env.EMAIL_FROM || 'no-reply@localhost'\n await resend.emails.send({ to, subject, from: fromAddr, react })\n}\n\n"],
5
- "mappings": "AAAA,SAAS,cAAc;AAUvB,eAAsB,UAAU,EAAE,IAAI,SAAS,OAAO,KAAK,GAAqB;AAC9E,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,2BAA2B;AACxD,QAAM,SAAS,IAAI,OAAO,MAAM;AAChC,QAAM,WAAW,QAAQ,QAAQ,IAAI,cAAc;AACnD,QAAM,OAAO,OAAO,KAAK,EAAE,IAAI,SAAS,MAAM,UAAU,MAAM,CAAC;AACjE;",
4
+ "sourcesContent": ["import { Resend } from 'resend'\nimport React from 'react'\n\nexport type SendEmailOptions = {\n to: string\n subject: string\n react: React.ReactElement\n from?: string\n replyTo?: string\n}\n\nexport async function sendEmail({ to, subject, react, from, replyTo }: SendEmailOptions) {\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) throw new Error('RESEND_API_KEY is not set')\n const resend = new Resend(apiKey)\n const fromAddr = from || process.env.EMAIL_FROM || 'no-reply@localhost'\n const payload = {\n to,\n subject,\n from: fromAddr,\n react,\n ...(replyTo ? { reply_to: replyTo } : {}),\n }\n await resend.emails.send(payload)\n}\n"],
5
+ "mappings": "AAAA,SAAS,cAAc;AAWvB,eAAsB,UAAU,EAAE,IAAI,SAAS,OAAO,MAAM,QAAQ,GAAqB;AACvF,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,2BAA2B;AACxD,QAAM,SAAS,IAAI,OAAO,MAAM;AAChC,QAAM,WAAW,QAAQ,QAAQ,IAAI,cAAc;AACnD,QAAM,UAAU;AAAA,IACd;AAAA,IACA;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA,GAAI,UAAU,EAAE,UAAU,QAAQ,IAAI,CAAC;AAAA,EACzC;AACA,QAAM,OAAO,OAAO,KAAK,OAAO;AAClC;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,48 @@
1
+ const NOTIFICATION_DOM_EVENTS = {
2
+ NEW: "om:notifications:new",
3
+ ACTIONED: "om:notifications:actioned",
4
+ COUNT_CHANGED: "om:notifications:count-changed"
5
+ };
6
+ function emitNotificationNew(detail) {
7
+ if (typeof window === "undefined" || typeof CustomEvent === "undefined") return;
8
+ window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.NEW, { detail }));
9
+ }
10
+ function emitNotificationActioned(notificationId) {
11
+ if (typeof window === "undefined" || typeof CustomEvent === "undefined") return;
12
+ window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.ACTIONED, { detail: { notificationId } }));
13
+ }
14
+ function emitNotificationCountChanged(count) {
15
+ if (typeof window === "undefined" || typeof CustomEvent === "undefined") return;
16
+ window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, { detail: { count } }));
17
+ }
18
+ function subscribeNotificationNew(handler) {
19
+ if (typeof window === "undefined") return () => {
20
+ };
21
+ const listener = (event) => handler(event.detail);
22
+ window.addEventListener(NOTIFICATION_DOM_EVENTS.NEW, listener);
23
+ return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.NEW, listener);
24
+ }
25
+ function subscribeNotificationActioned(handler) {
26
+ if (typeof window === "undefined") return () => {
27
+ };
28
+ const listener = (event) => handler(event.detail.notificationId);
29
+ window.addEventListener(NOTIFICATION_DOM_EVENTS.ACTIONED, listener);
30
+ return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.ACTIONED, listener);
31
+ }
32
+ function subscribeNotificationCountChanged(handler) {
33
+ if (typeof window === "undefined") return () => {
34
+ };
35
+ const listener = (event) => handler(event.detail.count);
36
+ window.addEventListener(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, listener);
37
+ return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, listener);
38
+ }
39
+ export {
40
+ NOTIFICATION_DOM_EVENTS,
41
+ emitNotificationActioned,
42
+ emitNotificationCountChanged,
43
+ emitNotificationNew,
44
+ subscribeNotificationActioned,
45
+ subscribeNotificationCountChanged,
46
+ subscribeNotificationNew
47
+ };
48
+ //# sourceMappingURL=notificationEvents.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/frontend/notificationEvents.ts"],
4
+ "sourcesContent": ["export const NOTIFICATION_DOM_EVENTS = {\n NEW: 'om:notifications:new',\n ACTIONED: 'om:notifications:actioned',\n COUNT_CHANGED: 'om:notifications:count-changed',\n} as const\n\nexport type NotificationNewDetail = {\n id: string\n type: string\n title: string\n severity: string\n}\n\nexport function emitNotificationNew(detail: NotificationNewDetail): void {\n if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return\n window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.NEW, { detail }))\n}\n\nexport function emitNotificationActioned(notificationId: string): void {\n if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return\n window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.ACTIONED, { detail: { notificationId } }))\n}\n\nexport function emitNotificationCountChanged(count: number): void {\n if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return\n window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, { detail: { count } }))\n}\n\nexport function subscribeNotificationNew(handler: (detail: NotificationNewDetail) => void): () => void {\n if (typeof window === 'undefined') return () => {}\n const listener = (event: Event) => handler((event as CustomEvent<NotificationNewDetail>).detail)\n window.addEventListener(NOTIFICATION_DOM_EVENTS.NEW, listener)\n return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.NEW, listener)\n}\n\nexport function subscribeNotificationActioned(handler: (notificationId: string) => void): () => void {\n if (typeof window === 'undefined') return () => {}\n const listener = (event: Event) => handler((event as CustomEvent<{ notificationId: string }>).detail.notificationId)\n window.addEventListener(NOTIFICATION_DOM_EVENTS.ACTIONED, listener)\n return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.ACTIONED, listener)\n}\n\nexport function subscribeNotificationCountChanged(handler: (count: number) => void): () => void {\n if (typeof window === 'undefined') return () => {}\n const listener = (event: Event) => handler((event as CustomEvent<{ count: number }>).detail.count)\n window.addEventListener(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, listener)\n return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, listener)\n}\n"],
5
+ "mappings": "AAAO,MAAM,0BAA0B;AAAA,EACrC,KAAK;AAAA,EACL,UAAU;AAAA,EACV,eAAe;AACjB;AASO,SAAS,oBAAoB,QAAqC;AACvE,MAAI,OAAO,WAAW,eAAe,OAAO,gBAAgB,YAAa;AACzE,SAAO,cAAc,IAAI,YAAY,wBAAwB,KAAK,EAAE,OAAO,CAAC,CAAC;AAC/E;AAEO,SAAS,yBAAyB,gBAA8B;AACrE,MAAI,OAAO,WAAW,eAAe,OAAO,gBAAgB,YAAa;AACzE,SAAO,cAAc,IAAI,YAAY,wBAAwB,UAAU,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC,CAAC;AACxG;AAEO,SAAS,6BAA6B,OAAqB;AAChE,MAAI,OAAO,WAAW,eAAe,OAAO,gBAAgB,YAAa;AACzE,SAAO,cAAc,IAAI,YAAY,wBAAwB,eAAe,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;AACpG;AAEO,SAAS,yBAAyB,SAA8D;AACrG,MAAI,OAAO,WAAW,YAAa,QAAO,MAAM;AAAA,EAAC;AACjD,QAAM,WAAW,CAAC,UAAiB,QAAS,MAA6C,MAAM;AAC/F,SAAO,iBAAiB,wBAAwB,KAAK,QAAQ;AAC7D,SAAO,MAAM,OAAO,oBAAoB,wBAAwB,KAAK,QAAQ;AAC/E;AAEO,SAAS,8BAA8B,SAAuD;AACnG,MAAI,OAAO,WAAW,YAAa,QAAO,MAAM;AAAA,EAAC;AACjD,QAAM,WAAW,CAAC,UAAiB,QAAS,MAAkD,OAAO,cAAc;AACnH,SAAO,iBAAiB,wBAAwB,UAAU,QAAQ;AAClE,SAAO,MAAM,OAAO,oBAAoB,wBAAwB,UAAU,QAAQ;AACpF;AAEO,SAAS,kCAAkC,SAA8C;AAC9F,MAAI,OAAO,WAAW,YAAa,QAAO,MAAM;AAAA,EAAC;AACjD,QAAM,WAAW,CAAC,UAAiB,QAAS,MAAyC,OAAO,KAAK;AACjG,SAAO,iBAAiB,wBAAwB,eAAe,QAAQ;AACvE,SAAO,MAAM,OAAO,oBAAoB,wBAAwB,eAAe,QAAQ;AACzF;",
6
+ "names": []
7
+ }
@@ -1,10 +1,10 @@
1
1
  import { Resend } from "resend";
2
- async function sendEmail({ to, subject, react, from }) {
2
+ async function sendEmail({ to, subject, react, from, replyTo }) {
3
3
  const apiKey = process.env.RESEND_API_KEY;
4
4
  if (!apiKey) throw new Error("RESEND_API_KEY is not set");
5
5
  const resend = new Resend(apiKey);
6
6
  const fromAddr = from || process.env.EMAIL_FROM || "no-reply@localhost";
7
- await resend.emails.send({ to, subject, from: fromAddr, react });
7
+ await resend.emails.send({ to, subject, from: fromAddr, react, replyTo });
8
8
  }
9
9
  export {
10
10
  sendEmail
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/lib/lib/email/send.ts"],
4
- "sourcesContent": ["import { Resend } from 'resend'\nimport React from 'react'\n\nexport type SendEmailOptions = {\n to: string\n subject: string\n react: React.ReactElement\n from?: string\n}\n\nexport async function sendEmail({ to, subject, react, from }: SendEmailOptions) {\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) throw new Error('RESEND_API_KEY is not set')\n const resend = new Resend(apiKey)\n const fromAddr = from || process.env.EMAIL_FROM || 'no-reply@localhost'\n await resend.emails.send({ to, subject, from: fromAddr, react })\n}\n\n"],
5
- "mappings": "AAAA,SAAS,cAAc;AAUvB,eAAsB,UAAU,EAAE,IAAI,SAAS,OAAO,KAAK,GAAqB;AAC9E,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,2BAA2B;AACxD,QAAM,SAAS,IAAI,OAAO,MAAM;AAChC,QAAM,WAAW,QAAQ,QAAQ,IAAI,cAAc;AACnD,QAAM,OAAO,OAAO,KAAK,EAAE,IAAI,SAAS,MAAM,UAAU,MAAM,CAAC;AACjE;",
4
+ "sourcesContent": ["import { Resend } from 'resend'\nimport React from 'react'\n\nexport type SendEmailOptions = {\n to: string\n subject: string\n react: React.ReactElement\n from?: string\n replyTo?: string\n}\n\nexport async function sendEmail({ to, subject, react, from, replyTo }: SendEmailOptions) {\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) throw new Error('RESEND_API_KEY is not set')\n const resend = new Resend(apiKey)\n const fromAddr = from || process.env.EMAIL_FROM || 'no-reply@localhost'\n await resend.emails.send({ to, subject, from: fromAddr, react, replyTo })\n}\n"],
5
+ "mappings": "AAAA,SAAS,cAAc;AAWvB,eAAsB,UAAU,EAAE,IAAI,SAAS,OAAO,MAAM,QAAQ,GAAqB;AACvF,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,2BAA2B;AACxD,QAAM,SAAS,IAAI,OAAO,MAAM;AAChC,QAAM,WAAW,QAAQ,QAAQ,IAAI,cAAc;AACnD,QAAM,OAAO,OAAO,KAAK,EAAE,IAAI,SAAS,MAAM,UAAU,OAAO,QAAQ,CAAC;AAC1E;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.4.2-canary-e6bf6a353e";
1
+ const APP_VERSION = "0.4.2-canary-49d47ff90e";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.4.2-canary-e6bf6a353e'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.4.2-canary-49d47ff90e'\nexport const appVersion = APP_VERSION\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,2 @@
1
+ export * from "./types.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/notifications/index.ts"],
4
+ "sourcesContent": ["export * from './types'\n"],
5
+ "mappings": "AAAA,cAAc;",
6
+ "names": []
7
+ }
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.4.2-canary-e6bf6a353e",
3
+ "version": "0.4.2-canary-49d47ff90e",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -0,0 +1,36 @@
1
+ import type { AwilixContainer } from 'awilix'
2
+ import { createRequestContainer } from '../di/container'
3
+ import { getAuthFromRequest, type AuthContext } from '../auth/server'
4
+ import { resolveTranslations } from '../i18n/server'
5
+
6
+ export type RequestContext = {
7
+ container: AwilixContainer
8
+ auth: AuthContext
9
+ organizationScope?: unknown
10
+ selectedOrganizationId?: string | null
11
+ organizationIds?: string[] | null
12
+ translate: (key: string, fallback?: string) => string
13
+ }
14
+
15
+ export type ResolveRequestContextResult = {
16
+ ctx: RequestContext
17
+ }
18
+
19
+ /**
20
+ * Resolves the request context for API routes.
21
+ * This includes container, auth, and translations.
22
+ * For organization-scoped routes, use resolveOrganizationScopeForRequest separately.
23
+ */
24
+ export async function resolveRequestContext(req: Request): Promise<ResolveRequestContextResult> {
25
+ const container = await createRequestContainer()
26
+ const auth = await getAuthFromRequest(req)
27
+ const { translate } = await resolveTranslations()
28
+
29
+ const ctx: RequestContext = {
30
+ container,
31
+ auth,
32
+ translate,
33
+ }
34
+
35
+ return { ctx }
36
+ }
@@ -0,0 +1,150 @@
1
+ import { z } from 'zod'
2
+ import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
3
+
4
+ export type PasswordPolicy = {
5
+ minLength: number
6
+ requireDigit: boolean
7
+ requireUppercase: boolean
8
+ requireSpecial: boolean
9
+ }
10
+
11
+ export type PasswordRequirementId = 'minLength' | 'digit' | 'uppercase' | 'special'
12
+
13
+ export type PasswordRequirement = {
14
+ id: PasswordRequirementId
15
+ value?: number
16
+ }
17
+
18
+ export type PasswordValidationResult = {
19
+ ok: boolean
20
+ violations: PasswordRequirementId[]
21
+ }
22
+
23
+ export type PasswordRequirementFormatter = (
24
+ key: string,
25
+ fallback: string,
26
+ params?: Record<string, string | number>,
27
+ ) => string
28
+
29
+ const DEFAULT_POLICY: PasswordPolicy = {
30
+ minLength: 6,
31
+ requireDigit: true,
32
+ requireUppercase: true,
33
+ requireSpecial: true,
34
+ }
35
+
36
+ const ENV_KEYS = {
37
+ minLength: 'OM_PASSWORD_MIN_LENGTH',
38
+ requireDigit: 'OM_PASSWORD_REQUIRE_DIGIT',
39
+ requireUppercase: 'OM_PASSWORD_REQUIRE_UPPERCASE',
40
+ requireSpecial: 'OM_PASSWORD_REQUIRE_SPECIAL',
41
+ } as const
42
+
43
+ const PUBLIC_PREFIX = 'NEXT_PUBLIC_'
44
+
45
+ function readEnvValue(env: NodeJS.ProcessEnv, key: keyof typeof ENV_KEYS): string | undefined {
46
+ const rawKey = ENV_KEYS[key]
47
+ const publicKey = `${PUBLIC_PREFIX}${rawKey}`
48
+ const rawValue = env[rawKey]
49
+ if (typeof rawValue === 'string' && rawValue.trim().length > 0) return rawValue
50
+ const publicValue = env[publicKey]
51
+ if (typeof publicValue === 'string' && publicValue.trim().length > 0) return publicValue
52
+ return undefined
53
+ }
54
+
55
+ function parsePositiveInt(raw: string | undefined, fallback: number, min = 1): number {
56
+ if (!raw) return fallback
57
+ const parsed = Number.parseInt(raw, 10)
58
+ if (Number.isNaN(parsed)) return fallback
59
+ return Math.max(min, parsed)
60
+ }
61
+
62
+ export function getPasswordPolicy(env: NodeJS.ProcessEnv = process.env): PasswordPolicy {
63
+ const minLength = parsePositiveInt(
64
+ readEnvValue(env, 'minLength'),
65
+ DEFAULT_POLICY.minLength,
66
+ 1,
67
+ )
68
+ return {
69
+ minLength,
70
+ requireDigit: parseBooleanWithDefault(
71
+ readEnvValue(env, 'requireDigit'),
72
+ DEFAULT_POLICY.requireDigit,
73
+ ),
74
+ requireUppercase: parseBooleanWithDefault(
75
+ readEnvValue(env, 'requireUppercase'),
76
+ DEFAULT_POLICY.requireUppercase,
77
+ ),
78
+ requireSpecial: parseBooleanWithDefault(
79
+ readEnvValue(env, 'requireSpecial'),
80
+ DEFAULT_POLICY.requireSpecial,
81
+ ),
82
+ }
83
+ }
84
+
85
+ export function getPasswordRequirements(policy: PasswordPolicy = getPasswordPolicy()): PasswordRequirement[] {
86
+ const requirements: PasswordRequirement[] = [{ id: 'minLength', value: policy.minLength }]
87
+ if (policy.requireDigit) requirements.push({ id: 'digit' })
88
+ if (policy.requireUppercase) requirements.push({ id: 'uppercase' })
89
+ if (policy.requireSpecial) requirements.push({ id: 'special' })
90
+ return requirements
91
+ }
92
+
93
+ export function formatPasswordRequirements(
94
+ policy: PasswordPolicy,
95
+ translate: PasswordRequirementFormatter,
96
+ keyPrefix = 'auth.password.requirements',
97
+ ): string {
98
+ const items = getPasswordRequirements(policy).map((requirement) => {
99
+ switch (requirement.id) {
100
+ case 'minLength':
101
+ return translate(
102
+ `${keyPrefix}.minLength`,
103
+ 'At least {min} characters',
104
+ { min: requirement.value ?? policy.minLength },
105
+ )
106
+ case 'digit':
107
+ return translate(`${keyPrefix}.digit`, 'One number')
108
+ case 'uppercase':
109
+ return translate(`${keyPrefix}.uppercase`, 'One uppercase letter')
110
+ case 'special':
111
+ return translate(`${keyPrefix}.special`, 'One special character')
112
+ default:
113
+ return ''
114
+ }
115
+ }).filter((value) => value && value.trim().length > 0)
116
+
117
+ if (!items.length) return ''
118
+ const separator = translate(`${keyPrefix}.separator`, ', ')
119
+ return items.join(separator)
120
+ }
121
+
122
+ export function validatePassword(
123
+ password: string,
124
+ policy: PasswordPolicy = getPasswordPolicy(),
125
+ ): PasswordValidationResult {
126
+ const violations: PasswordRequirementId[] = []
127
+ if (password.length < policy.minLength) violations.push('minLength')
128
+ if (policy.requireDigit && !/[0-9]/.test(password)) violations.push('digit')
129
+ if (policy.requireUppercase && !/[A-Z]/.test(password)) violations.push('uppercase')
130
+ if (policy.requireSpecial && !/[^A-Za-z0-9]/.test(password)) violations.push('special')
131
+ return { ok: violations.length === 0, violations }
132
+ }
133
+
134
+ export function buildPasswordSchema(options?: {
135
+ policy?: PasswordPolicy
136
+ maxLength?: number
137
+ message?: string
138
+ }): z.ZodType<string> {
139
+ const policy = options?.policy ?? getPasswordPolicy()
140
+ const maxLength = options?.maxLength
141
+ const message = options?.message ?? 'Password does not meet the requirements.'
142
+ let schema = z.string().min(policy.minLength, message)
143
+ if (typeof maxLength === 'number') schema = schema.max(maxLength, message)
144
+ return schema.superRefine((value, ctx) => {
145
+ const result = validatePassword(value, policy)
146
+ if (!result.ok) {
147
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message })
148
+ }
149
+ })
150
+ }
@@ -14,6 +14,7 @@ export type AuthContext = {
14
14
  email?: string
15
15
  roles?: string[]
16
16
  isApiKey?: boolean
17
+ userId?: string
17
18
  keyId?: string
18
19
  keyName?: string
19
20
  [k: string]: unknown
@@ -132,6 +133,9 @@ async function resolveApiKeyAuth(secret: string): Promise<AuthContext> {
132
133
  // best-effort update; ignore write failures
133
134
  }
134
135
 
136
+ // For session keys, use sessionUserId; for regular keys, use createdBy
137
+ const actualUserId = record.sessionUserId ?? record.createdBy ?? null
138
+
135
139
  return {
136
140
  sub: `api_key:${record.id}`,
137
141
  tenantId: record.tenantId ?? null,
@@ -140,6 +144,7 @@ async function resolveApiKeyAuth(secret: string): Promise<AuthContext> {
140
144
  isApiKey: true,
141
145
  keyId: record.id,
142
146
  keyName: record.name,
147
+ ...(actualUserId ? { userId: actualUserId } : {}),
143
148
  }
144
149
  } catch {
145
150
  return null
@@ -0,0 +1,60 @@
1
+ import React from 'react'
2
+ import { sendEmail } from '../send'
3
+
4
+ const sendMock = jest.fn().mockResolvedValue({ id: 'email-1' })
5
+ const ResendMock = jest.fn().mockImplementation(() => ({
6
+ emails: { send: sendMock },
7
+ }))
8
+
9
+ jest.mock('resend', () => ({
10
+ Resend: ResendMock,
11
+ }))
12
+
13
+ describe('sendEmail', () => {
14
+ const originalEnv = process.env
15
+
16
+ beforeEach(() => {
17
+ process.env = {
18
+ ...originalEnv,
19
+ RESEND_API_KEY: 'test-key',
20
+ EMAIL_FROM: 'from@example.com',
21
+ }
22
+ sendMock.mockClear()
23
+ ResendMock.mockClear()
24
+ })
25
+
26
+ afterEach(() => {
27
+ process.env = originalEnv
28
+ })
29
+
30
+ it('maps replyTo to reply_to in Resend payload', async () => {
31
+ await sendEmail({
32
+ to: 'user@example.com',
33
+ subject: 'Hello',
34
+ react: React.createElement('div', null, 'Hi'),
35
+ replyTo: 'reply@example.com',
36
+ })
37
+
38
+ expect(ResendMock).toHaveBeenCalledWith('test-key')
39
+ expect(sendMock).toHaveBeenCalledWith(
40
+ expect.objectContaining({
41
+ to: 'user@example.com',
42
+ subject: 'Hello',
43
+ from: 'from@example.com',
44
+ reply_to: 'reply@example.com',
45
+ })
46
+ )
47
+ })
48
+
49
+ it('omits reply_to when replyTo is not provided', async () => {
50
+ await sendEmail({
51
+ to: 'user@example.com',
52
+ subject: 'Hello',
53
+ react: React.createElement('div', null, 'Hi'),
54
+ })
55
+
56
+ const payload = sendMock.mock.calls[0]?.[0] as Record<string, unknown>
57
+ expect(payload).toBeDefined()
58
+ expect(payload.reply_to).toBeUndefined()
59
+ })
60
+ })
@@ -6,13 +6,20 @@ export type SendEmailOptions = {
6
6
  subject: string
7
7
  react: React.ReactElement
8
8
  from?: string
9
+ replyTo?: string
9
10
  }
10
11
 
11
- export async function sendEmail({ to, subject, react, from }: SendEmailOptions) {
12
+ export async function sendEmail({ to, subject, react, from, replyTo }: SendEmailOptions) {
12
13
  const apiKey = process.env.RESEND_API_KEY
13
14
  if (!apiKey) throw new Error('RESEND_API_KEY is not set')
14
15
  const resend = new Resend(apiKey)
15
16
  const fromAddr = from || process.env.EMAIL_FROM || 'no-reply@localhost'
16
- await resend.emails.send({ to, subject, from: fromAddr, react })
17
+ const payload = {
18
+ to,
19
+ subject,
20
+ from: fromAddr,
21
+ react,
22
+ ...(replyTo ? { reply_to: replyTo } : {}),
23
+ }
24
+ await resend.emails.send(payload)
17
25
  }
18
-
@@ -0,0 +1,48 @@
1
+ export const NOTIFICATION_DOM_EVENTS = {
2
+ NEW: 'om:notifications:new',
3
+ ACTIONED: 'om:notifications:actioned',
4
+ COUNT_CHANGED: 'om:notifications:count-changed',
5
+ } as const
6
+
7
+ export type NotificationNewDetail = {
8
+ id: string
9
+ type: string
10
+ title: string
11
+ severity: string
12
+ }
13
+
14
+ export function emitNotificationNew(detail: NotificationNewDetail): void {
15
+ if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return
16
+ window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.NEW, { detail }))
17
+ }
18
+
19
+ export function emitNotificationActioned(notificationId: string): void {
20
+ if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return
21
+ window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.ACTIONED, { detail: { notificationId } }))
22
+ }
23
+
24
+ export function emitNotificationCountChanged(count: number): void {
25
+ if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return
26
+ window.dispatchEvent(new CustomEvent(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, { detail: { count } }))
27
+ }
28
+
29
+ export function subscribeNotificationNew(handler: (detail: NotificationNewDetail) => void): () => void {
30
+ if (typeof window === 'undefined') return () => {}
31
+ const listener = (event: Event) => handler((event as CustomEvent<NotificationNewDetail>).detail)
32
+ window.addEventListener(NOTIFICATION_DOM_EVENTS.NEW, listener)
33
+ return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.NEW, listener)
34
+ }
35
+
36
+ export function subscribeNotificationActioned(handler: (notificationId: string) => void): () => void {
37
+ if (typeof window === 'undefined') return () => {}
38
+ const listener = (event: Event) => handler((event as CustomEvent<{ notificationId: string }>).detail.notificationId)
39
+ window.addEventListener(NOTIFICATION_DOM_EVENTS.ACTIONED, listener)
40
+ return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.ACTIONED, listener)
41
+ }
42
+
43
+ export function subscribeNotificationCountChanged(handler: (count: number) => void): () => void {
44
+ if (typeof window === 'undefined') return () => {}
45
+ const listener = (event: Event) => handler((event as CustomEvent<{ count: number }>).detail.count)
46
+ window.addEventListener(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, listener)
47
+ return () => window.removeEventListener(NOTIFICATION_DOM_EVENTS.COUNT_CHANGED, listener)
48
+ }
@@ -6,13 +6,13 @@ export type SendEmailOptions = {
6
6
  subject: string
7
7
  react: React.ReactElement
8
8
  from?: string
9
+ replyTo?: string
9
10
  }
10
11
 
11
- export async function sendEmail({ to, subject, react, from }: SendEmailOptions) {
12
+ export async function sendEmail({ to, subject, react, from, replyTo }: SendEmailOptions) {
12
13
  const apiKey = process.env.RESEND_API_KEY
13
14
  if (!apiKey) throw new Error('RESEND_API_KEY is not set')
14
15
  const resend = new Resend(apiKey)
15
16
  const fromAddr = from || process.env.EMAIL_FROM || 'no-reply@localhost'
16
- await resend.emails.send({ to, subject, from: fromAddr, react })
17
+ await resend.emails.send({ to, subject, from: fromAddr, react, replyTo })
17
18
  }
18
-
@@ -0,0 +1 @@
1
+ export * from './types'
@@ -0,0 +1,106 @@
1
+ import type { ComponentType } from 'react'
2
+
3
+ export type NotificationStatus = 'unread' | 'read' | 'actioned' | 'dismissed'
4
+ export type NotificationSeverity = 'info' | 'warning' | 'success' | 'error'
5
+
6
+ export type NotificationAction = {
7
+ id: string
8
+ label: string
9
+ labelKey?: string
10
+ variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost'
11
+ icon?: string
12
+ commandId?: string
13
+ href?: string
14
+ confirmRequired?: boolean
15
+ confirmMessage?: string
16
+ }
17
+
18
+ export type NotificationActionData = {
19
+ actions: NotificationAction[]
20
+ primaryActionId?: string
21
+ }
22
+
23
+ export type NotificationTypeAction = {
24
+ id: string
25
+ labelKey: string
26
+ variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost'
27
+ icon?: string
28
+ commandId?: string
29
+ href?: string
30
+ confirmRequired?: boolean
31
+ confirmMessageKey?: string
32
+ }
33
+
34
+ export type NotificationRendererProps = {
35
+ notification: {
36
+ id: string
37
+ type: string
38
+ title: string
39
+ body?: string | null
40
+ titleKey?: string | null
41
+ bodyKey?: string | null
42
+ titleVariables?: Record<string, string> | null
43
+ bodyVariables?: Record<string, string> | null
44
+ icon?: string | null
45
+ severity: string
46
+ status: string
47
+ sourceModule?: string | null
48
+ sourceEntityType?: string | null
49
+ sourceEntityId?: string | null
50
+ linkHref?: string | null
51
+ createdAt: string
52
+ }
53
+ onAction: (actionId: string) => Promise<void>
54
+ onDismiss: () => Promise<void>
55
+ actions: NotificationTypeAction[]
56
+ }
57
+
58
+ export type NotificationTypeDefinition = {
59
+ type: string
60
+ module: string
61
+ titleKey: string
62
+ bodyKey?: string
63
+ icon: string
64
+ severity: NotificationSeverity
65
+ actions: NotificationTypeAction[]
66
+ primaryActionId?: string
67
+ linkHref?: string
68
+ Renderer?: ComponentType<NotificationRendererProps>
69
+ expiresAfterHours?: number
70
+ }
71
+
72
+ export type NotificationDto = {
73
+ id: string
74
+ type: string
75
+ title: string
76
+ body?: string | null
77
+ titleKey?: string | null
78
+ bodyKey?: string | null
79
+ titleVariables?: Record<string, string> | null
80
+ bodyVariables?: Record<string, string> | null
81
+ icon?: string | null
82
+ severity: string
83
+ status: string
84
+ actions: Array<{
85
+ id: string
86
+ label: string
87
+ labelKey?: string
88
+ variant?: string
89
+ icon?: string
90
+ }>
91
+ primaryActionId?: string
92
+ sourceModule?: string | null
93
+ sourceEntityType?: string | null
94
+ sourceEntityId?: string | null
95
+ linkHref?: string | null
96
+ createdAt: string
97
+ readAt?: string | null
98
+ actionTaken?: string | null
99
+ }
100
+
101
+ export type NotificationPollData = {
102
+ unreadCount: number
103
+ recent: NotificationDto[]
104
+ hasNew: boolean
105
+ lastId?: string
106
+ }