mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Freezes the authority split the third level ships with. A token exercises a MANAGE grant, and a Guardian's authority over a managed profile, exactly as a browser does — proved at the resolver, where neither had a Bearer case before. A token cannot offer MANAGE and cannot create a managed profile at all, because both gates resolve through the session cookie; the second had no test until now, and it runs against a real database with a wildcard token to make the width of the permission irrelevant. The claim set is held together by a map naming, per claim, the file and the test title that proves it, so removing any one of them fails rather than quietly narrowing what a native client may do. The fence's client-facing half is frozen beside it: the account payload publishes the record context on a cookie session and null on a token, and the published contract says so under the same header names the server matches on.
149 lines
6.2 KiB
TypeScript
149 lines
6.2 KiB
TypeScript
/**
|
|
* v1.37.0 — what the native client may do with the third level, frozen.
|
|
*
|
|
* The release adds MANAGE and managed profiles, and the authority split it
|
|
* ships with is deliberately asymmetric:
|
|
*
|
|
* * a token EXERCISES a MANAGE grant exactly as a browser does, per request,
|
|
* through the account selector;
|
|
* * a token cannot MINT one, and cannot create a managed profile at all,
|
|
* because both acts are gated on a fresh second factor that resolves
|
|
* through the session cookie and has no Bearer fall-through.
|
|
*
|
|
* The asymmetry is a consequence of the step-up rather than a policy invented
|
|
* for the native client, and it is the sentence Plan 16's handoff note is
|
|
* derived from — so it is pinned here as a set rather than left implicit in
|
|
* five files that each prove a quarter of it.
|
|
*
|
|
* ## Why this file is a map and not a fifth copy of the assertions
|
|
*
|
|
* Every claim below is proved BEHAVIOURALLY somewhere — against the real
|
|
* resolver, or the real route, or a real Postgres. Re-asserting them here would
|
|
* make five tests that agree with each other and drift together. What has no
|
|
* home is the CLAIM SET: that these four facts are one contract, and that
|
|
* removing any one of them is a change to what a native client may do. This
|
|
* file names each claim, the file that proves it and the test title that does,
|
|
* and fails when a title moves or a file stops carrying it.
|
|
*
|
|
* The pattern is the repo's own — `record-session-fence-acceptance-map.test.ts`
|
|
* does the same for the fence's ten cases, for the same reason.
|
|
*/
|
|
import { readFileSync, existsSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const ROOT = process.cwd();
|
|
|
|
interface NativeAuthorityClaim {
|
|
/** What the contract promises. */
|
|
claim: string;
|
|
/** Where it is proved, relative to the repository root. */
|
|
file: string;
|
|
/** A fragment of the test title that proves it. */
|
|
title: string;
|
|
/** How it is proved, so a reader knows what the evidence is worth. */
|
|
kind: "resolver unit" | "route unit" | "postgres integration";
|
|
}
|
|
|
|
const NATIVE_AUTHORITY: NativeAuthorityClaim[] = [
|
|
{
|
|
claim: "A token exercises a MANAGE grant on a shared record.",
|
|
file: "src/lib/__tests__/acting-account-resolver.test.ts",
|
|
title: "admits a MANAGE grant over the Bearer transport too",
|
|
kind: "resolver unit",
|
|
},
|
|
{
|
|
claim: "A token exercises a Guardian's authority over a managed profile.",
|
|
file: "src/lib/__tests__/acting-account-resolver.test.ts",
|
|
title: "admits a Guardian over the Bearer transport, per request",
|
|
kind: "resolver unit",
|
|
},
|
|
{
|
|
claim:
|
|
"A token cannot offer MANAGE, and is told so in a code it can branch on.",
|
|
file: "src/app/api/account/grants/__tests__/invite-scope-and-manage.test.ts",
|
|
title: "refuses a Bearer caller with a code it can act on",
|
|
kind: "route unit",
|
|
},
|
|
{
|
|
claim:
|
|
"The refusal lands before the invitee lookup, so it is no account oracle.",
|
|
file: "src/app/api/account/grants/__tests__/invite-scope-and-manage.test.ts",
|
|
title:
|
|
"refuses the Bearer caller before it discloses whether the account exists",
|
|
kind: "route unit",
|
|
},
|
|
{
|
|
claim: "A token keeps minting the two levels it always could.",
|
|
file: "src/app/api/account/grants/__tests__/invite-scope-and-manage.test.ts",
|
|
title: "lets a Bearer caller keep minting the two levels it always could",
|
|
kind: "route unit",
|
|
},
|
|
{
|
|
claim: "A token cannot create a managed profile, at any permission width.",
|
|
file: "tests/integration/managed-profile-surface.test.ts",
|
|
title: "cannot be reached with a token, however wide its permissions",
|
|
kind: "postgres integration",
|
|
},
|
|
{
|
|
claim:
|
|
"The positive control: a cookie session with a fresh factor creates one.",
|
|
file: "tests/integration/managed-profile-surface.test.ts",
|
|
title: "creates a profile from exactly the body the form composes",
|
|
kind: "postgres integration",
|
|
},
|
|
{
|
|
claim: "A token cannot read the Guardian roster either — cookie-only.",
|
|
file: "tests/integration/managed-profile-surface.test.ts",
|
|
title: "refuses a Bearer caller, because the family is cookie-only",
|
|
kind: "postgres integration",
|
|
},
|
|
];
|
|
|
|
describe("the native client's authority over the third level", () => {
|
|
it("claims a non-empty set, over more than one kind of evidence", () => {
|
|
// A map that named nothing would pass every assertion below it.
|
|
expect(NATIVE_AUTHORITY.length).toBeGreaterThan(5);
|
|
expect(new Set(NATIVE_AUTHORITY.map((entry) => entry.kind)).size).toBe(3);
|
|
expect(new Set(NATIVE_AUTHORITY.map((entry) => entry.file)).size).toBe(3);
|
|
});
|
|
|
|
it.each(NATIVE_AUTHORITY)("is proved: $claim", ({ file, title }) => {
|
|
const path = join(ROOT, file);
|
|
expect(existsSync(path), `${file} is missing`).toBe(true);
|
|
const source = readFileSync(path, "utf8");
|
|
// The title as a test title, not as prose in a comment: a claim proved by
|
|
// a docblock is the shape this repo has been burned by.
|
|
expect(source, `${file} no longer carries "${title}"`).toContain(
|
|
`"${title}"`,
|
|
);
|
|
});
|
|
|
|
it("keeps the mint refusal and the exercise on opposite sides", () => {
|
|
// The one-sentence version of the contract, asserted on the source of the
|
|
// gate rather than on prose about it: the transport check is what makes
|
|
// the invitation cookie-only, and it sits above the step-up call so that a
|
|
// token is never handed the session gate's "not authenticated".
|
|
const invite = readFileSync(
|
|
join(ROOT, "src/app/api/account/grants/route.ts"),
|
|
"utf8",
|
|
);
|
|
const transportCheck = invite.indexOf('auth.authMethod !== "cookie"');
|
|
const stepUp = invite.indexOf("await requireFreshMfaIfEnrolled(");
|
|
expect(transportCheck).toBeGreaterThan(-1);
|
|
expect(stepUp).toBeGreaterThan(-1);
|
|
expect(transportCheck).toBeLessThan(stepUp);
|
|
|
|
// And the managed-profile creation does not even have the softer gate:
|
|
// `requireFreshMfa` refuses an account with no second factor enrolled,
|
|
// where `requireFreshMfaIfEnrolled` would wave it through.
|
|
const create = readFileSync(
|
|
join(ROOT, "src/app/api/managed-profiles/route.ts"),
|
|
"utf8",
|
|
);
|
|
expect(create).toContain("requireFreshMfa(");
|
|
expect(create).not.toContain("requireFreshMfaIfEnrolled");
|
|
});
|
|
});
|