Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion PG-PROFILE-51-REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ tail:

The completed profile receipt recorded 1,034 files; that historical receipt
is unchanged. The template equivalence registry is a different measure:
156 eligible plus 37 cold-required files, for **193** total. The
157 eligible plus 37 cold-required files, for **194** total. The
handle-generation, issuance-cutover, opaque-cursor and search-index repair
tests remain registered. These counts are recomputed from the merged registry arrays.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright The PDP-Connect Contributors
// SPDX-License-Identifier: Apache-2.0

import type { ProviderInvalidationProofStore } from "../../server/stores/provider-invalidation-proof-store.ts";
import {
deriveSignInProof,
observeLastTopLevelUrl,
signInUrlPatternFromManifest,
} from "./sign-in-proof.ts";

export interface RecordSignInProofInput {
/** The leased surface's DevTools base URL, or null when the run had no leased surface. */
readonly cdpUrl: string | null;
readonly connectionId: string;
readonly connectorId: string;
/**
* Re-checked after the DevTools read and before the write; false means the
* run was superseded while the read was in flight, so nothing is written.
*/
readonly isCurrent?: () => boolean;
readonly manifest: Parameters<typeof signInUrlPatternFromManifest>[0];
readonly nowIso: string;
readonly runId: string;
/** The run's terminal status from the runtime result. */
readonly status: string | null | undefined;
}

export interface RecordSignInProofDeps {
readonly fetchImpl?: typeof fetch;
readonly store: ProviderInvalidationProofStore;
}

/**
* Run-completion writer. A failed run on a connector that declares a sign-in
* pattern records a proof when the run's last top-level URL matches it. A
* succeeded run marks earlier proofs repaired so they cannot prompt again.
* Connectors without a pattern, and every other status, change nothing.
*/
export async function recordSignInProofAfterRun(
deps: RecordSignInProofDeps,
input: RecordSignInProofInput,
): Promise<"recorded" | "repaired" | "none"> {
// Repair first: a success must clear earlier proofs even when the manifest
// no longer declares a pattern.
if (input.status === "succeeded") {
await deps.store.markRepaired(input.connectionId, input.nowIso);
return "repaired";
}
const pattern = signInUrlPatternFromManifest(input.manifest);
if (!pattern) {
return "none";
}
if (input.status !== "failed" || !input.cdpUrl) {
return "none";
}
const lastTopLevelUrl = await observeLastTopLevelUrl(
input.cdpUrl,
deps.fetchImpl,
).catch(() => null);
const proof = deriveSignInProof({
connectionId: input.connectionId,
lastTopLevelUrl,
observedAt: input.nowIso,
pattern,
provider: input.connectorId,
runFailed: true,
runId: input.runId,
});
if (!proof || input.isCurrent?.() === false) {
return "none";
}
await deps.store.record(proof);
return "recorded";
}
143 changes: 143 additions & 0 deletions reference-implementation/runtime/browser-surface/sign-in-proof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright The PDP-Connect Contributors
// SPDX-License-Identifier: Apache-2.0

import type { ProviderInvalidationProof } from "./repair-decision.ts";

/**
* Runtime-side provider-invalidation proof.
*
* A proof exists only when the run FAILED and the runtime's own observation of
* the run's browser (its last top-level page URL) matched the sign-in URL
* pattern the connector manifest declares. A visit to the sign-in page on a
* healthy run is not proof, and nothing here reads connector-reported gaps or
* message text.
*/

export const MAX_SIGN_IN_PATTERN_LENGTH = 512;
const MAX_URL_LENGTH = 2048;

/** Declared at `runtime_requirements.bindings.browser.sign_in_url_pattern`. */
export function signInUrlPatternFromManifest(manifest: {
readonly runtime_requirements?: {
readonly bindings?: Record<string, unknown>;
};
}): string | null {
const browser = manifest.runtime_requirements?.bindings?.browser;
if (
browser === null ||
typeof browser !== "object" ||
Array.isArray(browser)
) {
return null;
}
const pattern = (browser as { readonly sign_in_url_pattern?: unknown })
.sign_in_url_pattern;
const trimmed = typeof pattern === "string" ? pattern.trim() : "";
return trimmed.length > 0 && trimmed.length <= MAX_SIGN_IN_PATTERN_LENGTH
? trimmed
: null;
}

/**
* Anchored glob over the whole URL; `*` matches any run of characters and
* everything else is literal. Segments are matched left to right with
* `indexOf`, never a regular expression, so a hostile pattern cannot trigger
* backtracking; both inputs are length-capped, which bounds the work.
*/
export function signInUrlMatchesPattern(pattern: string, url: string): boolean {
if (
pattern.length > MAX_SIGN_IN_PATTERN_LENGTH ||
url.length > MAX_URL_LENGTH
) {
return false;
}
const parts = pattern.split("*");
if (parts.length === 1) {
return pattern === url;
}
const first = parts[0] ?? "";
const last = parts.at(-1) ?? "";
if (
!(url.startsWith(first) && url.endsWith(last)) ||
first.length + last.length > url.length
) {
return false;
}
const end = url.length - last.length;
let position = first.length;
for (const part of parts.slice(1, -1)) {
if (part === "") {
continue;
}
const found = url.indexOf(part, position);
if (found < 0 || found + part.length > end) {
return false;
}
position = found + part.length;
}
return true;
}

/**
* Reads the run's top-level page URL from its own browser surface (DevTools
* target list). It sends nothing to the provider. The list does not carry
* navigation order or run ownership, so it fails closed: the URL is returned
* only when exactly one page target has an http(s) URL. Several candidates
* (another tab, a popup) make "the last navigation" unknowable, so no proof.
*/
export async function observeLastTopLevelUrl(
cdpUrl: string,
fetchImpl: typeof fetch = globalThis.fetch.bind(globalThis),
timeoutMs = 3000,
): Promise<string | null> {
const signal = AbortSignal.timeout(timeoutMs);
const response = await fetchImpl(new URL("/json/list", cdpUrl), { signal });
if (!response.ok) {
return null;
}
const targets: unknown = await response.json();
if (!Array.isArray(targets)) {
return null;
}
const candidates = (
targets as readonly { readonly type?: unknown; readonly url?: unknown }[]
)
.filter(
(target) =>
target?.type === "page" &&
typeof target.url === "string" &&
/^https?:/i.test(target.url),
)
.map((target) => target.url as string);
return candidates.length === 1 ? (candidates[0] ?? null) : null;
}

export interface SignInProofInput {
readonly connectionId: string;
readonly lastTopLevelUrl: string | null;
readonly observedAt: string;
readonly pattern: string | null;
readonly provider: string;
readonly runFailed: boolean;
readonly runId: string;
}

/** Pure rule: failed run AND last top-level URL matches the declared pattern. */
export function deriveSignInProof(
input: SignInProofInput,
): ProviderInvalidationProof | null {
if (!(input.runFailed && input.pattern && input.lastTopLevelUrl)) {
return null;
}
if (!signInUrlMatchesPattern(input.pattern, input.lastTopLevelUrl)) {
return null;
}
return {
connection_id: input.connectionId,
evidence_id: input.runId,
kind: "provider_invalidation_proof",
observed_at: input.observedAt,
provider: input.provider,
verified: true,
};
}
7 changes: 7 additions & 0 deletions reference-implementation/runtime/connection-health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2601,6 +2601,13 @@ function credentialsValidCondition(input: ComputeConnectionHealthInput): Connect
if (credentialAbsent) {
return credentialRequiredCondition();
}
// A runtime-verified provider invalidation proof outranks the success
// branch below: a run with no failure reason must not read as "credentials
// accepted" while an unrepaired proof stands. Replay suppression is inside
// `browserSessionRepairAuthorized`.
if (browserSessionRepairAuthorized(input)) {
return browserSessionRequiredCondition(null);
}
if (input.run?.latestStatus === "succeeded") {
return condition({
message: "The latest successful run proved credentials were accepted.",
Expand Down
Loading
Loading