@porulle/core 0.42.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@porulle/core",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
package/src/auth/actor.ts CHANGED
@@ -37,11 +37,7 @@ export function getCustomerPermissions(config: CommerceConfig): string[] {
37
37
  return config.auth?.customerPermissions ?? [...DEFAULT_CUSTOMER_PERMISSIONS];
38
38
  }
39
39
 
40
- function resolvePermissions(
41
- session: AuthSessionLike,
42
- config: CommerceConfig,
43
- ): string[] {
44
- const role = session.session.activeOrganizationRole;
40
+ function resolvePermissions(role: string | null | undefined, config: CommerceConfig): string[] {
45
41
  if (!role) return getCustomerPermissions(config);
46
42
  const roleConfig = config.auth?.roles?.[role];
47
43
  return roleConfig ? roleConfig.permissions : [];
@@ -124,7 +120,7 @@ export async function resolveActor(
124
120
  if (!session) return null;
125
121
 
126
122
  const defaultOrgId = config.auth?.defaultOrganizationId ?? DEFAULT_ORG_ID;
127
- let role = session.session.activeOrganizationRole as string | undefined;
123
+ let role: string | undefined;
128
124
  let orgId = session.session.activeOrganizationId as string | null;
129
125
 
130
126
  if (!role) {
@@ -141,10 +137,6 @@ export async function resolveActor(
141
137
  }
142
138
  }
143
139
 
144
- const enrichedSession = {
145
- ...session,
146
- session: { ...session.session, activeOrganizationRole: role ?? null },
147
- };
148
140
  return {
149
141
  type: "user",
150
142
  userId: session.user.id,
@@ -153,7 +145,7 @@ export async function resolveActor(
153
145
  vendorId: session.user.vendorId ?? null,
154
146
  organizationId: orgId ?? defaultOrgId,
155
147
  role: role ?? "customer",
156
- permissions: resolvePermissions(enrichedSession, config),
148
+ permissions: resolvePermissions(role, config),
157
149
  sessionCreatedAt: toIsoString(session.session.createdAt),
158
150
  } satisfies Actor;
159
151
  }
package/src/auth/setup.ts CHANGED
@@ -7,6 +7,7 @@ import type { CommerceConfig } from "../config/types.js";
7
7
  import type { DatabaseAdapter } from "../kernel/database/adapter.js";
8
8
  import * as authSchema from "./auth-schema.js";
9
9
  import { AUTH_COOKIE_PREFIX } from "./actor.js";
10
+ import { SUPPRESSED_AUTH_PATH_LIST } from "./suppressed-auth-paths.js";
10
11
 
11
12
  type BetterAuthDbProvider = "pg";
12
13
 
@@ -141,6 +142,11 @@ export function createAuth(
141
142
  try {
142
143
  const auth = betterAuth({
143
144
  ...(config.auth?.baseURL ? { baseURL: config.auth.baseURL } : {}),
145
+ // Endpoints whose only currency is a raw session bearer token. Read in
146
+ // the router's own onRequest, before rate limiting and before any plugin
147
+ // hook, so no mount point or config reaches them. See
148
+ // ./suppressed-auth-paths.ts for the per-path argument.
149
+ disabledPaths: [...SUPPRESSED_AUTH_PATH_LIST],
144
150
  database: drizzleAdapter(db.db as unknown as Record<string, unknown>, {
145
151
  provider: resolveAuthDbProvider(db.provider),
146
152
  schema: authSchema,
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Better Auth endpoints this repository refuses to serve, because their only
3
+ * currency is a raw session bearer token.
4
+ *
5
+ * THE DEFAULT IS TO SERVE. A Better Auth endpoint absent from this list is
6
+ * mounted, because suppressing one is a removal from a published surface and
7
+ * the list is the place where that removal is argued rather than assumed.
8
+ *
9
+ * These are handed to Better Auth's own `disabledPaths` option rather than
10
+ * refused by a middleware in front of the handler. The difference is what a
11
+ * future mount can reach around. `runtime/server.ts` funnels `/api/auth/*` into
12
+ * `auth.handler`, and `disabledPaths` is read inside that handler's router, in
13
+ * `onRequest`, BEFORE rate limiting and before any plugin hook — so there is no
14
+ * ordering, no second mount point and no consumer configuration that reaches
15
+ * the endpoint without passing it. A Hono middleware would have to be
16
+ * remembered at every mount point instead, and this is an auth surface.
17
+ *
18
+ * MATCHING IS EXACT, on the path Better Auth normalizes a request to: the
19
+ * pathname with the auth base path stripped, so `/api/auth/list-sessions`
20
+ * arrives here as `/list-sessions`. A consumer cannot move that out from under
21
+ * the list without breaking every auth route at once — `runtime/server.ts`
22
+ * mounts the handler at a literal `/api/auth/*`, and Better Auth derives the
23
+ * router's base path from `auth.baseURL`, so a `baseURL` carrying some other
24
+ * path makes EVERY endpoint unreachable rather than just these. The failure is
25
+ * loud, which is the property that matters: a guard that silently stops
26
+ * covering its route is worse than no guard.
27
+ *
28
+ * Suppression is unconditional and not configurable. A config flag here would
29
+ * be a documented way to switch the leak back on, and there is no caller that
30
+ * wants one: nothing in this repository calls either path.
31
+ */
32
+ export type SuppressedAuthPath = {
33
+ /** The path as Better Auth normalizes it — no `/api/auth` prefix. */
34
+ path: string;
35
+ justification: string;
36
+ };
37
+
38
+ export const SUPPRESSED_AUTH_PATHS: readonly SuppressedAuthPath[] = [
39
+ {
40
+ path: "/list-sessions",
41
+ justification:
42
+ "Returns every one of the caller's live sessions through parseSessionOutput, " +
43
+ "which filters by the session output schema — and `token` carries no " +
44
+ "`returned: false`, so each row arrives with its raw bearer token. One " +
45
+ "compromised session therefore yields durable capture of all of them, and " +
46
+ "the capture survives the victim revoking the session they know about. " +
47
+ "Its guard is freshSessionMiddleware, whose freshAge defaults to a day, so " +
48
+ "a session phished minutes ago is fresh enough to ask.",
49
+ },
50
+ {
51
+ path: "/revoke-session",
52
+ justification:
53
+ "Takes `{ token }` as its only handle, so it is the consumer of what " +
54
+ "/list-sessions leaks and has no legitimate caller once that path is gone: " +
55
+ "the only token a client can still hold is its own, which sign-out ends. " +
56
+ "Revoking by an opaque id belongs to the consumer route that owns the " +
57
+ "shaped list. /revoke-sessions and /revoke-other-sessions take no token " +
58
+ "and stay — they are the supported way to end a session whose token the " +
59
+ "caller cannot obtain.",
60
+ },
61
+ ];
62
+
63
+ /** The paths alone, in the shape Better Auth's `disabledPaths` option takes. */
64
+ export const SUPPRESSED_AUTH_PATH_LIST: readonly string[] = SUPPRESSED_AUTH_PATHS.map(
65
+ (entry) => entry.path,
66
+ );
@@ -501,7 +501,6 @@ export interface AuthSessionLike {
501
501
  };
502
502
  session: {
503
503
  activeOrganizationId?: string | null;
504
- activeOrganizationRole?: string | null;
505
504
  /** When the session was minted — the moment the human last authenticated. */
506
505
  createdAt?: Date | string | null;
507
506
  };
package/src/index.ts CHANGED
@@ -43,6 +43,8 @@ export { OrganizationService } from "./modules/organization/service.js";
43
43
  export { createScopedDb } from "./kernel/database/scoped-db.js";
44
44
  export type { ScopedOrganizationId } from "./kernel/database/scoped-db.js";
45
45
  export { IDENTITY_FREE_ROUTES, isIdentityFreeRoute } from "./auth/identity-free-routes.js";
46
+ export { SUPPRESSED_AUTH_PATHS, SUPPRESSED_AUTH_PATH_LIST } from "./auth/suppressed-auth-paths.js";
47
+ export type { SuppressedAuthPath } from "./auth/suppressed-auth-paths.js";
46
48
  export type { IdentityFreeRoute } from "./auth/identity-free-routes.js";
47
49
  export {
48
50
  AUTHENTICATION_REQUIRED_MESSAGE,