@cosmicdrift/kumiko-bundled-features 0.146.1 → 0.146.3
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": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.146.
|
|
3
|
+
"version": "0.146.3",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -111,11 +111,11 @@
|
|
|
111
111
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
112
112
|
},
|
|
113
113
|
"dependencies": {
|
|
114
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.146.
|
|
115
|
-
"@cosmicdrift/kumiko-framework": "0.146.
|
|
116
|
-
"@cosmicdrift/kumiko-headless": "0.146.
|
|
117
|
-
"@cosmicdrift/kumiko-renderer": "0.146.
|
|
118
|
-
"@cosmicdrift/kumiko-renderer-web": "0.146.
|
|
114
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.146.3",
|
|
115
|
+
"@cosmicdrift/kumiko-framework": "0.146.3",
|
|
116
|
+
"@cosmicdrift/kumiko-headless": "0.146.3",
|
|
117
|
+
"@cosmicdrift/kumiko-renderer": "0.146.3",
|
|
118
|
+
"@cosmicdrift/kumiko-renderer-web": "0.146.3",
|
|
119
119
|
"@mollie/api-client": "^4.5.0",
|
|
120
120
|
"imapflow": "^1.3.3",
|
|
121
121
|
"mailparser": "^3.9.8",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
buildSessionRoles,
|
|
2
3
|
createSystemUser,
|
|
3
4
|
defineWriteHandler,
|
|
4
5
|
type SessionUser,
|
|
5
|
-
stripForbiddenMembershipRoles,
|
|
6
6
|
type TenantId,
|
|
7
7
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
8
8
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
@@ -169,11 +169,10 @@ export function createLoginHandler(opts: LoginHandlerOptions = {}) {
|
|
|
169
169
|
// membership. Dedupe via Set damit eine Rolle die in beiden Quellen
|
|
170
170
|
// steht nicht doppelt im Session-Roles landet.
|
|
171
171
|
const globalRoles = parseRoles(found.roles ?? null);
|
|
172
|
-
//
|
|
173
|
-
// SystemAdmin) — read-time backstop against a
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
);
|
|
172
|
+
// buildSessionRoles calls stripForbiddenMembershipRoles to strip reserved
|
|
173
|
+
// only (globalRoles keeps SystemAdmin) — read-time backstop against a
|
|
174
|
+
// rebuild-resurrected role.
|
|
175
|
+
const mergedRoles = buildSessionRoles(globalRoles, chosen.roles);
|
|
177
176
|
const baseSession: SessionUser = {
|
|
178
177
|
id: found.id,
|
|
179
178
|
tenantId: chosen.tenantId,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, defineWriteHandler,
|
|
1
|
+
import { access, defineWriteHandler, stepsPipeline } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
// Logout — JWT is stateless, so server-side we only return OK. A future
|
|
@@ -8,5 +8,7 @@ export const logoutWrite = defineWriteHandler({
|
|
|
8
8
|
name: "logout",
|
|
9
9
|
schema: z.object({}),
|
|
10
10
|
access: { roles: access.authenticated },
|
|
11
|
-
perform:
|
|
11
|
+
perform: stepsPipeline(({ r }) => [
|
|
12
|
+
r.step.return({ isSuccess: true, data: { kind: "logged-out" } }),
|
|
13
|
+
]),
|
|
12
14
|
});
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import {
|
|
3
|
+
import { buildSessionRoles } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
5
5
|
import { tenantMembershipsTable } from "../tenant";
|
|
6
6
|
import { userTable } from "../user";
|
|
7
7
|
|
|
8
8
|
// Live role resolution for a (userId, tenantId), mirroring login.write.ts:
|
|
9
9
|
// global roles (users.roles) ∪ tenant-membership roles (forbidden roles
|
|
10
|
-
// stripped
|
|
10
|
+
// stripped via stripForbiddenMembershipRoles, see engine/membership-roles).
|
|
11
|
+
// Resolved fresh on every PAT request — a snapshot baked at mint
|
|
11
12
|
// time would keep a since-revoked admin role for the token's whole (months-long)
|
|
12
13
|
// life. Returns null when the user has no membership in that tenant: removed
|
|
13
14
|
// from the tenant → the PAT stops authenticating there.
|
|
@@ -24,6 +25,5 @@ export async function resolvePatRoles(
|
|
|
24
25
|
if (!membership) return null;
|
|
25
26
|
const userRow = await fetchOne<{ roles: string | null }>(db, userTable, { id: userId });
|
|
26
27
|
const globalRoles = parseRoles(userRow?.roles ?? null);
|
|
27
|
-
|
|
28
|
-
return [...new Set([...globalRoles, ...membershipRoles])];
|
|
28
|
+
return buildSessionRoles(globalRoles, parseRoles(membership.roles));
|
|
29
29
|
}
|
|
@@ -129,6 +129,18 @@ describe("seo :: GET /sitemap.xml", () => {
|
|
|
129
129
|
expect(res.headers.get("etag")).toBeTruthy();
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
test("x-forwarded-proto: https → every merged URL uses https, not the raw http hop", async () => {
|
|
133
|
+
const res = await stack.app.request("http://a.example.com/sitemap.xml", {
|
|
134
|
+
headers: { "x-forwarded-proto": "https" },
|
|
135
|
+
});
|
|
136
|
+
expect(res.status).toBe(200);
|
|
137
|
+
const xml = await res.text();
|
|
138
|
+
expect(xml).toContain("<loc>https://a.example.com/</loc>");
|
|
139
|
+
expect(xml).toContain("<loc>https://a.example.com/legal/impressum</loc>");
|
|
140
|
+
expect(xml).toContain("<loc>https://a.example.com/p/about</loc>");
|
|
141
|
+
expect(xml).not.toContain("http://a.example.com");
|
|
142
|
+
});
|
|
143
|
+
|
|
132
144
|
test("host without a managed-pages tenant → callback + legal-pages entries only", async () => {
|
|
133
145
|
const res = await stack.app.request("http://unknown.example.com/sitemap.xml");
|
|
134
146
|
expect(res.status).toBe(200);
|
package/src/seo/feature.ts
CHANGED
|
@@ -147,12 +147,21 @@ async function gatherEntries(
|
|
|
147
147
|
return entries;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
// TLS-terminating reverse proxies (ingress-nginx et al.) forward the
|
|
151
|
+
// original request over plain HTTP internally, so `c.req.url`'s scheme
|
|
152
|
+
// reflects the proxy hop, not what the client actually used — trusting it
|
|
153
|
+
// downgraded every legal-pages/managed-pages URL in sitemap.xml/llms.txt to
|
|
154
|
+
// http:// in production. `x-forwarded-proto` carries the real scheme;
|
|
155
|
+
// fall back to the raw URL only when the header is absent (plain local dev).
|
|
150
156
|
function requestHost(c: { req: { header: (name: string) => string | undefined; url: string } }): {
|
|
151
157
|
origin: string;
|
|
152
158
|
host: string;
|
|
153
159
|
} {
|
|
154
160
|
const url = new URL(c.req.url);
|
|
155
|
-
|
|
161
|
+
const host = c.req.header("host") ?? url.host;
|
|
162
|
+
const forwardedProto = c.req.header("x-forwarded-proto")?.split(",")[0]?.trim();
|
|
163
|
+
const protocol = forwardedProto || url.protocol.replace(":", "");
|
|
164
|
+
return { origin: `${protocol}://${host}`, host };
|
|
156
165
|
}
|
|
157
166
|
|
|
158
167
|
// seo — site-discovery routes (sitemap.xml/llms.txt/robots.txt) plus the
|