@porulle/core 0.13.0 → 0.15.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 +10 -0
- package/dist/auth/actor.d.ts.map +1 -0
- package/dist/auth/actor.js +91 -0
- package/dist/auth/middleware.d.ts +1 -1
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +5 -89
- package/dist/auth/setup.d.ts.map +1 -1
- package/dist/auth/setup.js +2 -1
- package/dist/config/types.d.ts +4 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/kernel/database/plugin-types.d.ts +11 -0
- package/dist/kernel/database/plugin-types.d.ts.map +1 -1
- package/dist/kernel/database/scoped-db.d.ts +12 -0
- package/dist/kernel/database/scoped-db.d.ts.map +1 -1
- package/dist/kernel/database/scoped-db.js +12 -0
- package/dist/kernel/plugin/manifest.d.ts +3 -0
- package/dist/kernel/plugin/manifest.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.js +35 -6
- package/dist/modules/search/adapter.d.ts +5 -0
- package/dist/modules/search/adapter.d.ts.map +1 -1
- package/dist/modules/search/module.d.ts.map +1 -1
- package/dist/modules/search/module.js +3 -1
- package/dist/runtime/kernel-types.d.ts +7 -0
- package/dist/runtime/kernel-types.d.ts.map +1 -1
- package/dist/runtime/kernel.d.ts +1 -0
- package/dist/runtime/kernel.d.ts.map +1 -1
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +33 -1
- package/package.json +3 -3
- package/src/auth/actor.ts +110 -0
- package/src/auth/middleware.ts +11 -99
- package/src/auth/setup.ts +2 -1
- package/src/config/types.ts +4 -2
- package/src/index.ts +6 -1
- package/src/kernel/database/plugin-types.ts +11 -0
- package/src/kernel/database/scoped-db.ts +12 -0
- package/src/kernel/plugin/manifest.ts +57 -12
- package/src/modules/search/adapter.ts +3 -0
- package/src/modules/search/module.ts +4 -1
- package/src/runtime/kernel-types.ts +9 -0
- package/src/runtime/kernel.ts +1 -0
- package/src/runtime/server.ts +39 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommerceConfig, PluginPermission } from "../config/types.js";
|
|
2
2
|
import type { HookRegistry } from "../kernel/hooks/registry.js";
|
|
3
3
|
import type { DatabaseAdapter } from "../kernel/database/adapter.js";
|
|
4
|
+
import type { PluginDb } from "../kernel/database/plugin-types.js";
|
|
4
5
|
import { CatalogServiceImpl } from "../modules/catalog/service.js";
|
|
5
6
|
import { InventoryService } from "../modules/inventory/service.js";
|
|
6
7
|
import { MediaService } from "../modules/media/service.js";
|
|
@@ -63,6 +64,12 @@ export interface Kernel {
|
|
|
63
64
|
pluginPermissions: PluginPermission[];
|
|
64
65
|
logger: ReturnType<typeof createLogger>;
|
|
65
66
|
}
|
|
67
|
+
export interface ConfigRouteDatabase extends DatabaseAdapter {
|
|
68
|
+
readonly scoped: PluginDb;
|
|
69
|
+
}
|
|
70
|
+
export type ConfigRouteKernel = Omit<Kernel, "database"> & {
|
|
71
|
+
database: ConfigRouteDatabase;
|
|
72
|
+
};
|
|
66
73
|
export declare const KERNEL_REQUIRED_SERVICE_KEYS: ["catalog", "inventory", "media", "cart", "orders", "payments", "fulfillment", "customers", "webhooks", "analytics", "pricing", "promotions", "tax", "shipping", "search", "settings", "documents", "audit", "compensationFailures"];
|
|
67
74
|
export declare function assertKernelServicesReady(services: Partial<Kernel["services"]>): asserts services is Kernel["services"];
|
|
68
75
|
export declare function assertSortedBefore(topo: readonly string[], a: string, b: string): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kernel-types.d.ts","sourceRoot":"","sources":["../../src/runtime/kernel-types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"kernel-types.d.ts","sourceRoot":"","sources":["../../src/runtime/kernel-types.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime/kernel.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommerceConfig } from "../config/types.js";
|
|
2
2
|
import { type Kernel, type WebhookDeliveryPayload } from "./kernel-types.js";
|
|
3
3
|
export type { Kernel, WebhookDeliveryPayload };
|
|
4
|
+
export type { ConfigRouteKernel, ConfigRouteDatabase } from "./kernel-types.js";
|
|
4
5
|
export { KERNEL_ALL_MODULES, kernelModuleInstantiationOrder, } from "./kernel-modules.js";
|
|
5
6
|
export declare function createKernel(config: CommerceConfig): Kernel;
|
|
6
7
|
//# sourceMappingURL=kernel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kernel.d.ts","sourceRoot":"","sources":["../../src/runtime/kernel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAqBzD,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,sBAAsB,EAC5B,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;AAC/C,OAAO,EACL,kBAAkB,EAClB,8BAA8B,GAC/B,MAAM,qBAAqB,CAAC;AAE7B,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA0I3D"}
|
|
1
|
+
{"version":3,"file":"kernel.d.ts","sourceRoot":"","sources":["../../src/runtime/kernel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAqBzD,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,sBAAsB,EAC5B,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;AAC/C,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAChF,OAAO,EACL,kBAAkB,EAClB,8BAA8B,GAC/B,MAAM,qBAAqB,CAAC;AAE7B,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA0I3D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/runtime/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAQhD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/runtime/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAQhD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAUzD,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAStE,KAAK,SAAS,GAAG;IACf,SAAS,EAAE;QACT,IAAI,EAAE,YAAY,CAAC;QACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAcvC;AAeD;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,cAAc;;;;;sBAkXxB,MAAM,UAAU,MAAM;GA8GtD"}
|
package/dist/runtime/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { rateLimiter } from "hono-rate-limiter";
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { createClientIpResolver } from "./client-ip.js";
|
|
9
9
|
import { authMiddleware } from "../auth/middleware.js";
|
|
10
|
+
import { resolveOrgIdForCommerce } from "../auth/org.js";
|
|
10
11
|
import { organizationGuard } from "../auth/organization-guard.js";
|
|
11
12
|
import { CommerceCsrfError } from "../kernel/errors.js";
|
|
12
13
|
import { createRestRoutes } from "../interfaces/rest/index.js";
|
|
@@ -16,6 +17,9 @@ import { rewriteCommerceAliasRequest } from "./url-alias-rewrite.js";
|
|
|
16
17
|
import { mapErrorToResponse } from "../kernel/error-mapper.js";
|
|
17
18
|
import { assertRouteCoverage } from "../interfaces/rest/route-coverage.js";
|
|
18
19
|
import { markRoutePermissionGuard } from "../interfaces/rest/utils.js";
|
|
20
|
+
import { isStrictOrgResolution } from "../auth/strict-org-resolution.js";
|
|
21
|
+
import { runWithPluginDatabaseScope } from "../kernel/database/plugin-db-context.js";
|
|
22
|
+
import { buildConfigRoutesKernel } from "../kernel/plugin/manifest.js";
|
|
19
23
|
/**
|
|
20
24
|
* Returns true if running in a Node-like runtime that supports
|
|
21
25
|
* `process.on()` + `process.exit()`. Edge runtimes (Cloudflare Workers,
|
|
@@ -212,10 +216,16 @@ export async function createServer(config) {
|
|
|
212
216
|
const signInEmailRateKeyCache = new WeakMap();
|
|
213
217
|
const getClientIp = createClientIpResolver(config);
|
|
214
218
|
const keyGenerator = getClientIp;
|
|
219
|
+
app.use("/api/auth/get-session", rateLimiter({
|
|
220
|
+
windowMs: 60 * 1000,
|
|
221
|
+
limit: config.rateLimits?.session ?? 120,
|
|
222
|
+
keyGenerator,
|
|
223
|
+
}));
|
|
215
224
|
app.use("/api/auth/*", rateLimiter({
|
|
216
225
|
windowMs: 60 * 1000,
|
|
217
226
|
limit: config.rateLimits?.auth ?? 10,
|
|
218
227
|
keyGenerator,
|
|
228
|
+
skip: (c) => c.req.path === "/api/auth/get-session",
|
|
219
229
|
}));
|
|
220
230
|
app.use("/api/auth/sign-in/email", async (c, next) => {
|
|
221
231
|
if (c.req.method !== "POST") {
|
|
@@ -280,6 +290,17 @@ export async function createServer(config) {
|
|
|
280
290
|
return auth.handler(c.req.raw);
|
|
281
291
|
});
|
|
282
292
|
app.use("*", authMiddleware(auth, config));
|
|
293
|
+
app.use("*", async (c, next) => {
|
|
294
|
+
let organizationId;
|
|
295
|
+
try {
|
|
296
|
+
organizationId = resolveOrgIdForCommerce(c.get("actor"), config);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
await next();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
await runWithPluginDatabaseScope(organizationId, next);
|
|
303
|
+
});
|
|
283
304
|
app.use("*", async (c, next) => {
|
|
284
305
|
c.set("auth", auth);
|
|
285
306
|
await next();
|
|
@@ -296,7 +317,7 @@ export async function createServer(config) {
|
|
|
296
317
|
// body-limit stack apply (all are scoped to /api/*).
|
|
297
318
|
app.route("/api/me", createCustomerPortalRoutes(kernel));
|
|
298
319
|
if (config.routes) {
|
|
299
|
-
config.routes(app, kernel, auth);
|
|
320
|
+
config.routes(app, buildConfigRoutesKernel(kernel), auth);
|
|
300
321
|
}
|
|
301
322
|
// OpenAPI spec — disabled in production unless explicitly enabled
|
|
302
323
|
const exposeSpec = config.exposeOpenApiSpec ?? !isProd;
|
|
@@ -420,6 +441,17 @@ export async function createServer(config) {
|
|
|
420
441
|
"or set config.jobs.autorun.enabled = true for in-process polling.");
|
|
421
442
|
}
|
|
422
443
|
}
|
|
444
|
+
const defaultOrganizationId = config.auth?.defaultOrganizationId;
|
|
445
|
+
const hasDefaultOrganization = typeof defaultOrganizationId === "string" && defaultOrganizationId.trim().length > 0;
|
|
446
|
+
if (isStrictOrgResolution(config) &&
|
|
447
|
+
!hasDefaultOrganization &&
|
|
448
|
+
!config.auth?.storeResolver) {
|
|
449
|
+
throw new Error("Cannot resolve an organization for a request with no actor. Set " +
|
|
450
|
+
"auth.defaultOrganizationId for a single-tenant deployment, or " +
|
|
451
|
+
"auth.storeResolver for a multi-tenant one. To keep the previous " +
|
|
452
|
+
"permissive behaviour during a migration, set " +
|
|
453
|
+
"auth.strictOrgResolution: false (or STRICT_ORG_RESOLUTION=false).");
|
|
454
|
+
}
|
|
423
455
|
assertRouteCoverage(app);
|
|
424
456
|
const dispatchFetch = app.fetch.bind(app);
|
|
425
457
|
app.fetch = ((input, env, executionCtx) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@porulle/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"eslint": "^9.39.1",
|
|
57
57
|
"typescript": "5.9.2",
|
|
58
58
|
"vitest": "^3.2.4",
|
|
59
|
-
"@porulle/
|
|
60
|
-
"@porulle/
|
|
59
|
+
"@porulle/typescript-config": "0.1.0",
|
|
60
|
+
"@porulle/eslint-config": "0.1.0"
|
|
61
61
|
},
|
|
62
62
|
"publishConfig": {
|
|
63
63
|
"access": "public"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { AuthSessionLike, CommerceConfig } from "../config/types.js";
|
|
2
|
+
import type { Actor } from "./types.js";
|
|
3
|
+
import type { AuthInstance } from "./setup.js";
|
|
4
|
+
import { DEFAULT_ORG_ID } from "./org.js";
|
|
5
|
+
|
|
6
|
+
export const AUTH_COOKIE_PREFIX = "uc";
|
|
7
|
+
export const SESSION_COOKIE_NAME = `${AUTH_COOKIE_PREFIX}.session_token`;
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_CUSTOMER_PERMISSIONS = [
|
|
10
|
+
"catalog:read",
|
|
11
|
+
"cart:create",
|
|
12
|
+
"cart:read",
|
|
13
|
+
"cart:update",
|
|
14
|
+
"orders:create",
|
|
15
|
+
"orders:read:own",
|
|
16
|
+
"customers:read:self",
|
|
17
|
+
"customers:update:self",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
export function getCustomerPermissions(config: CommerceConfig): string[] {
|
|
21
|
+
return config.auth?.customerPermissions ?? [...DEFAULT_CUSTOMER_PERMISSIONS];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolvePermissions(
|
|
25
|
+
session: AuthSessionLike,
|
|
26
|
+
config: CommerceConfig,
|
|
27
|
+
): string[] {
|
|
28
|
+
const role = session.session.activeOrganizationRole;
|
|
29
|
+
if (!role) return getCustomerPermissions(config);
|
|
30
|
+
const roleConfig = config.auth?.roles?.[role];
|
|
31
|
+
return roleConfig ? roleConfig.permissions : [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Resolve a better-auth session and its porulle organization permissions. */
|
|
35
|
+
export async function resolveActor(
|
|
36
|
+
headers: Headers,
|
|
37
|
+
auth: AuthInstance,
|
|
38
|
+
config: CommerceConfig,
|
|
39
|
+
request: Request = new Request("http://localhost", { headers }),
|
|
40
|
+
): Promise<Actor | null> {
|
|
41
|
+
let session: AuthSessionLike | null;
|
|
42
|
+
try {
|
|
43
|
+
session = (await auth.api.getSession({
|
|
44
|
+
headers,
|
|
45
|
+
})) as AuthSessionLike | null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!session) return null;
|
|
51
|
+
|
|
52
|
+
const defaultOrgId = config.auth?.defaultOrganizationId ?? DEFAULT_ORG_ID;
|
|
53
|
+
let role = session.session.activeOrganizationRole as string | undefined;
|
|
54
|
+
let orgId = session.session.activeOrganizationId as string | null;
|
|
55
|
+
|
|
56
|
+
if (!role && auth.api.getFullOrganization) {
|
|
57
|
+
try {
|
|
58
|
+
const org = await auth.api.getFullOrganization({
|
|
59
|
+
query: { organizationId: orgId ?? defaultOrgId },
|
|
60
|
+
headers,
|
|
61
|
+
});
|
|
62
|
+
if (org?.members) {
|
|
63
|
+
const membership = org.members.find(
|
|
64
|
+
(m) => m.userId === session.user.id,
|
|
65
|
+
);
|
|
66
|
+
if (membership) {
|
|
67
|
+
role = membership.role;
|
|
68
|
+
orgId = orgId ?? defaultOrgId;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// fall through — treat as customer
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!role && orgId && auth.api.getActiveMemberRole) {
|
|
77
|
+
try {
|
|
78
|
+
const roleResult = await auth.api.getActiveMemberRole({ headers });
|
|
79
|
+
role = (roleResult as Record<string, unknown>)?.role as
|
|
80
|
+
| string
|
|
81
|
+
| undefined;
|
|
82
|
+
} catch {
|
|
83
|
+
// fall through — treat as customer
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!orgId && config.auth?.storeResolver) {
|
|
88
|
+
try {
|
|
89
|
+
const resolved = await config.auth.storeResolver(request);
|
|
90
|
+
if (resolved) orgId = resolved;
|
|
91
|
+
} catch {
|
|
92
|
+
// fall through — use defaultOrgId
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const enrichedSession = {
|
|
97
|
+
...session,
|
|
98
|
+
session: { ...session.session, activeOrganizationRole: role ?? null },
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
type: "user",
|
|
102
|
+
userId: session.user.id,
|
|
103
|
+
email: session.user.email ?? null,
|
|
104
|
+
name: session.user.name ?? "User",
|
|
105
|
+
vendorId: session.user.vendorId ?? null,
|
|
106
|
+
organizationId: orgId ?? defaultOrgId,
|
|
107
|
+
role: role ?? "customer",
|
|
108
|
+
permissions: resolvePermissions(enrichedSession, config),
|
|
109
|
+
} satisfies Actor;
|
|
110
|
+
}
|
package/src/auth/middleware.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { MiddlewareHandler } from "hono";
|
|
2
|
-
import type {
|
|
2
|
+
import type { CommerceConfig } from "../config/types.js";
|
|
3
3
|
import type { Actor } from "./types.js";
|
|
4
4
|
import type { AuthInstance } from "./setup.js";
|
|
5
|
+
import { getCustomerPermissions, resolveActor } from "./actor.js";
|
|
5
6
|
import { DEFAULT_ORG_ID } from "./org.js";
|
|
6
7
|
import { isStrictOrgResolution } from "./strict-org-resolution.js";
|
|
7
8
|
|
|
@@ -12,37 +13,11 @@ function emptyToNull(value: string | null | undefined): string | null {
|
|
|
12
13
|
// Exported so the storefront contract can be pinned by a test: an anonymous
|
|
13
14
|
// visitor resolved by `storeResolver` gets these permissions, so dropping
|
|
14
15
|
// `catalog:read` here would 401 every public storefront read.
|
|
15
|
-
|
|
16
|
-
export const DEFAULT_CUSTOMER_PERMISSIONS = [
|
|
17
|
-
"catalog:read",
|
|
18
|
-
"cart:create",
|
|
19
|
-
"cart:read",
|
|
20
|
-
"cart:update",
|
|
21
|
-
"orders:create",
|
|
22
|
-
"orders:read:own",
|
|
23
|
-
"customers:read:self",
|
|
24
|
-
"customers:update:self",
|
|
25
|
-
] as const;
|
|
26
|
-
|
|
27
|
-
function getCustomerPermissions(config: CommerceConfig): string[] {
|
|
28
|
-
return config.auth?.customerPermissions ?? [...DEFAULT_CUSTOMER_PERMISSIONS];
|
|
29
|
-
}
|
|
16
|
+
export { DEFAULT_CUSTOMER_PERMISSIONS } from "./actor.js";
|
|
30
17
|
|
|
31
18
|
const LEGACY_STORE_RESOLVER_WARN_COOLDOWN_MS = 60_000;
|
|
32
19
|
let lastLegacyStoreResolverWarnAt = 0;
|
|
33
20
|
|
|
34
|
-
function resolvePermissions(
|
|
35
|
-
session: AuthSessionLike,
|
|
36
|
-
config: CommerceConfig,
|
|
37
|
-
): string[] {
|
|
38
|
-
const role = session.session.activeOrganizationRole;
|
|
39
|
-
if (!role) {
|
|
40
|
-
return getCustomerPermissions(config);
|
|
41
|
-
}
|
|
42
|
-
const roleConfig = config.auth?.roles?.[role];
|
|
43
|
-
return roleConfig ? roleConfig.permissions : [];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
21
|
export function authMiddleware(
|
|
47
22
|
auth: AuthInstance,
|
|
48
23
|
config: CommerceConfig,
|
|
@@ -68,77 +43,14 @@ export function authMiddleware(
|
|
|
68
43
|
}
|
|
69
44
|
}
|
|
70
45
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
let role = session.session.activeOrganizationRole as string | undefined;
|
|
80
|
-
let orgId = session.session.activeOrganizationId as string | null;
|
|
81
|
-
|
|
82
|
-
// If no active org, try to resolve the user's membership in org_default.
|
|
83
|
-
// This handles the common case where the user is a member but hasn't
|
|
84
|
-
// called organization/set-active (single-store apps, seed scripts, tests).
|
|
85
|
-
if (!role && auth.api.getFullOrganization) {
|
|
86
|
-
try {
|
|
87
|
-
const org = await auth.api.getFullOrganization({
|
|
88
|
-
query: { organizationId: orgId ?? defaultOrgId },
|
|
89
|
-
headers: c.req.raw.headers,
|
|
90
|
-
});
|
|
91
|
-
if (org?.members) {
|
|
92
|
-
const membership = org.members.find(
|
|
93
|
-
(m) => m.userId === session.user.id,
|
|
94
|
-
);
|
|
95
|
-
if (membership) {
|
|
96
|
-
role = membership.role;
|
|
97
|
-
orgId = orgId ?? defaultOrgId;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
} catch {
|
|
101
|
-
// fall through — treat as customer
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Also try getActiveMemberRole if active org is set
|
|
106
|
-
if (!role && orgId && auth.api.getActiveMemberRole) {
|
|
107
|
-
try {
|
|
108
|
-
const roleResult = await auth.api.getActiveMemberRole({
|
|
109
|
-
headers: c.req.raw.headers,
|
|
110
|
-
});
|
|
111
|
-
role = (roleResult as Record<string, unknown>)?.role as string | undefined;
|
|
112
|
-
} catch {
|
|
113
|
-
// fall through — treat as customer
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// For customers without org membership, resolve the store from the request.
|
|
118
|
-
// This enables multi-store SaaS where each storefront is a different org.
|
|
119
|
-
if (!orgId && config.auth?.storeResolver) {
|
|
120
|
-
try {
|
|
121
|
-
const resolved = await config.auth.storeResolver(c.req.raw);
|
|
122
|
-
if (resolved) orgId = resolved;
|
|
123
|
-
} catch {
|
|
124
|
-
// fall through — use defaultOrgId
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const enrichedSession = {
|
|
129
|
-
...session,
|
|
130
|
-
session: { ...session.session, activeOrganizationRole: role ?? null },
|
|
131
|
-
};
|
|
132
|
-
c.set("actor", {
|
|
133
|
-
type: "user",
|
|
134
|
-
userId: session.user.id,
|
|
135
|
-
email: session.user.email ?? null,
|
|
136
|
-
name: session.user.name ?? "User",
|
|
137
|
-
vendorId: session.user.vendorId ?? null,
|
|
138
|
-
organizationId: orgId ?? defaultOrgId,
|
|
139
|
-
role: role ?? "customer",
|
|
140
|
-
permissions: resolvePermissions(enrichedSession, config),
|
|
141
|
-
} satisfies Actor);
|
|
46
|
+
const actor = await resolveActor(
|
|
47
|
+
c.req.raw.headers,
|
|
48
|
+
auth,
|
|
49
|
+
config,
|
|
50
|
+
c.req.raw,
|
|
51
|
+
);
|
|
52
|
+
if (actor) {
|
|
53
|
+
c.set("actor", actor);
|
|
142
54
|
await next();
|
|
143
55
|
return;
|
|
144
56
|
}
|
package/src/auth/setup.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { organization, twoFactor, phoneNumber, jwt, bearer } from "better-auth/p
|
|
|
6
6
|
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
|
+
import { AUTH_COOKIE_PREFIX } from "./actor.js";
|
|
9
10
|
|
|
10
11
|
type BetterAuthDbProvider = "pg";
|
|
11
12
|
|
|
@@ -178,7 +179,7 @@ export function createAuth(
|
|
|
178
179
|
},
|
|
179
180
|
},
|
|
180
181
|
advanced: {
|
|
181
|
-
cookiePrefix:
|
|
182
|
+
cookiePrefix: AUTH_COOKIE_PREFIX,
|
|
182
183
|
useSecureCookies: process.env.NODE_ENV === "production",
|
|
183
184
|
// VAPT r2 (pi) finding: SameSite was never set anywhere. Without it,
|
|
184
185
|
// browsers default to Lax for new cookies, but the explicit setting
|
package/src/config/types.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Actor } from "../auth/types.js";
|
|
|
3
3
|
import type { BeforeHook, AfterHook } from "../kernel/hooks/types.js";
|
|
4
4
|
import type { PaymentAdapter } from "../modules/payments/adapter.js";
|
|
5
5
|
import type { StorageAdapter } from "../modules/media/adapter.js";
|
|
6
|
-
import type {
|
|
6
|
+
import type { ConfigRouteKernel } from "../runtime/kernel.js";
|
|
7
7
|
import type { AuthInstance } from "../auth/setup.js";
|
|
8
8
|
import type { DatabaseAdapter } from "../kernel/database/adapter.js";
|
|
9
9
|
import type { TaxAdapter } from "../modules/tax/adapter.js";
|
|
@@ -439,7 +439,7 @@ export interface CommerceConfig {
|
|
|
439
439
|
* third argument so integrations can mint/revoke API keys or call
|
|
440
440
|
* `auth.api.*` without module-global holder shims.
|
|
441
441
|
*/
|
|
442
|
-
routes?: (app: Hono<any>, kernel:
|
|
442
|
+
routes?: (app: Hono<any>, kernel: ConfigRouteKernel, auth?: AuthInstance) => void;
|
|
443
443
|
/** Log level for structured logging. Default: "info". */
|
|
444
444
|
logLevel?: "fatal" | "error" | "warn" | "info" | "debug" | "trace";
|
|
445
445
|
/**
|
|
@@ -453,6 +453,8 @@ export interface CommerceConfig {
|
|
|
453
453
|
api?: number;
|
|
454
454
|
/** Requests per minute for auth endpoints. Default: 10. */
|
|
455
455
|
auth?: number;
|
|
456
|
+
/** Requests per minute for session reads. Default: 120. */
|
|
457
|
+
session?: number;
|
|
456
458
|
/** Requests per minute for checkout. Default: 5. */
|
|
457
459
|
checkout?: number;
|
|
458
460
|
/** Attempts per 15 minutes for sign-in per email. Default: 10. */
|
package/src/index.ts
CHANGED
|
@@ -26,12 +26,17 @@ export { createLogger } from "./runtime/logger.js";
|
|
|
26
26
|
export type { Logger as PinoLogger } from "./runtime/logger.js";
|
|
27
27
|
export { setupGracefulShutdown } from "./runtime/shutdown.js";
|
|
28
28
|
export { createKernel } from "./runtime/kernel.js";
|
|
29
|
-
export type { Kernel } from "./runtime/kernel.js";
|
|
29
|
+
export type { Kernel, ConfigRouteKernel, ConfigRouteDatabase } from "./runtime/kernel.js";
|
|
30
30
|
// Test utilities moved to @porulle/core/testing sub-path export.
|
|
31
31
|
// Import from "@porulle/core/testing" instead of "@porulle/core".
|
|
32
32
|
// This avoids pulling drizzle-kit/tsx/esbuild into Turbopack's bundle graph.
|
|
33
33
|
|
|
34
34
|
export type { Actor } from "./auth/types.js";
|
|
35
|
+
export {
|
|
36
|
+
AUTH_COOKIE_PREFIX,
|
|
37
|
+
resolveActor,
|
|
38
|
+
SESSION_COOKIE_NAME,
|
|
39
|
+
} from "./auth/actor.js";
|
|
35
40
|
export { resolveOrgId, resolveOrgIdForCommerce, ensureDefaultOrg, DEFAULT_ORG_ID } from "./auth/org.js";
|
|
36
41
|
export { createSystemActor } from "./auth/system-actor.js";
|
|
37
42
|
export { OrganizationService } from "./modules/organization/service.js";
|
|
@@ -13,6 +13,17 @@ import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
|
|
|
13
13
|
/**
|
|
14
14
|
* Database instance type for plugin services.
|
|
15
15
|
* This is the Drizzle PgDatabase with an opaque schema record.
|
|
16
|
+
*
|
|
17
|
+
* Tenant scoping depends on which handle you were given, not on this type.
|
|
18
|
+
* `PluginContext.database.db` is wrapped so that `insert`, `select`, `update`
|
|
19
|
+
* and `delete` on any table with an `organizationId` column are constrained to
|
|
20
|
+
* the request's organization. `PluginContext.database.unscoped` and the kernel
|
|
21
|
+
* handle passed to `config.routes` are not wrapped at all.
|
|
22
|
+
*
|
|
23
|
+
* `execute()` is never scoped, on any handle — see the scope-boundary note in
|
|
24
|
+
* `scoped-db.ts`. When you drop to raw SQL, filter by organization yourself and
|
|
25
|
+
* interpolate values as bind parameters (`sql`... ${value}`) rather than
|
|
26
|
+
* through `sql.raw`.
|
|
16
27
|
*/
|
|
17
28
|
export type PluginDb = PgDatabase<PgQueryResultHKT, Record<string, unknown>>;
|
|
18
29
|
|
|
@@ -9,6 +9,18 @@
|
|
|
9
9
|
* Plugin route handlers receive this scoped db via PluginContext.database.db
|
|
10
10
|
* (organization resolved per operation from AsyncLocalStorage when used from
|
|
11
11
|
* plugin routes) or a fixed string / getter from router() / tests.
|
|
12
|
+
*
|
|
13
|
+
* SCOPE BOUNDARY — what this does NOT cover:
|
|
14
|
+
*
|
|
15
|
+
* Only `insert`, `select`, `update` and `delete` are wrapped. `execute()` is
|
|
16
|
+
* deliberately outside the boundary: injecting a tenant predicate into
|
|
17
|
+
* arbitrary SQL would mean parsing it, and a parser that is wrong once is worse
|
|
18
|
+
* than no guarantee at all. So `db.execute(sql`...`)` on a scoped handle is
|
|
19
|
+
* exactly as unscoped as one on the raw handle — which is the surprising part,
|
|
20
|
+
* because the caller reached for the safe object and still has to filter by
|
|
21
|
+
* organization themselves.
|
|
22
|
+
*
|
|
23
|
+
* Adding a fifth wrapped method is fine. Adding a raw-SQL rewriter is not.
|
|
12
24
|
*/
|
|
13
25
|
|
|
14
26
|
import { and, eq, getTableColumns } from "drizzle-orm";
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
getPluginDatabaseScopeOrganizationId,
|
|
10
10
|
runWithPluginDatabaseScope,
|
|
11
11
|
} from "../database/plugin-db-context.js";
|
|
12
|
-
import type { Kernel } from "../../runtime/kernel.js";
|
|
12
|
+
import type { ConfigRouteKernel, Kernel } from "../../runtime/kernel.js";
|
|
13
13
|
import type {
|
|
14
14
|
CommerceConfig,
|
|
15
15
|
CommercePlugin,
|
|
@@ -73,8 +73,12 @@ export interface PluginContext {
|
|
|
73
73
|
|
|
74
74
|
const UNSCOPED_WARN_COOLDOWN_MS = 60_000;
|
|
75
75
|
const UNSCOPED_WARN_EVERY_N = 100;
|
|
76
|
+
const configRouteRawDatabases = new WeakMap<object, DatabaseAdapter>();
|
|
76
77
|
|
|
77
|
-
function createRateLimitedUnscopedAccessWarning(
|
|
78
|
+
export function createRateLimitedUnscopedAccessWarning(
|
|
79
|
+
logger: Pick<PluginLogger, "warn">,
|
|
80
|
+
message: string,
|
|
81
|
+
): () => void {
|
|
78
82
|
let lastWarnAt = 0;
|
|
79
83
|
let accessCount = 0;
|
|
80
84
|
return () => {
|
|
@@ -84,24 +88,64 @@ function createRateLimitedUnscopedAccessWarning(logger: PluginLogger): () => voi
|
|
|
84
88
|
return;
|
|
85
89
|
}
|
|
86
90
|
lastWarnAt = now;
|
|
87
|
-
logger.warn(
|
|
88
|
-
"[plugin:database] PluginContext.database.unscoped is deprecated (MT-4 escape hatch); prefer ctx.database.db for tenant-scoped access.",
|
|
89
|
-
);
|
|
91
|
+
logger.warn(message);
|
|
90
92
|
};
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
function getPluginDatabaseOrganizationId(commerceConfig: CommerceConfig): string {
|
|
96
|
+
return (
|
|
97
|
+
getPluginDatabaseScopeOrganizationId() ??
|
|
98
|
+
resolveOrgIdForCommerce(null, commerceConfig)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getRawDatabaseForConfigRoutes(database: DatabaseAdapter): DatabaseAdapter {
|
|
103
|
+
if (database && typeof database === "object") {
|
|
104
|
+
return configRouteRawDatabases.get(database) ?? database;
|
|
105
|
+
}
|
|
106
|
+
return database;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function buildConfigRoutesKernel(kernel: Kernel): ConfigRouteKernel {
|
|
110
|
+
const rawDatabase = getRawDatabaseForConfigRoutes(kernel.database);
|
|
111
|
+
const rawDb = rawDatabase.db as PluginDb;
|
|
112
|
+
const scopedDb = createScopedDb(
|
|
113
|
+
rawDb,
|
|
114
|
+
() => getPluginDatabaseOrganizationId(kernel.config),
|
|
115
|
+
);
|
|
116
|
+
const warnUnscoped = createRateLimitedUnscopedAccessWarning(
|
|
117
|
+
kernel.logger,
|
|
118
|
+
"[config:database] kernel.database.db is unscoped; prefer kernel.database.scoped for tenant-scoped access.",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const database: ConfigRouteKernel["database"] = {
|
|
122
|
+
...rawDatabase,
|
|
123
|
+
get db(): unknown {
|
|
124
|
+
warnUnscoped();
|
|
125
|
+
return rawDatabase.db;
|
|
126
|
+
},
|
|
127
|
+
get scoped(): PluginDb {
|
|
128
|
+
return scopedDb;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
configRouteRawDatabases.set(database, rawDatabase);
|
|
132
|
+
|
|
133
|
+
return { ...kernel, database };
|
|
134
|
+
}
|
|
135
|
+
|
|
93
136
|
function buildPluginContextDatabase(
|
|
94
137
|
rawDb: PluginDb,
|
|
95
138
|
transactionFn: PluginContext["database"]["transaction"],
|
|
96
139
|
commerceConfig: CommerceConfig,
|
|
97
140
|
logger: PluginLogger,
|
|
98
141
|
): PluginContext["database"] {
|
|
99
|
-
const orgGetter = () =>
|
|
100
|
-
getPluginDatabaseScopeOrganizationId() ??
|
|
101
|
-
resolveOrgIdForCommerce(null, commerceConfig);
|
|
142
|
+
const orgGetter = () => getPluginDatabaseOrganizationId(commerceConfig);
|
|
102
143
|
|
|
103
144
|
const scopedDb = createScopedDb(rawDb, orgGetter);
|
|
104
|
-
const warnUnscoped = createRateLimitedUnscopedAccessWarning(
|
|
145
|
+
const warnUnscoped = createRateLimitedUnscopedAccessWarning(
|
|
146
|
+
logger,
|
|
147
|
+
"[plugin:database] PluginContext.database.unscoped is deprecated (MT-4 escape hatch); prefer ctx.database.db for tenant-scoped access.",
|
|
148
|
+
);
|
|
105
149
|
|
|
106
150
|
return {
|
|
107
151
|
get db(): PluginDb {
|
|
@@ -261,7 +305,7 @@ export function defineCommercePlugin(
|
|
|
261
305
|
const pluginRoutes = manifest.routes;
|
|
262
306
|
result = {
|
|
263
307
|
...result,
|
|
264
|
-
routes: (app: Hono, kernel:
|
|
308
|
+
routes: (app: Hono, kernel: ConfigRouteKernel, auth?: unknown) => {
|
|
265
309
|
existingRoutes?.(app, kernel, auth as never);
|
|
266
310
|
const k = kernel as {
|
|
267
311
|
config: CommerceConfig;
|
|
@@ -269,14 +313,15 @@ export function defineCommercePlugin(
|
|
|
269
313
|
database: DatabaseAdapter;
|
|
270
314
|
logger: PluginLogger;
|
|
271
315
|
};
|
|
316
|
+
const rawDatabase = getRawDatabaseForConfigRoutes(k.database);
|
|
272
317
|
// Narrow DatabaseAdapter<unknown> → PluginContext.database once here.
|
|
273
318
|
// All plugin code receives typed PluginDb — no casts downstream.
|
|
274
319
|
const regs = pluginRoutes({
|
|
275
320
|
config: k.config,
|
|
276
321
|
services: k.services,
|
|
277
322
|
database: buildPluginContextDatabase(
|
|
278
|
-
|
|
279
|
-
|
|
323
|
+
rawDatabase.db as PluginDb,
|
|
324
|
+
rawDatabase.transaction as PluginContext["database"]["transaction"],
|
|
280
325
|
k.config,
|
|
281
326
|
k.logger,
|
|
282
327
|
),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Result } from "../../kernel/result.js";
|
|
2
|
+
import type { PluginDb } from "../../kernel/database/plugin-types.js";
|
|
2
3
|
|
|
3
4
|
export interface SearchDocument {
|
|
4
5
|
id: string;
|
|
@@ -58,6 +59,8 @@ export interface SearchQueryResult {
|
|
|
58
59
|
|
|
59
60
|
export interface SearchAdapter {
|
|
60
61
|
readonly providerId: string;
|
|
62
|
+
/** Called once when the search module wires the adapter to the live database. */
|
|
63
|
+
init?(deps: { db: PluginDb }): void;
|
|
61
64
|
index(documents: SearchDocument[]): Promise<Result<void>>;
|
|
62
65
|
remove(ids: string[]): Promise<Result<void>>;
|
|
63
66
|
search(params: SearchQueryParams): Promise<Result<SearchQueryResult>>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CommerceConfig } from "../../config/types.js";
|
|
2
|
+
import type { PluginDb } from "../../kernel/database/plugin-types.js";
|
|
2
3
|
import { defineModule } from "../../kernel/module/index.js";
|
|
3
4
|
import type { CatalogServiceImpl } from "../catalog/service.js";
|
|
4
5
|
import { SearchService } from "./service.js";
|
|
@@ -14,12 +15,14 @@ export const searchModule = defineModule<Record<string, never>, SearchService, S
|
|
|
14
15
|
schema: () => ({}),
|
|
15
16
|
service: (deps) => {
|
|
16
17
|
const config = deps.config as CommerceConfig;
|
|
18
|
+
const adapter = config.search?.adapter;
|
|
19
|
+
adapter?.init?.({ db: deps.db.db as PluginDb });
|
|
17
20
|
return new SearchService({
|
|
18
21
|
catalogRepository: deps.services.catalog.repository,
|
|
19
22
|
resolveEntityFieldDefinitions: deps.services.catalog.resolveEntityFieldDefinitions.bind(deps.services.catalog),
|
|
20
23
|
config,
|
|
21
24
|
...(config.entities ? { entities: config.entities } : {}),
|
|
22
|
-
...(
|
|
25
|
+
...(adapter ? { adapter } : {}),
|
|
23
26
|
...(config.search?.defaultFacets
|
|
24
27
|
? { defaultFacets: config.search.defaultFacets }
|
|
25
28
|
: {}),
|