@porulle/core 0.44.0 → 0.46.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/setup.d.ts.map +1 -1
- package/dist/auth/setup.js +30 -0
- package/dist/config/types.d.ts +54 -0
- package/dist/config/types.d.ts.map +1 -1
- 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/entity-service.d.ts +17 -0
- package/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +26 -0
- package/dist/modules/catalog/schema.d.ts +5 -5
- package/dist/modules/catalog/service.d.ts.map +1 -1
- package/dist/modules/catalog/service.js +7 -0
- 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 -1
- package/package.json +1 -1
- package/src/auth/setup.ts +31 -0
- package/src/config/types.ts +61 -0
- package/src/modules/catalog/entity-service.ts +31 -0
- package/src/modules/catalog/service.ts +7 -0
package/package.json
CHANGED
package/src/auth/setup.ts
CHANGED
|
@@ -141,6 +141,10 @@ export function createAuth(
|
|
|
141
141
|
|
|
142
142
|
try {
|
|
143
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 ?? {}),
|
|
144
148
|
...(config.auth?.baseURL ? { baseURL: config.auth.baseURL } : {}),
|
|
145
149
|
// Endpoints whose only currency is a raw session bearer token. Read in
|
|
146
150
|
// the router's own onRequest, before rate limiting and before any plugin
|
|
@@ -158,6 +162,33 @@ export function createAuth(
|
|
|
158
162
|
emailAndPassword: {
|
|
159
163
|
enabled: true,
|
|
160
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
|
+
: {}),
|
|
161
192
|
sendResetPassword: async ({ user, url }: AuthEmailPayload) => {
|
|
162
193
|
if (!config.email) return;
|
|
163
194
|
await config.email.send({
|
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 {
|
|
@@ -538,6 +538,37 @@ export class EntityService {
|
|
|
538
538
|
return Ok(undefined);
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
+
/**
|
|
542
|
+
* Fire `catalog.afterUpdate` for an entity whose RELATED rows changed while the entity row did
|
|
543
|
+
* not. Same before/after pair as `setAttributes` above, for the same reason: every consumer of
|
|
544
|
+
* this hook keys on the entity, not on the row that actually moved.
|
|
545
|
+
*
|
|
546
|
+
* It exists because `CatalogService.reviewCustomField` wrote an approved custom field straight
|
|
547
|
+
* through the repository and ran no hooks at all. `create`, `update` and `setAttributes` each
|
|
548
|
+
* resolve and run their after-hooks; that one method did not, so approving a proposal notified
|
|
549
|
+
* nothing. Anything downstream that keeps a derived copy of an entity's custom fields — a search
|
|
550
|
+
* projection, a feed, a cache — had no way to learn that a value a person had just approved was
|
|
551
|
+
* now the live one, and went on serving the old one until something else happened to touch the
|
|
552
|
+
* entity.
|
|
553
|
+
*
|
|
554
|
+
* Callers pass the paths they changed so a handler can narrow. A missing entity is not an error:
|
|
555
|
+
* the caller has already established it, and racing a delete is not this method's problem.
|
|
556
|
+
*/
|
|
557
|
+
async notifyEntityUpdated(
|
|
558
|
+
entityId: string,
|
|
559
|
+
changedFieldPaths: readonly string[],
|
|
560
|
+
actor: Actor | null,
|
|
561
|
+
ctx?: CatalogWriteContext,
|
|
562
|
+
): Promise<void> {
|
|
563
|
+
const txCtx = isWriteContextTransactional(ctx) ? ctx : undefined;
|
|
564
|
+
const entity = await this.repo.findEntityById(entityId, txCtx);
|
|
565
|
+
if (!entity) return;
|
|
566
|
+
const afterHooks = this.deps.hooks.resolve("catalog.afterUpdate") as CatalogUpdateAfterHook[];
|
|
567
|
+
const context = catalogHookContext(this.deps, actor, ctx, "update");
|
|
568
|
+
context.context.changedFieldPaths = [...changedFieldPaths];
|
|
569
|
+
await runAfterHooks(afterHooks, entity, entity, "update", context, (hook) => this.deps.hooks.runsInTransaction(hook));
|
|
570
|
+
}
|
|
571
|
+
|
|
541
572
|
async getAttributes(entityId: string, locale: string, actor: Actor | null, ctx?: TxContext): Promise<Result<SellableAttribute>> {
|
|
542
573
|
try {
|
|
543
574
|
assertPermission(actor, "catalog:read");
|
|
@@ -1050,6 +1050,13 @@ export class CatalogServiceImpl implements CatalogService {
|
|
|
1050
1050
|
await this.repository.rejectOtherProposedCustomFields(entityId, fieldName, locale, updated.id, txCtx);
|
|
1051
1051
|
}
|
|
1052
1052
|
await this.captureRevision(entityId, reviewer, "update", txCtx);
|
|
1053
|
+
// An approval REPLACES the live value: the approved row was deleted above and this proposal
|
|
1054
|
+
// takes its place. That is an entity change to every consumer of `catalog.afterUpdate`, and
|
|
1055
|
+
// until now it fired none, so an approved fact reached nothing downstream. A rejection is
|
|
1056
|
+
// not: the live row is untouched and there is nothing for a consumer to re-read.
|
|
1057
|
+
if (status === "approved") {
|
|
1058
|
+
await this.entities.notifyEntityUpdated(entityId, [`customFields.${locale}.${fieldName}`], reviewer, txCtx);
|
|
1059
|
+
}
|
|
1053
1060
|
return Ok(updated);
|
|
1054
1061
|
});
|
|
1055
1062
|
}
|