From d4090b7d8d9bb3ea4cdb66abf9b87f9bcae5a97b Mon Sep 17 00:00:00 2001 From: Lectom Date: Fri, 15 May 2026 18:20:49 +0900 Subject: [PATCH] fix: align local Ory cookie domain rendering --- Makefile | 5 +- .../templates/ory/kratos/kratos.yml.template | 2 +- docker/ory/kratos/kratos.yml.template | 2 +- orgfront/package.json | 3 + orgfront/src/sdk/org-context-chart/index.ts | 491 ++++++++++++++++++ .../org-context-chart/orgContextChart.test.ts | 171 ++++++ orgfront/tsconfig.node.json | 2 +- orgfront/vite.org-context-chart.config.ts | 25 + scripts/render_ory_config.sh | 53 ++ ...orgfront_org_context_chart_package_test.sh | 22 + test/ory_v26_compose_policy_test.sh | 21 + 11 files changed, 793 insertions(+), 4 deletions(-) create mode 100644 orgfront/src/sdk/org-context-chart/index.ts create mode 100644 orgfront/src/sdk/org-context-chart/orgContextChart.test.ts create mode 100644 orgfront/vite.org-context-chart.config.ts create mode 100644 test/orgfront_org_context_chart_package_test.sh diff --git a/Makefile b/Makefile index 78bf47fd..34535890 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,7 @@ up: up-all up-all: ensure-networks render-ory-config @echo "Starting ALL stacks (infra + ory + app)..." docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) up --build -d + docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) restart kratos # --- 개별 스택 실행 --- up-infra: ensure-networks @@ -65,6 +66,7 @@ up-infra: ensure-networks up-ory: ensure-networks render-ory-config @echo "Starting Ory stack (kratos/hydra/keto/oathkeeper)..." docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) up -d + docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) restart kratos up-app: ensure-networks render-ory-config @echo "Starting App stack (backend/userfront/adminfront/devfront/orgfront)..." @@ -114,7 +116,8 @@ ensure-ory: ensure-networks render-ory-config echo "Starting missing Ory stack containers in daemon mode..."; \ docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) up -d; \ else \ - echo "Ory stack is already running."; \ + echo "Ory stack is already running. Restarting Kratos to apply rendered dev config..."; \ + docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) restart kratos; \ fi up-dev: ensure-infra ensure-ory diff --git a/deploy/templates/ory/kratos/kratos.yml.template b/deploy/templates/ory/kratos/kratos.yml.template index de5b981b..15ec5879 100644 --- a/deploy/templates/ory/kratos/kratos.yml.template +++ b/deploy/templates/ory/kratos/kratos.yml.template @@ -15,7 +15,7 @@ serve: session: cookie: - domain: hmac.kr + domain: ${KRATOS_SESSION_COOKIE_DOMAIN} same_site: Lax path: / diff --git a/docker/ory/kratos/kratos.yml.template b/docker/ory/kratos/kratos.yml.template index a82b210f..f0f3c6b7 100644 --- a/docker/ory/kratos/kratos.yml.template +++ b/docker/ory/kratos/kratos.yml.template @@ -19,7 +19,7 @@ serve: session: cookie: - domain: hmac.kr + domain: ${KRATOS_SESSION_COOKIE_DOMAIN} same_site: Lax path: / diff --git a/orgfront/package.json b/orgfront/package.json index c48e46c8..0742c6f7 100644 --- a/orgfront/package.json +++ b/orgfront/package.json @@ -9,6 +9,9 @@ "scripts": { "dev": "vite --host 127.0.0.1", "build": "tsc -b && vite build", + "build:org-context-chart": "npm run build:org-context-chart:full && npm run build:org-context-chart:min", + "build:org-context-chart:full": "vite build --config vite.org-context-chart.config.ts", + "build:org-context-chart:min": "ORG_CONTEXT_CHART_MINIFY=true vite build --config vite.org-context-chart.config.ts", "lint": "biome check .", "preview": "vite preview", "test": "playwright test", diff --git a/orgfront/src/sdk/org-context-chart/index.ts b/orgfront/src/sdk/org-context-chart/index.ts new file mode 100644 index 00000000..a4efe70f --- /dev/null +++ b/orgfront/src/sdk/org-context-chart/index.ts @@ -0,0 +1,491 @@ +export type OrgContextMember = { + id?: string; + email: string; + name: string; + phone?: string; + department?: string; + grade?: string; + position?: string; + jobTitle?: string; + isOwner?: boolean; + isLeader?: boolean; + isPrimary?: boolean; +}; + +export type OrgContextTenant = { + id: string; + type: string; + name: string; + slug: string; + parentId?: string | null; + status: string; + description: string; + domains: string[]; + memberCount: number; + visibility: string; + orgUnitType?: string; + createdAt: string; + updatedAt: string; + members: OrgContextMember[]; +}; + +export type OrgContextTreeNode = OrgContextTenant & { + children: OrgContextTreeNode[]; +}; + +export type OrgContextResponse = { + schemaVersion: "baron.org-context.v1"; + issuedAt: string; + scope: { + tenantId: string; + tenantSlug: string; + }; + tree: OrgContextTreeNode; + tenants: OrgContextTenant[]; +}; + +export type OrgChartNode = OrgContextTenant & { + children: OrgChartNode[]; + depth: number; + path: string[]; +}; + +export type OrgChartMember = OrgContextMember & { + tenantIds: string[]; +}; + +export type OrgChartModel = { + root: OrgChartNode; + nodes: OrgChartNode[]; + tenantsById: Map; + tenantsBySlug: Map; + membersByEmail: Map; + response: OrgContextResponse; +}; + +export type OrgContextClientOptions = { + baseUrl: string; + credentials?: { + keyId: string; + keySecret: string; + }; + fetch?: typeof fetch; + headers?: Record; +}; + +export type FetchOrgContextOptions = { + tenantSlug?: string; + includeUsers?: boolean; + includeUserIds?: boolean; +}; + +export type OrgPickerSelection = { + id: string; + name: string; + type: "tenant" | "user"; +}; + +export type OrgPickerOptions = { + mode?: "single" | "multiple"; + selectable?: "tenant" | "user" | "both"; + includeDescendants?: boolean; + onChange?: (selection: OrgPickerSelection[]) => void; +}; + +export type OrgPickerController = { + destroy: () => void; + getSelection: () => OrgPickerSelection[]; +}; + +const API_PATH = "/api/v1/integrations/org-context"; + +export function createOrgContextClient(options: OrgContextClientOptions) { + const fetcher = options.fetch ?? globalThis.fetch; + if (!fetcher) { + throw new Error("A fetch implementation is required."); + } + + return { + async fetchOrgContext( + query: FetchOrgContextOptions = {}, + ): Promise { + const url = new URL(API_PATH, normalizeBaseUrl(options.baseUrl)); + appendQuery(url, "tenantSlug", query.tenantSlug); + appendQuery(url, "includeUsers", query.includeUsers); + appendQuery(url, "includeUserIds", query.includeUserIds); + + const headers: Record = { + Accept: "application/json", + ...options.headers, + }; + if (options.credentials) { + headers["X-Baron-Key-ID"] = options.credentials.keyId; + headers["X-Baron-Key-Secret"] = options.credentials.keySecret; + } + + const response = await fetcher(url.toString(), { headers }); + if (!response.ok) { + throw new Error(`Org context request failed with ${response.status}`); + } + + const payload = (await response.json()) as OrgContextResponse; + if (payload.schemaVersion !== "baron.org-context.v1") { + throw new Error( + `Unsupported org context schema: ${payload.schemaVersion}`, + ); + } + return payload; + }, + }; +} + +export function buildOrgChartModel( + response: OrgContextResponse, +): OrgChartModel { + if (response.schemaVersion !== "baron.org-context.v1") { + throw new Error( + `Unsupported org context schema: ${response.schemaVersion}`, + ); + } + + const nodes: OrgChartNode[] = []; + const tenantsById = new Map(); + const tenantsBySlug = new Map(); + const membersByEmail = new Map(); + + const visit = ( + node: OrgContextTreeNode, + depth: number, + ancestorPath: string[], + ): OrgChartNode => { + const path = [...ancestorPath, node.id]; + const chartNode: OrgChartNode = { + ...node, + children: [], + depth, + path, + }; + nodes.push(chartNode); + tenantsById.set(chartNode.id, chartNode); + tenantsBySlug.set(chartNode.slug, chartNode); + + for (const member of chartNode.members) { + const key = member.email.toLowerCase(); + const existing = membersByEmail.get(key); + if (existing) { + existing.tenantIds.push(chartNode.id); + } else { + membersByEmail.set(key, { + ...member, + tenantIds: [chartNode.id], + }); + } + } + + chartNode.children = node.children.map((child) => + visit(child, depth + 1, path), + ); + return chartNode; + }; + + return { + root: visit(response.tree, 0, []), + nodes, + tenantsById, + tenantsBySlug, + membersByEmail, + response, + }; +} + +export function renderOrgChart( + container: HTMLElement, + model: OrgChartModel, +): { destroy: () => void } { + container.replaceChildren(); + container.classList.add("baron-org-chart"); + const root = document.createElement("div"); + root.className = "baron-org-chart__tree"; + root.append(renderChartNode(model.root)); + container.append(root); + return { + destroy() { + container.replaceChildren(); + container.classList.remove("baron-org-chart"); + }, + }; +} + +export function renderOrgPicker( + container: HTMLElement, + model: OrgChartModel, + options: OrgPickerOptions = {}, +): OrgPickerController { + const mode = options.mode ?? "single"; + const selectable = options.selectable ?? "tenant"; + const includeDescendants = options.includeDescendants ?? false; + const selected = new Map(); + + const emitChange = () => { + const selection = Array.from(selected.values()); + options.onChange?.(selection); + container.dispatchEvent( + new CustomEvent("baron-org-picker-change", { + bubbles: true, + detail: { selection }, + }), + ); + }; + + const toggleSelection = ( + selection: OrgPickerSelection, + checked: boolean, + descendants: OrgPickerSelection[], + ) => { + if (mode === "single") { + selected.clear(); + if (checked) { + selected.set(selectionKey(selection), selection); + } + emitChange(); + rerender(); + return; + } + + const targets = + includeDescendants && selection.type === "tenant" + ? [selection, ...descendants] + : [selection]; + for (const target of targets) { + if (checked) { + selected.set(selectionKey(target), target); + } else { + selected.delete(selectionKey(target)); + } + } + emitChange(); + rerender(); + }; + + const renderPickerNode = (node: OrgChartNode): HTMLElement => { + const item = document.createElement("li"); + item.className = "baron-org-picker__item"; + + const row = document.createElement("label"); + row.className = "baron-org-picker__row"; + row.style.paddingLeft = `${node.depth * 16}px`; + + const tenantSelection: OrgPickerSelection = { + id: node.id, + name: node.name, + type: "tenant", + }; + if (selectable === "tenant" || selectable === "both") { + row.append( + createPickerInput({ + mode, + selection: tenantSelection, + selected, + onToggle: (checked) => + toggleSelection( + tenantSelection, + checked, + collectDescendantSelections(node, selectable), + ), + }), + ); + } + row.append(createLabelText(node.name, node.type)); + item.append(row); + + if (selectable === "user" || selectable === "both") { + for (const member of node.members) { + item.append( + renderMemberPickerRow(member, node, mode, selected, (value) => + toggleSelection(value, !selected.has(selectionKey(value)), []), + ), + ); + } + } + + if (node.children.length > 0) { + const children = document.createElement("ul"); + children.className = "baron-org-picker__children"; + for (const child of node.children) { + children.append(renderPickerNode(child)); + } + item.append(children); + } + return item; + }; + + const rerender = () => { + container.replaceChildren(); + container.classList.add("baron-org-picker"); + const list = document.createElement("ul"); + list.className = "baron-org-picker__list"; + list.append(renderPickerNode(model.root)); + container.append(list); + }; + + rerender(); + + return { + destroy() { + container.replaceChildren(); + container.classList.remove("baron-org-picker"); + selected.clear(); + }, + getSelection() { + return Array.from(selected.values()); + }, + }; +} + +function appendQuery( + url: URL, + key: string, + value: string | boolean | undefined, +) { + if (value !== undefined) { + url.searchParams.set(key, String(value)); + } +} + +function normalizeBaseUrl(baseUrl: string) { + return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`; +} + +function renderChartNode(node: OrgChartNode): HTMLElement { + const item = document.createElement("section"); + item.className = "baron-org-chart__node"; + item.dataset.baronOrgNode = node.id; + + const title = document.createElement("h3"); + title.className = "baron-org-chart__title"; + title.textContent = node.name; + item.append(title); + + const meta = document.createElement("p"); + meta.className = "baron-org-chart__meta"; + meta.textContent = [node.type, node.orgUnitType, node.visibility] + .filter(Boolean) + .join(" · "); + item.append(meta); + + if (node.members.length > 0) { + const memberList = document.createElement("ul"); + memberList.className = "baron-org-chart__members"; + for (const member of node.members) { + const memberItem = document.createElement("li"); + memberItem.textContent = formatMember(member); + memberList.append(memberItem); + } + item.append(memberList); + } + + if (node.children.length > 0) { + const children = document.createElement("div"); + children.className = "baron-org-chart__children"; + for (const child of node.children) { + children.append(renderChartNode(child)); + } + item.append(children); + } + return item; +} + +function renderMemberPickerRow( + member: OrgContextMember, + node: OrgChartNode, + mode: "single" | "multiple", + selected: Map, + onSelect: (selection: OrgPickerSelection) => void, +) { + const selection: OrgPickerSelection = { + id: member.id || `${node.id}:${member.email}`, + name: member.name, + type: "user", + }; + const row = document.createElement("label"); + row.className = "baron-org-picker__row baron-org-picker__row--member"; + row.style.paddingLeft = `${node.depth * 16 + 24}px`; + row.append( + createPickerInput({ + mode, + selection, + selected, + onToggle: () => onSelect(selection), + }), + ); + row.append(createLabelText(member.name, member.email)); + return row; +} + +function createPickerInput({ + mode, + selection, + selected, + onToggle, +}: { + mode: "single" | "multiple"; + selection: OrgPickerSelection; + selected: Map; + onToggle: (checked: boolean) => void; +}) { + const input = document.createElement("input"); + input.type = mode === "single" ? "radio" : "checkbox"; + input.name = "baron-org-picker"; + input.value = selectionKey(selection); + input.checked = selected.has(selectionKey(selection)); + const handleToggle = () => { + onToggle(mode === "single" ? true : !selected.has(selectionKey(selection))); + }; + input.addEventListener("click", handleToggle); + return input; +} + +function createLabelText(primary: string, secondary?: string) { + const text = document.createElement("span"); + text.className = "baron-org-picker__label"; + text.textContent = secondary ? `${primary} (${secondary})` : primary; + return text; +} + +function collectDescendantSelections( + node: OrgChartNode, + selectable: "tenant" | "user" | "both", +): OrgPickerSelection[] { + const selections: OrgPickerSelection[] = []; + const visit = (child: OrgChartNode) => { + if (selectable === "tenant" || selectable === "both") { + selections.push({ id: child.id, name: child.name, type: "tenant" }); + } + if (selectable === "user" || selectable === "both") { + for (const member of child.members) { + selections.push({ + id: member.id || `${child.id}:${member.email}`, + name: member.name, + type: "user", + }); + } + } + for (const grandchild of child.children) { + visit(grandchild); + } + }; + for (const child of node.children) { + visit(child); + } + return selections; +} + +function selectionKey(selection: OrgPickerSelection) { + return `${selection.type}:${selection.id}`; +} + +function formatMember(member: OrgContextMember) { + return [member.name, member.position, member.jobTitle] + .filter(Boolean) + .join(" · "); +} diff --git a/orgfront/src/sdk/org-context-chart/orgContextChart.test.ts b/orgfront/src/sdk/org-context-chart/orgContextChart.test.ts new file mode 100644 index 00000000..9039ef70 --- /dev/null +++ b/orgfront/src/sdk/org-context-chart/orgContextChart.test.ts @@ -0,0 +1,171 @@ +import { describe, expect, it, vi } from "vitest"; +import { + type OrgContextResponse, + buildOrgChartModel, + createOrgContextClient, + renderOrgChart, + renderOrgPicker, +} from "./index"; + +const sampleOrgContext: OrgContextResponse = { + schemaVersion: "baron.org-context.v1", + issuedAt: "2026-05-15T00:00:00Z", + scope: { + tenantId: "root", + tenantSlug: "hanmac-family", + }, + tree: { + id: "root", + type: "COMPANY_GROUP", + name: "한맥가족", + slug: "hanmac-family", + status: "active", + description: "", + domains: [], + memberCount: 0, + visibility: "public", + createdAt: "2026-05-15T00:00:00Z", + updatedAt: "2026-05-15T00:00:00Z", + members: [], + children: [ + { + id: "company-baron", + type: "COMPANY", + name: "Baron", + slug: "baron", + parentId: "root", + status: "active", + description: "", + domains: ["baron.example"], + memberCount: 1, + visibility: "public", + createdAt: "2026-05-15T00:00:00Z", + updatedAt: "2026-05-15T00:00:00Z", + members: [ + { + email: "leader@example.com", + name: "Leader", + grade: "책임", + position: "팀장", + isLeader: true, + isOwner: true, + isPrimary: true, + }, + ], + children: [ + { + id: "team-platform", + type: "USER_GROUP", + name: "Platform", + slug: "platform", + parentId: "company-baron", + status: "active", + description: "", + domains: [], + memberCount: 1, + visibility: "internal", + orgUnitType: "팀", + createdAt: "2026-05-15T00:00:00Z", + updatedAt: "2026-05-15T00:00:00Z", + members: [ + { + email: "engineer@example.com", + name: "Engineer", + jobTitle: "Frontend Engineer", + isLeader: false, + isOwner: false, + isPrimary: false, + }, + ], + children: [], + }, + ], + }, + ], + }, + tenants: [], +}; + +describe("org-context chart SDK", () => { + it("builds chart and lookup models from org-context v1", () => { + const model = buildOrgChartModel(sampleOrgContext); + + expect(model.root.name).toBe("한맥가족"); + expect(model.nodes).toHaveLength(3); + expect(model.tenantsBySlug.get("platform")?.orgUnitType).toBe("팀"); + expect(model.membersByEmail.get("engineer@example.com")?.tenantIds).toEqual( + ["team-platform"], + ); + }); + + it("fetches org-context through authenticated API headers", async () => { + const fetcher = vi.fn(async () => { + return new Response(JSON.stringify(sampleOrgContext), { + headers: { "content-type": "application/json" }, + status: 200, + }); + }); + const client = createOrgContextClient({ + baseUrl: "https://sso.example.com", + credentials: { + keyId: "client-id", + keySecret: "client-secret", + }, + fetch: fetcher, + }); + + await client.fetchOrgContext({ + tenantSlug: "baron", + includeUsers: true, + includeUserIds: false, + }); + + const [url, init] = fetcher.mock.calls[0]; + expect(String(url)).toBe( + "https://sso.example.com/api/v1/integrations/org-context?tenantSlug=baron&includeUsers=true&includeUserIds=false", + ); + expect(init.headers).toMatchObject({ + "X-Baron-Key-ID": "client-id", + "X-Baron-Key-Secret": "client-secret", + }); + }); + + it("renders chart and picker DOM with selection events", () => { + const model = buildOrgChartModel(sampleOrgContext); + const chartContainer = document.createElement("div"); + const pickerContainer = document.createElement("div"); + const onChange = vi.fn(); + + renderOrgChart(chartContainer, model); + const picker = renderOrgPicker(pickerContainer, model, { + mode: "multiple", + selectable: "both", + onChange, + }); + + expect( + chartContainer.querySelectorAll("[data-baron-org-node]"), + ).toHaveLength(3); + const platformCheckbox = pickerContainer.querySelector( + 'input[value="tenant:team-platform"]', + ); + expect(platformCheckbox).not.toBeNull(); + + platformCheckbox?.click(); + + expect(picker.getSelection()).toEqual([ + { + id: "team-platform", + name: "Platform", + type: "tenant", + }, + ]); + expect(onChange).toHaveBeenCalledWith([ + { + id: "team-platform", + name: "Platform", + type: "tenant", + }, + ]); + }); +}); diff --git a/orgfront/tsconfig.node.json b/orgfront/tsconfig.node.json index 8a67f62f..c4cffd0b 100644 --- a/orgfront/tsconfig.node.json +++ b/orgfront/tsconfig.node.json @@ -22,5 +22,5 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["vite.config.ts"] + "include": ["vite.config.ts", "vite.org-context-chart.config.ts"] } diff --git a/orgfront/vite.org-context-chart.config.ts b/orgfront/vite.org-context-chart.config.ts new file mode 100644 index 00000000..64e9ee0e --- /dev/null +++ b/orgfront/vite.org-context-chart.config.ts @@ -0,0 +1,25 @@ +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vite"; + +const isMinifiedBuild = process.env.ORG_CONTEXT_CHART_MINIFY === "true"; +const fileSuffix = isMinifiedBuild ? ".min" : ""; + +export default defineConfig({ + build: { + emptyOutDir: !isMinifiedBuild, + lib: { + entry: fileURLToPath( + new URL("./src/sdk/org-context-chart/index.ts", import.meta.url), + ), + fileName: (format) => + format === "es" + ? `baron-org-context-chart${fileSuffix}.js` + : `baron-org-context-chart${fileSuffix}.umd.cjs`, + formats: ["es", "umd"], + name: "BaronOrgContextChart", + }, + minify: isMinifiedBuild, + outDir: "dist/org-context-chart", + sourcemap: true, + }, +}); diff --git a/scripts/render_ory_config.sh b/scripts/render_ory_config.sh index 09613e6f..52f9e823 100755 --- a/scripts/render_ory_config.sh +++ b/scripts/render_ory_config.sh @@ -62,6 +62,58 @@ append_unique_url() { KRATOS_ALLOWED_RETURN_URLS+=("$candidate") } +url_host() { + local url="${1:-}" + [[ -n "$url" ]] || return 0 + + local without_scheme="$url" + if [[ "$without_scheme" == *"://"* ]]; then + without_scheme="${without_scheme#*://}" + fi + without_scheme="${without_scheme%%/*}" + without_scheme="${without_scheme%%\?*}" + without_scheme="${without_scheme%%#*}" + + if [[ "$without_scheme" == \[*\]* ]]; then + without_scheme="${without_scheme#[}" + without_scheme="${without_scheme%%]*}" + elif [[ "$without_scheme" == *:* ]]; then + without_scheme="${without_scheme%%:*}" + fi + + printf '%s' "$without_scheme" +} + +resolve_kratos_session_cookie_domain() { + if [[ -n "${KRATOS_SESSION_COOKIE_DOMAIN:-}" ]]; then + export KRATOS_SESSION_COOKIE_DOMAIN + return 0 + fi + + local public_host + public_host="$(url_host "${KRATOS_BROWSER_URL:-}")" + if [[ -z "$public_host" ]]; then + public_host="$(url_host "${KRATOS_UI_URL:-}")" + fi + + case "$public_host" in + localhost|127.0.0.1|0.0.0.0|*.localhost) + KRATOS_SESSION_COOKIE_DOMAIN="localhost" + ;; + *.hmac.kr|hmac.kr) + KRATOS_SESSION_COOKIE_DOMAIN="hmac.kr" + ;; + "") + KRATOS_SESSION_COOKIE_DOMAIN="localhost" + ;; + *) + KRATOS_SESSION_COOKIE_DOMAIN="$public_host" + ;; + esac + + export KRATOS_SESSION_COOKIE_DOMAIN +} + build_kratos_allowed_return_urls_yaml() { KRATOS_ALLOWED_RETURN_URLS=() if [[ -n "${KRATOS_ALLOWED_RETURN_URLS_JSON:-}" ]]; then @@ -137,6 +189,7 @@ OATHKEEPER_INTROSPECT_CLIENT_SECRET="${OATHKEEPER_INTROSPECT_CLIENT_SECRET:-oath export KRATOS_DSN HYDRA_DSN KETO_DSN HYDRA_SYSTEM_SECRET export OATHKEEPER_INTROSPECT_CLIENT_ID OATHKEEPER_INTROSPECT_CLIENT_SECRET +resolve_kratos_session_cookie_domain build_kratos_allowed_return_urls_yaml mkdir -p "$OUTPUT_DIR/kratos" "$OUTPUT_DIR/hydra" "$OUTPUT_DIR/keto" "$OUTPUT_DIR/oathkeeper" diff --git a/test/orgfront_org_context_chart_package_test.sh b/test/orgfront_org_context_chart_package_test.sh new file mode 100644 index 00000000..761bb7b6 --- /dev/null +++ b/test/orgfront_org_context_chart_package_test.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +fail() { + echo "[org-context-chart-package] $*" >&2 + exit 1 +} + +assert_contains() { + local file="$1" + local needle="$2" + grep -Fq "$needle" "$file" || fail "$file must contain: $needle" +} + +assert_contains orgfront/package.json "build:org-context-chart:min" +assert_contains orgfront/vite.org-context-chart.config.ts "ORG_CONTEXT_CHART_MINIFY" +assert_contains orgfront/vite.org-context-chart.config.ts ".min" + +echo "OK: OrgContext chart package emits explicit minified bundles" diff --git a/test/ory_v26_compose_policy_test.sh b/test/ory_v26_compose_policy_test.sh index 1b22b922..2f1c01a4 100644 --- a/test/ory_v26_compose_policy_test.sh +++ b/test/ory_v26_compose_policy_test.sh @@ -268,6 +268,18 @@ if ! grep -q '^render-ory-config:' "$repo_root/Makefile"; then echo "ERROR: Makefile must render Ory config before starting Ory services." >&2 exit 1 fi +if ! awk '/^ensure-ory:/ { in_target=1 } in_target && /^[^[:space:]].*:/ && $0 !~ /^ensure-ory:/ { exit } in_target { print }' "$repo_root/Makefile" | grep -q 'restart kratos'; then + echo "ERROR: make up-dev must restart Kratos when Ory is already running so rendered dev config is applied." >&2 + exit 1 +fi +if ! awk '/^up-all:/ { in_target=1 } in_target && /^[^[:space:]].*:/ && $0 !~ /^up-all:/ { exit } in_target { print }' "$repo_root/Makefile" | grep -q 'restart kratos'; then + echo "ERROR: make up must restart Kratos after rendering Ory config." >&2 + exit 1 +fi +if ! awk '/^up-ory:/ { in_target=1 } in_target && /^[^[:space:]].*:/ && $0 !~ /^up-ory:/ { exit } in_target { print }' "$repo_root/Makefile" | grep -q 'restart kratos'; then + echo "ERROR: make up-ory must restart Kratos after rendering Ory config." >&2 + exit 1 +fi if ! grep -q 'scripts/render_ory_config.sh' "$repo_root/.gitea/workflows/staging_code_pull.yml"; then echo "ERROR: staging code pull must render Ory config before docker compose up." >&2 @@ -285,6 +297,11 @@ if grep -Eq '^[[:space:]]*rm -rf "?\$OUTPUT_DIR"?[[:space:]]*$' "$repo_root/scri fi "$repo_root/scripts/render_ory_config.sh" >/dev/null +local_rendered_kratos="$repo_root/config/.generated/ory/kratos/kratos.yml" +if ! awk '/session:/ { in_session=1 } in_session && /domain:/ { print; exit }' "$local_rendered_kratos" | grep -q 'domain: localhost'; then + echo "ERROR: rendered local Kratos config must use localhost as session.cookie.domain for dev runs." >&2 + exit 1 +fi stage_render_dir="$(mktemp -d)" stage_render_env="$(mktemp)" @@ -310,6 +327,10 @@ if awk '/allowed_return_urls:/ { in_block=1; next } in_block && /^[[:space:]]+me echo "ERROR: rendered stage Kratos allowed_return_urls must not fall back to localhost." >&2 exit 1 fi +if ! awk '/session:/ { in_session=1 } in_session && /domain:/ { print; exit }' "$stage_rendered_kratos" | grep -q 'domain: hmac.kr'; then + echo "ERROR: rendered stage Kratos config must derive hmac.kr as session.cookie.domain." >&2 + exit 1 +fi rm -rf "$stage_render_dir" "$stage_render_env" for generated_config in \