mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix: block delegated provider egress
Carry bounded work authority through queues and revalidate it at dispatch.\nResolve shared records without personal credentials.
This commit is contained in:
1 parent
f7a06df2bc
commit
e209db24f4
11 files changed
+466
-25
No files matched your search
+54
-1
@@ -16,6 +16,11 @@ import { isPublicUrl } from "@/lib/validations/notifications";
|
||||
import { isLocalAiHostAllowed } from "./local-host-allowlist";
|
||||
import { parseProviderChain, type ProviderChainType } from "./provider-chain";
|
||||
import type { ProviderChainResolved } from "./provider-runner";
|
||||
import {
|
||||
providerCredentialPolicy,
|
||||
providerWorkAuthorityForRecord,
|
||||
type ProviderCredentialPolicy,
|
||||
} from "@/lib/sharing/provider-work-authority";
|
||||
|
||||
const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
@@ -400,6 +405,12 @@ async function resolveAdminCodexProvider(): Promise<AIProvider | null> {
|
||||
* 4. Nothing configured → NoProvider().
|
||||
*/
|
||||
export async function resolveProvider(userId: string): Promise<AIProvider> {
|
||||
const policy = providerCredentialPolicy(
|
||||
providerWorkAuthorityForRecord(userId),
|
||||
);
|
||||
if (policy === "deny") return new NoProvider();
|
||||
if (policy === "operator-default") return resolveAdminProvider();
|
||||
|
||||
const userRow = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
@@ -459,6 +470,17 @@ export async function resolveProvider(userId: string): Promise<AIProvider> {
|
||||
export async function resolveProviderChain(
|
||||
userId: string,
|
||||
): Promise<ProviderChainResolved[]> {
|
||||
const policy = providerCredentialPolicy(
|
||||
providerWorkAuthorityForRecord(userId),
|
||||
);
|
||||
if (policy === "deny") return [];
|
||||
if (policy === "operator-default") {
|
||||
const provider = await resolveAdminProvider();
|
||||
return provider.type === "none"
|
||||
? []
|
||||
: [{ providerType: "admin-openai", instance: provider }];
|
||||
}
|
||||
|
||||
const userRow = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
@@ -484,6 +506,7 @@ export async function resolveProviderChain(
|
||||
const instance = await resolveProviderForType(entry.providerType, {
|
||||
userId,
|
||||
userRow,
|
||||
policy,
|
||||
});
|
||||
if (instance) {
|
||||
resolved.push({ providerType: entry.providerType, instance });
|
||||
@@ -545,7 +568,10 @@ export interface ProviderCredentialRow {
|
||||
export function userRowHasProviderCredential(
|
||||
row: ProviderCredentialRow,
|
||||
adminKeyConfigured: boolean,
|
||||
policy: ProviderCredentialPolicy = "personal",
|
||||
): boolean {
|
||||
if (policy === "deny") return false;
|
||||
if (policy === "operator-default") return adminKeyConfigured;
|
||||
const codexConnected =
|
||||
row.codexConnectionStatus === "connected" &&
|
||||
!!row.codexAccessTokenEncrypted &&
|
||||
@@ -608,6 +634,10 @@ export function userRowHasProviderCredential(
|
||||
export async function hasAnyConfiguredProvider(
|
||||
userId: string,
|
||||
): Promise<boolean> {
|
||||
const policy = providerCredentialPolicy(
|
||||
providerWorkAuthorityForRecord(userId),
|
||||
);
|
||||
if (policy === "deny") return false;
|
||||
const userRow = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
@@ -637,7 +667,13 @@ export async function hasAnyConfiguredProvider(
|
||||
adminCodexAccountIdEncrypted: true,
|
||||
},
|
||||
});
|
||||
if (userRowHasProviderCredential(userRow, !!settings?.adminAiKeyEncrypted)) {
|
||||
if (
|
||||
userRowHasProviderCredential(
|
||||
userRow,
|
||||
!!settings?.adminAiKeyEncrypted,
|
||||
policy,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// A user with no personal provider can still be served by the operator's
|
||||
@@ -685,7 +721,12 @@ export type ProviderManagedBy = "user" | "local" | "server";
|
||||
function resolveManagedByFromRow(
|
||||
row: ProviderCredentialRow,
|
||||
adminKeyConfigured: boolean,
|
||||
policy: ProviderCredentialPolicy = "personal",
|
||||
): ProviderManagedBy | null {
|
||||
if (policy === "deny") return null;
|
||||
if (policy === "operator-default") {
|
||||
return adminKeyConfigured ? "server" : null;
|
||||
}
|
||||
const codexConnected =
|
||||
row.codexConnectionStatus === "connected" &&
|
||||
!!row.codexAccessTokenEncrypted &&
|
||||
@@ -749,6 +790,10 @@ function resolveManagedByFromRow(
|
||||
export async function resolveProviderAvailability(
|
||||
userId: string,
|
||||
): Promise<{ aiAvailable: boolean; managedBy: ProviderManagedBy | null }> {
|
||||
const policy = providerCredentialPolicy(
|
||||
providerWorkAuthorityForRecord(userId),
|
||||
);
|
||||
if (policy === "deny") return { aiAvailable: false, managedBy: null };
|
||||
const userRow = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
@@ -781,6 +826,7 @@ export async function resolveProviderAvailability(
|
||||
const managedBy = resolveManagedByFromRow(
|
||||
userRow,
|
||||
!!settings?.adminAiKeyEncrypted,
|
||||
policy,
|
||||
);
|
||||
if (managedBy !== null) return { aiAvailable: true, managedBy };
|
||||
// The operator's shared central Codex is operator-managed egress ("server").
|
||||
@@ -808,8 +854,15 @@ async function resolveProviderForType(
|
||||
aiCompatKeyEncrypted: string | null;
|
||||
aiCompatModel: string | null;
|
||||
} | null;
|
||||
policy: ProviderCredentialPolicy;
|
||||
},
|
||||
): Promise<AIProvider | null> {
|
||||
if (ctx.policy === "deny") return null;
|
||||
if (ctx.policy === "operator-default") {
|
||||
return providerType === "admin-openai"
|
||||
? await resolveAdminProvider()
|
||||
: null;
|
||||
}
|
||||
switch (providerType) {
|
||||
case "codex": {
|
||||
return resolveCodexProvider(ctx.userId);
|
||||
|
||||
@@ -793,6 +793,12 @@ export async function requireRecordAuth(
|
||||
if (carrier.kind === "none") {
|
||||
// Do no harm: without a carrier this is byte-for-byte the pre-v1.36.0
|
||||
// request, resolved by the same `authenticateCaller` every other mode uses.
|
||||
getEvent()?.setProviderWorkAuthority({
|
||||
origin: "owner",
|
||||
recordUserId: auth.user.id,
|
||||
actorUserId: auth.user.id,
|
||||
grantId: null,
|
||||
});
|
||||
return { ...auth, actor: auth.user, grantId: null };
|
||||
}
|
||||
if (carrier.kind === "misplaced-header") {
|
||||
@@ -944,6 +950,12 @@ async function resolveSwitchedRecord(
|
||||
if (owner.id !== auth.user.id && owner.managedProfileAt === null) {
|
||||
getEvent()?.setDelegatedGenerationSuppressed();
|
||||
}
|
||||
getEvent()?.setProviderWorkAuthority({
|
||||
origin: owner.managedProfileAt === null ? "delegate" : "guardian",
|
||||
recordUserId: owner.id,
|
||||
actorUserId: auth.user.id,
|
||||
grantId: grant.id,
|
||||
});
|
||||
// Fire-and-forget, the `ApiToken.lastUsedAt` posture: the read must not wait
|
||||
// on the bookkeeping, and the bookkeeping failing must not fail the read.
|
||||
void touchGrantUsage(grant.id);
|
||||
|
||||
@@ -66,7 +66,17 @@ describe("runInsightStatusGenerate", () => {
|
||||
"medication-compliance": vi.fn(),
|
||||
};
|
||||
await runInsightStatusGenerate(
|
||||
{ userId: "u1", metric: "weight", locale: "en" },
|
||||
{
|
||||
userId: "u1",
|
||||
metric: "weight",
|
||||
locale: "en",
|
||||
authority: {
|
||||
origin: "owner",
|
||||
recordUserId: "u1",
|
||||
actorUserId: "u1",
|
||||
grantId: null,
|
||||
},
|
||||
},
|
||||
generators,
|
||||
);
|
||||
expect(weight).toHaveBeenCalledWith("u1", { locale: "en", force: true });
|
||||
@@ -78,7 +88,17 @@ describe("runInsightStatusGenerate", () => {
|
||||
await expect(
|
||||
runInsightStatusGenerate(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{ userId: "u1", metric: "nope" as any, locale: "de" },
|
||||
{
|
||||
userId: "u1",
|
||||
metric: "nope" as any,
|
||||
locale: "de",
|
||||
authority: {
|
||||
origin: "owner",
|
||||
recordUserId: "u1",
|
||||
actorUserId: "u1",
|
||||
grantId: null,
|
||||
},
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{} as any,
|
||||
),
|
||||
@@ -118,7 +138,17 @@ describe("enqueueStatusGeneration", () => {
|
||||
});
|
||||
expect(send).toHaveBeenCalledWith(
|
||||
INSIGHT_STATUS_GENERATE_QUEUE,
|
||||
{ userId: "u1", metric: "mood", locale: "en" },
|
||||
{
|
||||
userId: "u1",
|
||||
metric: "mood",
|
||||
locale: "en",
|
||||
authority: {
|
||||
origin: "system",
|
||||
recordUserId: "u1",
|
||||
actorUserId: null,
|
||||
grantId: null,
|
||||
},
|
||||
},
|
||||
expect.objectContaining({ singletonKey: "u1:mood" }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
import type { SupportedLocale } from "@/lib/insights/status-shared";
|
||||
import { getGlobalBoss } from "@/lib/jobs/boss-instance";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import {
|
||||
mayEnqueueProviderWork,
|
||||
providerWorkAuthorityForRecord,
|
||||
type ProviderWorkAuthority,
|
||||
} from "@/lib/sharing/provider-work-authority";
|
||||
|
||||
export const INSIGHT_STATUS_GENERATE_QUEUE = "insight-status-generate";
|
||||
|
||||
@@ -91,6 +96,8 @@ export interface InsightStatusGeneratePayload {
|
||||
metric: InsightStatusScope;
|
||||
/** Resolved client locale — the cache key the route reads against. */
|
||||
locale: SupportedLocale;
|
||||
/** Durable admission authority, checked again before worker dispatch. */
|
||||
authority?: ProviderWorkAuthority;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,8 +127,16 @@ export interface InsightStatusGeneratePayload {
|
||||
* the requested language.
|
||||
*/
|
||||
export async function enqueueStatusGeneration(
|
||||
payload: InsightStatusGeneratePayload,
|
||||
payload: Omit<InsightStatusGeneratePayload, "authority">,
|
||||
): Promise<void> {
|
||||
const authority = providerWorkAuthorityForRecord(payload.userId);
|
||||
if (!mayEnqueueProviderWork(authority)) {
|
||||
annotate({
|
||||
action: { name: "insights.status.generate.refused" },
|
||||
meta: { metric: payload.metric, reason: "provider_authority" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
const boss = getGlobalBoss();
|
||||
if (!boss) {
|
||||
annotate({
|
||||
@@ -131,7 +146,7 @@ export async function enqueueStatusGeneration(
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await boss.send(INSIGHT_STATUS_GENERATE_QUEUE, payload, {
|
||||
await boss.send(INSIGHT_STATUS_GENERATE_QUEUE, { ...payload, authority }, {
|
||||
singletonKey: `${payload.userId}:${payload.metric}`,
|
||||
// De-dupe within a short window so a polling client doesn't enqueue
|
||||
// a fresh job on every tick while a generation is already running.
|
||||
|
||||
@@ -56,6 +56,10 @@ import {
|
||||
type InsightStatusMetric,
|
||||
type InsightStatusGeneratePayload,
|
||||
} from "@/lib/jobs/insight-status-generate-shared";
|
||||
import {
|
||||
mayDispatchProviderWork,
|
||||
withProviderWorkAuthority,
|
||||
} from "@/lib/sharing/provider-work-authority";
|
||||
|
||||
export {
|
||||
INSIGHT_STATUS_GENERATE_QUEUE,
|
||||
@@ -93,6 +97,23 @@ const GENERATORS: Record<InsightStatusMetric, StatusGenerator> = {
|
||||
export async function runInsightStatusGenerate(
|
||||
payload: InsightStatusGeneratePayload,
|
||||
generators: Record<InsightStatusMetric, StatusGenerator> = GENERATORS,
|
||||
): Promise<void> {
|
||||
const authority = payload.authority;
|
||||
if (!authority || !(await mayDispatchProviderWork(authority))) {
|
||||
annotate({
|
||||
action: { name: "insights.status.generate.refused" },
|
||||
meta: { metric: payload.metric, reason: "provider_authority" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
await withProviderWorkAuthority(authority, () =>
|
||||
dispatchInsightStatusGenerate(payload, generators),
|
||||
);
|
||||
}
|
||||
|
||||
async function dispatchInsightStatusGenerate(
|
||||
payload: InsightStatusGeneratePayload,
|
||||
generators: Record<InsightStatusMetric, StatusGenerator> = GENERATORS,
|
||||
): Promise<void> {
|
||||
// v1.8.7.1 — a `metric:<METRIC_ID>` scope routes to the generic
|
||||
// HealthKit-metric generator rather than one of the seven specialised
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
*/
|
||||
import { getGlobalBoss } from "@/lib/jobs/boss-instance";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import {
|
||||
mayEnqueueProviderWork,
|
||||
providerWorkAuthorityForRecord,
|
||||
type ProviderWorkAuthority,
|
||||
} from "@/lib/sharing/provider-work-authority";
|
||||
import type { NarrativePeriod } from "@/lib/insights/narrative/period-narrative";
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
|
||||
@@ -36,6 +41,8 @@ export interface PeriodNarrativePayload {
|
||||
period?: NarrativePeriod;
|
||||
/** Locale to warm; defaults to the app default (`en`) when absent. */
|
||||
locale?: Locale;
|
||||
/** Present only for an on-demand record warm. */
|
||||
authority?: ProviderWorkAuthority;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,6 +57,14 @@ export async function enqueueNarrativeWarm(payload: {
|
||||
period: NarrativePeriod;
|
||||
locale: Locale;
|
||||
}): Promise<void> {
|
||||
const authority = providerWorkAuthorityForRecord(payload.userId);
|
||||
if (!mayEnqueueProviderWork(authority)) {
|
||||
annotate({
|
||||
action: { name: "insights.narrative.warm.refused" },
|
||||
meta: { period: payload.period, reason: "provider_authority" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
const boss = getGlobalBoss();
|
||||
if (!boss) return;
|
||||
try {
|
||||
@@ -59,6 +74,7 @@ export async function enqueueNarrativeWarm(payload: {
|
||||
userId: payload.userId,
|
||||
period: payload.period,
|
||||
locale: payload.locale,
|
||||
authority,
|
||||
} satisfies PeriodNarrativePayload,
|
||||
{
|
||||
singletonKey: `warm:${payload.userId}:${payload.period}:${payload.locale}`,
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
import type { PrismaClient } from "@/generated/prisma/client";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { getAssistantFlags } from "@/lib/feature-flags";
|
||||
import {
|
||||
mayDispatchProviderWork,
|
||||
withProviderWorkAuthority,
|
||||
} from "@/lib/sharing/provider-work-authority";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import {
|
||||
generatePeriodNarrative,
|
||||
@@ -254,13 +258,18 @@ function tally(
|
||||
*/
|
||||
export async function warmOneNarrative(
|
||||
payload: PeriodNarrativePayload,
|
||||
generate: typeof generatePeriodNarrative = generatePeriodNarrative,
|
||||
): Promise<NarrativeGenerateOutcome | null> {
|
||||
if (!payload.userId || !payload.period) return null;
|
||||
const authority = payload.authority;
|
||||
if (!authority || !(await mayDispatchProviderWork(authority))) return null;
|
||||
const flags = await getAssistantFlags();
|
||||
if (!flags.briefing && !flags.insightStatus) return null;
|
||||
return generatePeriodNarrative(payload.userId, {
|
||||
period: payload.period,
|
||||
locale: payload.locale ?? defaultLocale,
|
||||
force: true,
|
||||
});
|
||||
return withProviderWorkAuthority(authority, () =>
|
||||
generate(payload.userId!, {
|
||||
period: payload.period!,
|
||||
locale: payload.locale ?? defaultLocale,
|
||||
force: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { WideEvent, LogLevel, EventKind } from "./types";
|
||||
import type { ProviderWorkAuthority } from "@/lib/sharing/provider-work-authority";
|
||||
import { LOG_LEVEL_PRIORITY } from "./types";
|
||||
import { getDeployContext } from "./config";
|
||||
import { redactOptional, redactSecrets } from "./redact";
|
||||
@@ -117,6 +118,14 @@ export class WideEventBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
setProviderWorkAuthority(authority: ProviderWorkAuthority): this {
|
||||
this.event.auth = {
|
||||
...(this.event.auth ?? {}),
|
||||
provider_work_authority: authority,
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
setAction(action: WideEvent["action"]): this {
|
||||
this.event.action = action;
|
||||
return this;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ProviderWorkAuthority } from "@/lib/sharing/provider-work-authority";
|
||||
|
||||
/** Log-Levels in aufsteigender Schwere */
|
||||
export type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
@@ -76,6 +78,8 @@ export interface WideEvent {
|
||||
* managed profile and every background job is.
|
||||
*/
|
||||
delegated_generation?: "suppressed";
|
||||
/** Durable authority copied into provider queue payloads. */
|
||||
provider_work_authority?: ProviderWorkAuthority;
|
||||
};
|
||||
|
||||
// Business-Daten (was wurde getan)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getEvent } from "@/lib/logging/context";
|
||||
import { findActiveGrant } from "@/lib/sharing/grants";
|
||||
|
||||
/**
|
||||
* The authority that admitted provider work for a record.
|
||||
*
|
||||
* This envelope carries only durable identifiers. It travels in queue payloads,
|
||||
* never health content, so a worker can reject stale or delegated work before
|
||||
* a provider sees the record.
|
||||
*/
|
||||
export interface ProviderWorkAuthority {
|
||||
origin: "owner" | "delegate" | "guardian" | "system";
|
||||
recordUserId: string;
|
||||
actorUserId: string | null;
|
||||
grantId: string | null;
|
||||
}
|
||||
|
||||
export type ProviderCredentialPolicy =
|
||||
| "personal"
|
||||
| "operator-default"
|
||||
| "deny";
|
||||
|
||||
const workerAuthority = new AsyncLocalStorage<ProviderWorkAuthority>();
|
||||
|
||||
export function providerCredentialPolicy(
|
||||
authority: ProviderWorkAuthority | null,
|
||||
): ProviderCredentialPolicy {
|
||||
if (authority === null) return "deny";
|
||||
if (authority.origin === "delegate") return "deny";
|
||||
if (authority.origin === "guardian") return "operator-default";
|
||||
return "personal";
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the request or worker authority for exactly one record.
|
||||
*
|
||||
* A request that authenticated an actor but did not declare why it may reach a
|
||||
* different record has no authority to enqueue work for that record. Background
|
||||
* reconciliation has no actor and is represented explicitly as `system`.
|
||||
*/
|
||||
export function providerWorkAuthorityForRecord(
|
||||
recordUserId: string,
|
||||
): ProviderWorkAuthority | null {
|
||||
const worker = workerAuthority.getStore();
|
||||
if (worker) {
|
||||
return worker.recordUserId === recordUserId ? worker : null;
|
||||
}
|
||||
|
||||
const auth = getEvent()?.getAuth();
|
||||
const requestAuthority = auth?.provider_work_authority;
|
||||
if (requestAuthority) {
|
||||
return requestAuthority.recordUserId === recordUserId
|
||||
? requestAuthority
|
||||
: null;
|
||||
}
|
||||
|
||||
if (auth?.user_id) {
|
||||
return auth.user_id === recordUserId
|
||||
? {
|
||||
origin: "owner",
|
||||
recordUserId,
|
||||
actorUserId: recordUserId,
|
||||
grantId: null,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
return {
|
||||
origin: "system",
|
||||
recordUserId,
|
||||
actorUserId: null,
|
||||
grantId: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function mayEnqueueProviderWork(
|
||||
authority: ProviderWorkAuthority | null,
|
||||
): authority is ProviderWorkAuthority {
|
||||
return authority !== null && authority.origin !== "delegate";
|
||||
}
|
||||
|
||||
/** Recheck durable admission immediately before worker dispatch. */
|
||||
export async function mayDispatchProviderWork(
|
||||
authority: ProviderWorkAuthority | undefined,
|
||||
): Promise<boolean> {
|
||||
if (!authority) return false;
|
||||
|
||||
switch (authority.origin) {
|
||||
case "owner":
|
||||
return (
|
||||
authority.actorUserId === authority.recordUserId &&
|
||||
authority.grantId === null
|
||||
);
|
||||
case "system":
|
||||
return authority.actorUserId === null && authority.grantId === null;
|
||||
case "delegate":
|
||||
return false;
|
||||
case "guardian": {
|
||||
if (
|
||||
authority.actorUserId === null ||
|
||||
authority.actorUserId === authority.recordUserId ||
|
||||
authority.grantId === null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const [record, grant] = await Promise.all([
|
||||
prisma.user.findUnique({
|
||||
where: { id: authority.recordUserId },
|
||||
select: { managedProfileAt: true },
|
||||
}),
|
||||
findActiveGrant({
|
||||
grantorId: authority.recordUserId,
|
||||
granteeId: authority.actorUserId,
|
||||
}),
|
||||
]);
|
||||
return (
|
||||
record?.managedProfileAt != null &&
|
||||
grant?.id === authority.grantId &&
|
||||
grant.access === "MANAGE"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function withProviderWorkAuthority<T>(
|
||||
authority: ProviderWorkAuthority,
|
||||
work: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
return workerAuthority.run(authority, work);
|
||||
}
|
||||
@@ -37,6 +37,13 @@ vi.mock("@/lib/db-compat", () => ({
|
||||
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/feature-flags", () => ({
|
||||
getAssistantFlags: vi.fn().mockResolvedValue({
|
||||
briefing: true,
|
||||
insightStatus: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
import {
|
||||
ACCOUNT_SELECTOR_HEADER,
|
||||
apiHandler,
|
||||
@@ -46,9 +53,13 @@ import { hashToken } from "@/lib/auth/hmac";
|
||||
import {
|
||||
providerCredentialPolicy,
|
||||
type ProviderWorkAuthority,
|
||||
withProviderWorkAuthority,
|
||||
} from "@/lib/sharing/provider-work-authority";
|
||||
import { enqueueStatusGeneration } from "@/lib/jobs/insight-status-generate-shared";
|
||||
import { runInsightStatusGenerate } from "@/lib/jobs/insight-status-generate";
|
||||
import type { InsightStatusGeneratePayload } from "@/lib/jobs/insight-status-generate";
|
||||
import { resolveProvider } from "@/lib/ai/provider";
|
||||
import { invalidateStatusInsightsForTypes } from "@/lib/insights/status-invalidation";
|
||||
import { warmOneNarrative } from "@/lib/jobs/period-narrative-warm";
|
||||
|
||||
const PROVIDER_ORIGIN_KEY = "provider-origin-test-key";
|
||||
let tokenNumber = 0;
|
||||
@@ -97,14 +108,17 @@ async function grantManage(input: {
|
||||
const enqueueFromSharedMutation: (request: NextRequest) => Promise<Response> =
|
||||
apiHandler(async () => {
|
||||
const { user } = await requireRecordAuth("manage", "medications");
|
||||
await enqueueStatusGeneration({
|
||||
userId: user.id,
|
||||
metric: "weight",
|
||||
locale: "en",
|
||||
});
|
||||
await invalidateStatusInsightsForTypes(user.id, ["WEIGHT"]);
|
||||
return NextResponse.json({ data: { queued: true }, error: null });
|
||||
});
|
||||
|
||||
const resolveFromSharedRead: (request: NextRequest) => Promise<Response> =
|
||||
apiHandler(async () => {
|
||||
const { user } = await requireRecordAuth("manage", "record");
|
||||
const provider = await resolveProvider(user.id);
|
||||
return NextResponse.json({ data: { provider: provider.type }, error: null });
|
||||
});
|
||||
|
||||
function runMutation(): Promise<Response> {
|
||||
return enqueueFromSharedMutation(
|
||||
new NextRequest("http://localhost/api/test/provider-origin", {
|
||||
@@ -113,6 +127,12 @@ function runMutation(): Promise<Response> {
|
||||
);
|
||||
}
|
||||
|
||||
function runSharedRead(): Promise<Response> {
|
||||
return resolveFromSharedRead(
|
||||
new NextRequest("http://localhost/api/test/provider-origin-read"),
|
||||
);
|
||||
}
|
||||
|
||||
function authority(input: ProviderWorkAuthority): ProviderWorkAuthority {
|
||||
return input;
|
||||
}
|
||||
@@ -136,11 +156,12 @@ describe("sharing provider origin", () => {
|
||||
const response = await runMutation();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(send).toHaveBeenCalledTimes(1);
|
||||
const payload = send.mock.calls[0]![1] as {
|
||||
authority: ProviderWorkAuthority;
|
||||
userId: string;
|
||||
};
|
||||
expect(send).toHaveBeenCalledTimes(3);
|
||||
const payload = (
|
||||
send.mock.calls as unknown as Array<
|
||||
[string, InsightStatusGeneratePayload, unknown]
|
||||
>
|
||||
)[0]![1];
|
||||
expect(payload.userId).toBe("provider-owner");
|
||||
expect(payload.authority).toEqual(
|
||||
authority({
|
||||
@@ -152,7 +173,15 @@ describe("sharing provider origin", () => {
|
||||
);
|
||||
|
||||
const generate = vi.fn().mockResolvedValue(undefined);
|
||||
await runInsightStatusGenerate(payload, { weight: generate });
|
||||
await runInsightStatusGenerate(payload, {
|
||||
general: vi.fn(),
|
||||
"blood-pressure": vi.fn(),
|
||||
weight: generate,
|
||||
pulse: vi.fn(),
|
||||
bmi: vi.fn(),
|
||||
mood: vi.fn(),
|
||||
"medication-compliance": vi.fn(),
|
||||
});
|
||||
|
||||
expect(generate).toHaveBeenCalledWith("provider-owner", {
|
||||
locale: "en",
|
||||
@@ -192,13 +221,38 @@ describe("sharing provider origin", () => {
|
||||
grantId: "provider-delegated-grant",
|
||||
}),
|
||||
},
|
||||
{ weight: generate },
|
||||
{
|
||||
general: vi.fn(),
|
||||
"blood-pressure": vi.fn(),
|
||||
weight: generate,
|
||||
pulse: vi.fn(),
|
||||
bmi: vi.fn(),
|
||||
mood: vi.fn(),
|
||||
"medication-compliance": vi.fn(),
|
||||
},
|
||||
);
|
||||
|
||||
expect(generate).not.toHaveBeenCalled();
|
||||
|
||||
const narrativeGenerate = vi.fn().mockResolvedValue(null);
|
||||
await warmOneNarrative(
|
||||
{
|
||||
userId: "provider-record",
|
||||
period: "week",
|
||||
authority: authority({
|
||||
origin: "delegate",
|
||||
recordUserId: "provider-record",
|
||||
actorUserId: "provider-delegate",
|
||||
grantId: "provider-delegated-grant",
|
||||
}),
|
||||
},
|
||||
narrativeGenerate,
|
||||
);
|
||||
|
||||
expect(narrativeGenerate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps a managed-profile Guardian on operator/default provider configuration", () => {
|
||||
it("keeps a managed-profile Guardian on operator/default provider configuration", async () => {
|
||||
const guardian = authority({
|
||||
origin: "guardian",
|
||||
recordUserId: "managed-record",
|
||||
@@ -217,5 +271,90 @@ describe("sharing provider origin", () => {
|
||||
}),
|
||||
),
|
||||
).toBe("deny");
|
||||
|
||||
await getPrismaClient().user.create({
|
||||
data: {
|
||||
id: "managed-record",
|
||||
username: "managed-record",
|
||||
email: "managed-record@example.test",
|
||||
role: "USER",
|
||||
managedProfileAt: new Date(),
|
||||
aiProvider: "LOCAL",
|
||||
aiBaseUrl: "https://guardian-byok.example.test/v1",
|
||||
},
|
||||
});
|
||||
await createUser("guardian-actor");
|
||||
await grantManage({
|
||||
id: "guardian-grant",
|
||||
recordUserId: "managed-record",
|
||||
actorUserId: "guardian-actor",
|
||||
});
|
||||
|
||||
const provider = await withProviderWorkAuthority(guardian, () =>
|
||||
resolveProvider("managed-record"),
|
||||
);
|
||||
|
||||
expect(provider.type).toBe("none");
|
||||
});
|
||||
|
||||
it("returns no personal provider from the shared comprehensive-read origin", async () => {
|
||||
await getPrismaClient().user.create({
|
||||
data: {
|
||||
id: "provider-read-record",
|
||||
username: "provider-read-record",
|
||||
email: "provider-read-record@example.test",
|
||||
role: "USER",
|
||||
aiProvider: "LOCAL",
|
||||
aiBaseUrl: "https://llm.example.test/v1",
|
||||
},
|
||||
});
|
||||
await createUser("provider-read-delegate");
|
||||
await grantManage({
|
||||
id: "provider-read-grant",
|
||||
recordUserId: "provider-read-record",
|
||||
actorUserId: "provider-read-delegate",
|
||||
});
|
||||
|
||||
headerJar.set(
|
||||
"authorization",
|
||||
`Bearer ${await mintToken("provider-read-record")}`,
|
||||
);
|
||||
const owner = await runSharedRead();
|
||||
expect((await owner.json()).data.provider).not.toBe("none");
|
||||
|
||||
headerJar.set(
|
||||
"authorization",
|
||||
`Bearer ${await mintToken("provider-read-delegate")}`,
|
||||
);
|
||||
headerJar.set(ACCOUNT_SELECTOR_HEADER, "provider-read-record");
|
||||
const delegated = await runSharedRead();
|
||||
|
||||
expect((await delegated.json()).data.provider).toBe("none");
|
||||
});
|
||||
|
||||
it("allows explicit system narrative work as the nightly positive control", async () => {
|
||||
await createUser("provider-nightly");
|
||||
const narrativeGenerate = vi.fn().mockResolvedValue(null);
|
||||
|
||||
await warmOneNarrative(
|
||||
{
|
||||
userId: "provider-nightly",
|
||||
period: "week",
|
||||
locale: "en",
|
||||
authority: authority({
|
||||
origin: "system",
|
||||
recordUserId: "provider-nightly",
|
||||
actorUserId: null,
|
||||
grantId: null,
|
||||
}),
|
||||
},
|
||||
narrativeGenerate,
|
||||
);
|
||||
|
||||
expect(narrativeGenerate).toHaveBeenCalledWith("provider-nightly", {
|
||||
period: "week",
|
||||
locale: "en",
|
||||
force: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user