@porulle/core 0.43.0 → 0.45.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/dist/auth/actor.d.ts.map +1 -1
- package/dist/auth/actor.js +3 -8
- package/dist/auth/setup.d.ts.map +1 -1
- package/dist/auth/setup.js +36 -0
- package/dist/auth/suppressed-auth-paths.d.ts +40 -0
- package/dist/auth/suppressed-auth-paths.d.ts.map +1 -0
- package/dist/auth/suppressed-auth-paths.js +24 -0
- package/dist/config/types.d.ts +54 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/interfaces/rest/schemas/admin-entity-field-definitions.d.ts +4 -4
- package/dist/interfaces/rest/schemas/admin-jobs.d.ts +1 -1
- package/dist/interfaces/rest/schemas/catalog.d.ts +27 -27
- package/dist/interfaces/rest/schemas/media.d.ts +4 -4
- package/dist/interfaces/rest/schemas/promotions.d.ts +1 -1
- package/dist/interfaces/rest/schemas/responses.d.ts +5 -5
- package/dist/interfaces/rest/schemas/search.d.ts +1 -1
- package/dist/kernel/jobs/schema.d.ts +1 -1
- package/dist/modules/catalog/schema.d.ts +5 -5
- package/dist/modules/inventory/schema.d.ts +1 -1
- package/dist/modules/media/module.d.ts +1 -1
- package/dist/modules/media/schema.d.ts +1 -1
- package/dist/runtime/kernel-modules.d.ts +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +1 -1
- package/src/auth/actor.ts +3 -11
- package/src/auth/setup.ts +37 -0
- package/src/auth/suppressed-auth-paths.ts +66 -0
- package/src/config/types.ts +61 -1
- package/src/index.ts +2 -0
package/package.json
CHANGED
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
|
|
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(
|
|
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
|
|
|
@@ -140,7 +141,16 @@ export function createAuth(
|
|
|
140
141
|
|
|
141
142
|
try {
|
|
142
143
|
const auth = betterAuth({
|
|
144
|
+
// Upstream options core does not own, FIRST so every core-owned key below
|
|
145
|
+
// wins. `AuthConfig["extend"]` Omits the keys core builds, so a consumer
|
|
146
|
+
// cannot reach one from here — it is a compile error, not a silent drop.
|
|
147
|
+
...(config.auth?.extend ?? {}),
|
|
143
148
|
...(config.auth?.baseURL ? { baseURL: config.auth.baseURL } : {}),
|
|
149
|
+
// Endpoints whose only currency is a raw session bearer token. Read in
|
|
150
|
+
// the router's own onRequest, before rate limiting and before any plugin
|
|
151
|
+
// hook, so no mount point or config reaches them. See
|
|
152
|
+
// ./suppressed-auth-paths.ts for the per-path argument.
|
|
153
|
+
disabledPaths: [...SUPPRESSED_AUTH_PATH_LIST],
|
|
144
154
|
database: drizzleAdapter(db.db as unknown as Record<string, unknown>, {
|
|
145
155
|
provider: resolveAuthDbProvider(db.provider),
|
|
146
156
|
schema: authSchema,
|
|
@@ -152,6 +162,33 @@ export function createAuth(
|
|
|
152
162
|
emailAndPassword: {
|
|
153
163
|
enabled: true,
|
|
154
164
|
requireEmailVerification: config.auth?.requireEmailVerification ?? true,
|
|
165
|
+
// Forwarded from `config.auth.password`. Every one of these was dropped
|
|
166
|
+
// on the floor until 2026-09-20: Better Auth honours them, core never
|
|
167
|
+
// passed them, so setting them in a consumer config did nothing.
|
|
168
|
+
// `revokeSessionsOnPasswordReset` defaults to TRUE here, against Better
|
|
169
|
+
// Auth's own `false`. A password reset is the canonical "I think I am
|
|
170
|
+
// compromised" action, and leaving the attacker's session live through
|
|
171
|
+
// it is the wrong default for a commerce platform. Opt out explicitly.
|
|
172
|
+
revokeSessionsOnPasswordReset:
|
|
173
|
+
config.auth?.password?.revokeSessionsOnPasswordReset ?? true,
|
|
174
|
+
...(config.auth?.password?.minLength !== undefined
|
|
175
|
+
? { minPasswordLength: config.auth.password.minLength }
|
|
176
|
+
: {}),
|
|
177
|
+
...(config.auth?.password?.maxLength !== undefined
|
|
178
|
+
? { maxPasswordLength: config.auth.password.maxLength }
|
|
179
|
+
: {}),
|
|
180
|
+
...(config.auth?.password?.disableSignUp !== undefined
|
|
181
|
+
? { disableSignUp: config.auth.password.disableSignUp }
|
|
182
|
+
: {}),
|
|
183
|
+
...(config.auth?.password?.autoSignIn !== undefined
|
|
184
|
+
? { autoSignIn: config.auth.password.autoSignIn }
|
|
185
|
+
: {}),
|
|
186
|
+
...(config.auth?.password?.resetTokenExpiresIn !== undefined
|
|
187
|
+
? { resetPasswordTokenExpiresIn: config.auth.password.resetTokenExpiresIn }
|
|
188
|
+
: {}),
|
|
189
|
+
...(config.auth?.password?.onPasswordReset !== undefined
|
|
190
|
+
? { onPasswordReset: config.auth.password.onPasswordReset }
|
|
191
|
+
: {}),
|
|
155
192
|
sendResetPassword: async ({ user, url }: AuthEmailPayload) => {
|
|
156
193
|
if (!config.email) return;
|
|
157
194
|
await config.email.send({
|
|
@@ -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
|
+
);
|
package/src/config/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { betterAuth } from "better-auth";
|
|
1
2
|
import type { Hono, MiddlewareHandler } from "hono";
|
|
2
3
|
import type { Actor } from "../auth/types.js";
|
|
3
4
|
import type { BeforeHook, AfterHook } from "../kernel/hooks/types.js";
|
|
@@ -14,6 +15,9 @@ import type {
|
|
|
14
15
|
TaskDefinition,
|
|
15
16
|
} from "../kernel/jobs/types.js";
|
|
16
17
|
|
|
18
|
+
/** Better Auth's own options type, read off the function so no named export is required. */
|
|
19
|
+
type BetterAuthOptions = Parameters<typeof betterAuth>[0];
|
|
20
|
+
|
|
17
21
|
export interface RoleDefinition {
|
|
18
22
|
permissions: string[];
|
|
19
23
|
}
|
|
@@ -259,6 +263,63 @@ export interface AuthConfig {
|
|
|
259
263
|
* a trusted actor for integration tests. Default false.
|
|
260
264
|
*/
|
|
261
265
|
allowTestActor?: boolean;
|
|
266
|
+
/**
|
|
267
|
+
* Password policy and reset behaviour, forwarded into Better Auth's
|
|
268
|
+
* `emailAndPassword` block.
|
|
269
|
+
*
|
|
270
|
+
* These are surfaced deliberately rather than left to `extend`, because core
|
|
271
|
+
* OWNS `emailAndPassword` (it injects `sendResetPassword` and
|
|
272
|
+
* `sendVerificationEmail` from `config.email`), so a consumer cannot supply
|
|
273
|
+
* that block wholesale without losing the transactional email wiring.
|
|
274
|
+
*
|
|
275
|
+
* Every field here is one Better Auth honours and core previously dropped on
|
|
276
|
+
* the floor: setting them in a consumer config typechecked and did nothing.
|
|
277
|
+
*/
|
|
278
|
+
password?: {
|
|
279
|
+
/** Minimum password length. Better Auth's own default is 8. */
|
|
280
|
+
minLength?: number;
|
|
281
|
+
/** Maximum password length. Better Auth's own default is 128. */
|
|
282
|
+
maxLength?: number;
|
|
283
|
+
/** Block new email+password registrations. */
|
|
284
|
+
disableSignUp?: boolean;
|
|
285
|
+
/** Sign the user in immediately after sign-up. */
|
|
286
|
+
autoSignIn?: boolean;
|
|
287
|
+
/**
|
|
288
|
+
* End every other session when a password is reset. Better Auth defaults
|
|
289
|
+
* this to FALSE, which leaves an attacker's session live through the one
|
|
290
|
+
* action a compromised user takes to evict them. Set it to true unless you
|
|
291
|
+
* have a specific reason not to.
|
|
292
|
+
*/
|
|
293
|
+
revokeSessionsOnPasswordReset?: boolean;
|
|
294
|
+
/** Seconds a reset token stays valid. Better Auth's own default is 3600. */
|
|
295
|
+
resetTokenExpiresIn?: number;
|
|
296
|
+
/** Audit hook fired after a password is successfully changed. */
|
|
297
|
+
onPasswordReset?: (
|
|
298
|
+
data: { user: { id: string; email: string; name: string } },
|
|
299
|
+
request?: Request,
|
|
300
|
+
) => Promise<void>;
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Upstream Better Auth options core does not own, passed straight through.
|
|
304
|
+
*
|
|
305
|
+
* This is the escape hatch that stops the next upstream option from being
|
|
306
|
+
* silently dropped. The keys core builds itself are `Omit`ted, so setting one
|
|
307
|
+
* here is a COMPILE ERROR naming the key rather than a value that is accepted
|
|
308
|
+
* and ignored — the failure mode this option exists to remove.
|
|
309
|
+
*
|
|
310
|
+
* Spread FIRST in `createAuth`, so a core-owned key can never be clobbered.
|
|
311
|
+
*/
|
|
312
|
+
extend?: Omit<
|
|
313
|
+
BetterAuthOptions,
|
|
314
|
+
| "database"
|
|
315
|
+
| "trustedOrigins"
|
|
316
|
+
| "emailAndPassword"
|
|
317
|
+
| "socialProviders"
|
|
318
|
+
| "session"
|
|
319
|
+
| "advanced"
|
|
320
|
+
| "user"
|
|
321
|
+
| "plugins"
|
|
322
|
+
>;
|
|
262
323
|
}
|
|
263
324
|
|
|
264
325
|
export interface CartConfig {
|
|
@@ -501,7 +562,6 @@ export interface AuthSessionLike {
|
|
|
501
562
|
};
|
|
502
563
|
session: {
|
|
503
564
|
activeOrganizationId?: string | null;
|
|
504
|
-
activeOrganizationRole?: string | null;
|
|
505
565
|
/** When the session was minted — the moment the human last authenticated. */
|
|
506
566
|
createdAt?: Date | string | null;
|
|
507
567
|
};
|
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,
|