562 lines
19 KiB
TypeScript
562 lines
19 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { type UniDeskConfig, readConfig, rootPath } from "./config";
|
|
|
|
const DEFAULT_REPO = "pikasTech/unidesk";
|
|
const DEFAULT_BASE = "master";
|
|
const SECRET_ENV_KEYS = ["GH_TOKEN", "GITHUB_TOKEN"] as const;
|
|
const BROKER_URL_ENV_KEYS = ["UNIDESK_AUTH_BROKER_URL", "AUTH_BROKER_URL"] as const;
|
|
const BROKER_CREDENTIAL_REF_ENV_KEYS = ["UNIDESK_AUTH_BROKER_GITHUB_CREDENTIAL_REF", "AUTH_BROKER_GITHUB_CREDENTIAL_REF"] as const;
|
|
const BROKER_CONFIGURED_ENV_KEYS = ["UNIDESK_AUTH_BROKER_GITHUB_CONFIGURED", "AUTH_BROKER_GITHUB_CONFIGURED"] as const;
|
|
const AUTH_BROKER_SERVICE_ID = "auth-broker";
|
|
const DEFAULT_CAPABILITIES = [
|
|
"github.auth.status",
|
|
"github.issue.list",
|
|
"github.issue.read",
|
|
"github.pr.list",
|
|
"github.pr.read",
|
|
"github.pr.create",
|
|
"github.pr.comment.create",
|
|
"github.pr.preflight.dry-run",
|
|
] as const;
|
|
|
|
type BrokerCommand = "contract" | "credential-request" | "pr-preflight" | "health";
|
|
type RunnerDisposition = "ready" | "infra-blocked" | "business-failed";
|
|
type ConfigSource = "config.json" | "unavailable";
|
|
|
|
interface BrokerAdapterOptions {
|
|
command: BrokerCommand;
|
|
repo: string;
|
|
operation: string;
|
|
dryRun: boolean;
|
|
endpoint: string | null;
|
|
base: string;
|
|
head: string;
|
|
issueNumber: number | null;
|
|
}
|
|
|
|
interface AuthBrokerServiceRegistration {
|
|
ok: boolean;
|
|
serviceId: "auth-broker";
|
|
source: ConfigSource;
|
|
configured: boolean;
|
|
providerId: string | null;
|
|
deploymentMode: string | null;
|
|
proxyMode: string | null;
|
|
backendBaseUrl: string | null;
|
|
healthPath: string | null;
|
|
allowedPathPrefixes: string[];
|
|
composeService: string | null;
|
|
containerName: string | null;
|
|
dockerfile: string | null;
|
|
public: boolean | null;
|
|
error: string | null;
|
|
}
|
|
|
|
interface ComposeProfileRegistration {
|
|
ok: boolean;
|
|
source: "docker-compose.yml";
|
|
serviceId: "auth-broker";
|
|
servicePresent: boolean;
|
|
profileGated: boolean;
|
|
profiles: string[];
|
|
restart: string | null;
|
|
publicPortPublished: boolean;
|
|
exposesPort: boolean;
|
|
mutatesDefaultRuntime: false;
|
|
}
|
|
|
|
interface DeployManifestRegistration {
|
|
ok: boolean;
|
|
source: "deploy.json";
|
|
serviceId: "auth-broker";
|
|
prod: { present: boolean; commitId: string | null };
|
|
dev: { present: boolean; commitId: string | null };
|
|
dryRunOnly: true;
|
|
}
|
|
|
|
interface RuntimeCredentialRefPresence {
|
|
ok: boolean;
|
|
source: string | null;
|
|
configuredFlag: {
|
|
present: boolean;
|
|
key: string | null;
|
|
truthy: boolean;
|
|
};
|
|
credentialRef: {
|
|
present: boolean;
|
|
key: string | null;
|
|
valuePrinted: false;
|
|
valuePreview: string | null;
|
|
};
|
|
presenceOnly: true;
|
|
valuesRead: false;
|
|
valuesPrinted: false;
|
|
}
|
|
|
|
function hasEnvKey(name: string): boolean {
|
|
return Object.prototype.hasOwnProperty.call(process.env, name);
|
|
}
|
|
|
|
function firstPresentEnvKey(keys: readonly string[]): string | null {
|
|
for (const key of keys) {
|
|
if (hasEnvKey(key)) return key;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function stringOption(args: string[], name: string): string | undefined {
|
|
const index = args.indexOf(name);
|
|
if (index === -1) return undefined;
|
|
const value = args[index + 1];
|
|
if (value === undefined || value.length === 0) throw new Error(`${name} requires a non-empty value`);
|
|
return value;
|
|
}
|
|
|
|
function sanitizeEndpoint(value: string): string {
|
|
try {
|
|
const parsed = new URL(value);
|
|
if (parsed.username.length > 0) parsed.username = "***";
|
|
if (parsed.password.length > 0) parsed.password = "***";
|
|
if (parsed.search.length > 0) parsed.search = "?...";
|
|
parsed.hash = "";
|
|
return parsed.toString();
|
|
} catch {
|
|
return value.includes("?") ? `${value.split("?")[0]}?...` : value;
|
|
}
|
|
}
|
|
|
|
function sanitizeCredentialRef(value: string): string {
|
|
const trimmed = value.trim();
|
|
if (trimmed.length === 0) return "<empty>";
|
|
const separator = trimmed.indexOf(":");
|
|
if (separator <= 0) return "<credential-ref>";
|
|
return `${trimmed.slice(0, separator)}:<ref>`;
|
|
}
|
|
|
|
function numberOption(args: string[], name: string): number | null {
|
|
const raw = stringOption(args, name);
|
|
if (raw === undefined) return null;
|
|
const value = Number(raw);
|
|
if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`);
|
|
return value;
|
|
}
|
|
|
|
function firstConfiguredBrokerUrl(): string | null {
|
|
for (const key of BROKER_URL_ENV_KEYS) {
|
|
if (hasEnvKey(key)) return `<${key}>`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function envTruthy(key: string | null): boolean {
|
|
if (key === null) return false;
|
|
const value = process.env[key]?.trim().toLowerCase();
|
|
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
}
|
|
|
|
function runtimeCredentialRefPresence(): RuntimeCredentialRefPresence {
|
|
const configuredKey = firstPresentEnvKey(BROKER_CONFIGURED_ENV_KEYS);
|
|
const refKey = firstPresentEnvKey(BROKER_CREDENTIAL_REF_ENV_KEYS);
|
|
return {
|
|
ok: configuredKey !== null && envTruthy(configuredKey) && refKey !== null,
|
|
source: refKey === null ? null : "broker-held-github-credential-ref",
|
|
configuredFlag: {
|
|
present: configuredKey !== null,
|
|
key: configuredKey,
|
|
truthy: envTruthy(configuredKey),
|
|
},
|
|
credentialRef: {
|
|
present: refKey !== null,
|
|
key: refKey,
|
|
valuePrinted: false,
|
|
valuePreview: refKey === null ? null : sanitizeCredentialRef(process.env[refKey] ?? ""),
|
|
},
|
|
presenceOnly: true,
|
|
valuesRead: false,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function readConfigRegistration(): AuthBrokerServiceRegistration {
|
|
let config: UniDeskConfig;
|
|
try {
|
|
config = readConfig();
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
serviceId: AUTH_BROKER_SERVICE_ID,
|
|
source: "unavailable",
|
|
configured: false,
|
|
providerId: null,
|
|
deploymentMode: null,
|
|
proxyMode: null,
|
|
backendBaseUrl: null,
|
|
healthPath: null,
|
|
allowedPathPrefixes: [],
|
|
composeService: null,
|
|
containerName: null,
|
|
dockerfile: null,
|
|
public: null,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
};
|
|
}
|
|
const service = config.microservices.find((item) => item.id === AUTH_BROKER_SERVICE_ID);
|
|
return {
|
|
ok: service !== undefined,
|
|
serviceId: AUTH_BROKER_SERVICE_ID,
|
|
source: "config.json",
|
|
configured: service !== undefined,
|
|
providerId: service?.providerId ?? null,
|
|
deploymentMode: service?.deployment.mode ?? null,
|
|
proxyMode: service?.backend.proxyMode ?? null,
|
|
backendBaseUrl: service?.backend.nodeBaseUrl ?? null,
|
|
healthPath: service?.backend.healthPath ?? null,
|
|
allowedPathPrefixes: service?.backend.allowedPathPrefixes ?? [],
|
|
composeService: service?.repository.composeService ?? null,
|
|
containerName: service?.repository.containerName ?? null,
|
|
dockerfile: service?.repository.dockerfile ?? null,
|
|
public: service?.backend.public ?? null,
|
|
error: service === undefined ? "auth-broker is not registered in config.json microservices" : null,
|
|
};
|
|
}
|
|
|
|
function extractComposeServiceBlock(composeText: string, serviceName: string): string {
|
|
const lines = composeText.split("\n");
|
|
const startLine = lines.findIndex((line) => line === ` ${serviceName}:`);
|
|
if (startLine < 0) return "";
|
|
let endLine = lines.length;
|
|
for (let index = startLine + 1; index < lines.length; index += 1) {
|
|
if (/^ [A-Za-z0-9][A-Za-z0-9_-]*:$/u.test(lines[index] ?? "")) {
|
|
endLine = index;
|
|
break;
|
|
}
|
|
}
|
|
return lines.slice(startLine, endLine).join("\n");
|
|
}
|
|
|
|
function composeProfileRegistration(): ComposeProfileRegistration {
|
|
const composeText = readFileSync(rootPath("docker-compose.yml"), "utf8");
|
|
const block = extractComposeServiceBlock(composeText, AUTH_BROKER_SERVICE_ID);
|
|
const blockLines = block.split("\n");
|
|
const profiles: string[] = [];
|
|
const profileStart = blockLines.findIndex((line) => /^\s{4}profiles:\s*$/u.test(line));
|
|
if (profileStart >= 0) {
|
|
for (let index = profileStart + 1; index < blockLines.length; index += 1) {
|
|
const line = blockLines[index] ?? "";
|
|
if (/^\s{4}[A-Za-z0-9_-]+:/u.test(line)) break;
|
|
const profile = line.match(/^\s{6}-\s+"?([A-Za-z0-9_.-]+)"?\s*$/u)?.[1];
|
|
if (profile !== undefined) profiles.push(profile);
|
|
}
|
|
}
|
|
const restart = block.match(/^\s{4}restart:\s+"?([^"\n]+)"?\s*$/mu)?.[1] ?? null;
|
|
const portsSection = /^\s{4}ports:\s*$/mu.test(block);
|
|
const portMapping = /^\s{6}-\s+["']?[^"'\n]*4291:/mu.test(block);
|
|
return {
|
|
ok: block.length > 0 && profiles.includes(AUTH_BROKER_SERVICE_ID) && !portsSection && !portMapping,
|
|
source: "docker-compose.yml",
|
|
serviceId: AUTH_BROKER_SERVICE_ID,
|
|
servicePresent: block.length > 0,
|
|
profileGated: profiles.includes(AUTH_BROKER_SERVICE_ID),
|
|
profiles,
|
|
restart,
|
|
publicPortPublished: portsSection || portMapping,
|
|
exposesPort: block.includes('"4291"'),
|
|
mutatesDefaultRuntime: false,
|
|
};
|
|
}
|
|
|
|
function serviceCommitFromDeployJson(environment: "prod" | "dev"): string | null {
|
|
const parsed = JSON.parse(readFileSync(rootPath("deploy.json"), "utf8")) as {
|
|
environments?: Record<string, { services?: Array<{ id?: unknown; commitId?: unknown }> }>;
|
|
};
|
|
const service = parsed.environments?.[environment]?.services?.find((item) => item.id === AUTH_BROKER_SERVICE_ID);
|
|
return typeof service?.commitId === "string" ? service.commitId : null;
|
|
}
|
|
|
|
function deployManifestRegistration(): DeployManifestRegistration {
|
|
const prodCommit = serviceCommitFromDeployJson("prod");
|
|
const devCommit = serviceCommitFromDeployJson("dev");
|
|
return {
|
|
ok: prodCommit !== null && devCommit !== null,
|
|
source: "deploy.json",
|
|
serviceId: AUTH_BROKER_SERVICE_ID,
|
|
prod: { present: prodCommit !== null, commitId: prodCommit },
|
|
dev: { present: devCommit !== null, commitId: devCommit },
|
|
dryRunOnly: true,
|
|
};
|
|
}
|
|
|
|
function serviceRegistrationContract(): Record<string, unknown> {
|
|
return {
|
|
config: readConfigRegistration(),
|
|
compose: composeProfileRegistration(),
|
|
deploy: deployManifestRegistration(),
|
|
runtimeCredentialRef: runtimeCredentialRefPresence(),
|
|
defaultRuntimeMutation: {
|
|
ok: true,
|
|
mutatesCurrentProd: false,
|
|
composeProfileRequired: AUTH_BROKER_SERVICE_ID,
|
|
publicPortPublished: false,
|
|
liveDeployPerformed: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
function parseOptions(args: string[]): BrokerAdapterOptions {
|
|
const rawCommand = args[0] ?? "contract";
|
|
if (!["contract", "credential-request", "pr-preflight", "health"].includes(rawCommand)) {
|
|
throw new Error(`unknown auth-broker command: ${rawCommand}`);
|
|
}
|
|
const command = rawCommand as BrokerCommand;
|
|
const endpoint = stringOption(args, "--endpoint");
|
|
return {
|
|
command,
|
|
repo: stringOption(args, "--repo") ?? DEFAULT_REPO,
|
|
operation: stringOption(args, "--operation") ?? (command === "pr-preflight" ? "github.pr.preflight.dry-run" : "github.auth.status"),
|
|
dryRun: args.includes("--dry-run") || command === "contract" || command === "credential-request" || command === "pr-preflight" || command === "health",
|
|
endpoint: endpoint === undefined ? firstConfiguredBrokerUrl() : sanitizeEndpoint(endpoint),
|
|
base: stringOption(args, "--base") ?? DEFAULT_BASE,
|
|
head: stringOption(args, "--head") ?? "<head-branch>",
|
|
issueNumber: numberOption(args, "--issue"),
|
|
};
|
|
}
|
|
|
|
function runnerEnvTokenCoverage(): Record<string, unknown> {
|
|
const present = SECRET_ENV_KEYS.filter((key) => hasEnvKey(key));
|
|
return {
|
|
ok: present.length > 0,
|
|
source: present.length > 0 ? "runner-env" : null,
|
|
checkedKeys: SECRET_ENV_KEYS,
|
|
presentKeys: present,
|
|
missingKeys: SECRET_ENV_KEYS.filter((key) => !present.includes(key)),
|
|
presenceOnly: true,
|
|
valuesRead: false,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function brokerCoverage(endpoint: string | null): Record<string, unknown> {
|
|
const credential = runtimeCredentialRefPresence();
|
|
return {
|
|
ok: endpoint !== null,
|
|
source: endpoint === null ? null : "auth-broker",
|
|
endpoint: endpoint ?? null,
|
|
credentialRef: endpoint === null ? null : credential.credentialRef.valuePreview ?? "github:<ref>",
|
|
credentialRefPresent: credential.credentialRef.present,
|
|
credentialRefSourceKey: credential.credentialRef.key,
|
|
scope: endpoint === null ? null : "broker-held-github-credential",
|
|
runnerEnvTokenRequired: false,
|
|
valuesRead: false,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function brokerNeededResult(options: BrokerAdapterOptions): Record<string, unknown> {
|
|
return {
|
|
ok: false,
|
|
failureKind: "auth-missing",
|
|
degradedReason: "broker-needed",
|
|
runnerDisposition: "infra-blocked" as RunnerDisposition,
|
|
retryable: false,
|
|
brokerNeeded: true,
|
|
message: "No auth broker endpoint is configured for this dry-run contract; runner env token coverage is reported only for migration diagnostics.",
|
|
tokenCoverage: runnerEnvTokenCoverage(),
|
|
brokerCoverage: brokerCoverage(options.endpoint),
|
|
serviceRegistration: serviceRegistrationContract(),
|
|
authBroker: {
|
|
ok: false,
|
|
source: "broker/auth-broker-needed",
|
|
capability: "missing-token",
|
|
nextAction: "configure-auth-broker",
|
|
runnerEnvTokenRequired: false,
|
|
brokerIssuedTokenAvailable: false,
|
|
valuesRead: false,
|
|
valuesPrinted: false,
|
|
realPrCreateRequiresCommanderAuthorization: true,
|
|
},
|
|
next: [
|
|
"configure UNIDESK_AUTH_BROKER_URL or AUTH_BROKER_URL for broker-backed runner auth",
|
|
"keep GH_TOKEN/GITHUB_TOKEN out of ordinary runner env once broker mode is enabled",
|
|
],
|
|
redaction: {
|
|
valuesRead: false,
|
|
valuesPrinted: false,
|
|
forbiddenOutputKeys: ["token", "secret", "authorization", "cookie"],
|
|
},
|
|
};
|
|
}
|
|
|
|
function auditEventShape(options: BrokerAdapterOptions): Record<string, unknown> {
|
|
return {
|
|
requestId: "authbroker-contract-request",
|
|
observedAt: "ISO-8601 timestamp",
|
|
caller: { plane: "code-queue", taskId: null, queueId: null },
|
|
operation: options.operation,
|
|
repo: options.repo,
|
|
resource: options.command === "pr-preflight"
|
|
? { base: options.base, head: options.head, issueNumber: options.issueNumber }
|
|
: null,
|
|
dryRun: true,
|
|
credentialRef: "github:<ref>",
|
|
credentialKind: "github-rest-token-ref",
|
|
credentialValuePrinted: false,
|
|
upstream: { method: "planned", path: "planned GitHub REST path without query secrets" },
|
|
status: "HTTP status",
|
|
ok: "boolean",
|
|
failureKind: null,
|
|
degradedReason: null,
|
|
runnerDisposition: "ready|infra-blocked|business-failed",
|
|
retryable: "boolean",
|
|
durationMs: "integer",
|
|
redaction: { valuesPrinted: false },
|
|
};
|
|
}
|
|
|
|
function plannedCredentialRequest(options: BrokerAdapterOptions): Record<string, unknown> {
|
|
return {
|
|
requestId: "authbroker-cli-dry-run",
|
|
caller: { plane: "manual-cli" },
|
|
repo: options.repo,
|
|
operation: options.operation,
|
|
dryRun: true,
|
|
params: options.command === "pr-preflight"
|
|
? { base: options.base, head: options.head, issueNumber: options.issueNumber }
|
|
: {},
|
|
};
|
|
}
|
|
|
|
function readyContract(options: BrokerAdapterOptions): Record<string, unknown> {
|
|
return {
|
|
ok: true,
|
|
runnerDisposition: "ready" as RunnerDisposition,
|
|
failureKind: null,
|
|
degradedReason: null,
|
|
brokerNeeded: false,
|
|
dryRun: true,
|
|
mutation: false,
|
|
capabilities: [...DEFAULT_CAPABILITIES],
|
|
authBroker: {
|
|
ok: true,
|
|
source: "auth-broker",
|
|
capability: "broker-issued-token",
|
|
nextAction: "use-auth-broker",
|
|
runnerEnvTokenRequired: false,
|
|
brokerIssuedTokenAvailable: true,
|
|
valuesRead: false,
|
|
valuesPrinted: false,
|
|
realPrCreateRequiresCommanderAuthorization: true,
|
|
},
|
|
tokenCoverage: {
|
|
ok: true,
|
|
source: "auth-broker",
|
|
scope: "broker-held-github-credential",
|
|
runnerEnvTokenRequired: false,
|
|
valuesRead: false,
|
|
valuesPrinted: false,
|
|
},
|
|
brokerCoverage: brokerCoverage(options.endpoint),
|
|
credentialRequest: plannedCredentialRequest(options),
|
|
auditEventShape: auditEventShape(options),
|
|
serviceRegistration: serviceRegistrationContract(),
|
|
prCapabilityContract: {
|
|
targetBranch: options.base,
|
|
headBranch: options.head,
|
|
authSource: "broker-issued-token",
|
|
systemGhBinaryRequiredForWrites: false,
|
|
preflightCreatesPr: false,
|
|
preflightMergesPr: false,
|
|
realPrCreateRequiresCommanderAuthorization: true,
|
|
brokerProxy: {
|
|
ok: true,
|
|
operations: ["github.auth.status", "github.issue.read", "github.pr.read", "github.pr.create"],
|
|
writesRemote: false,
|
|
},
|
|
pushDryRun: {
|
|
runnerLocal: true,
|
|
coveredByBroker: false,
|
|
},
|
|
},
|
|
redaction: {
|
|
valuesRead: false,
|
|
valuesPrinted: false,
|
|
secretKeysBlocked: ["token", "secret", "authorization", "cookie"],
|
|
},
|
|
};
|
|
}
|
|
|
|
function contractResult(options: BrokerAdapterOptions): Record<string, unknown> {
|
|
return {
|
|
ok: true,
|
|
service: "auth-broker",
|
|
phase: "p0",
|
|
commands: [
|
|
"bun scripts/cli.ts auth-broker contract",
|
|
"bun scripts/cli.ts auth-broker health --dry-run",
|
|
"bun scripts/cli.ts auth-broker credential-request --operation github.pr.create --repo pikasTech/unidesk --dry-run",
|
|
"bun scripts/cli.ts auth-broker pr-preflight --repo pikasTech/unidesk --base master --head <head-branch> --issue 59 --dry-run",
|
|
],
|
|
capabilities: [...DEFAULT_CAPABILITIES],
|
|
permissionBoundary: {
|
|
allowedRepos: [DEFAULT_REPO],
|
|
liveGithubWrites: false,
|
|
arbitraryGhApi: false,
|
|
registryCredentials: false,
|
|
deployPermissions: false,
|
|
},
|
|
serviceRegistration: serviceRegistrationContract(),
|
|
runnerNoTokenResult: brokerNeededResult({ ...options, endpoint: null }),
|
|
readyShape: readyContract({ ...options, endpoint: "<UNIDESK_AUTH_BROKER_URL>" }),
|
|
};
|
|
}
|
|
|
|
export function authBrokerHelp(): unknown {
|
|
return {
|
|
command: "auth-broker",
|
|
output: "json",
|
|
usage: [
|
|
"bun scripts/cli.ts auth-broker contract",
|
|
"bun scripts/cli.ts auth-broker health --dry-run [--endpoint URL]",
|
|
"bun scripts/cli.ts auth-broker credential-request --operation github.pr.create --repo pikasTech/unidesk --dry-run [--endpoint URL]",
|
|
"bun scripts/cli.ts auth-broker pr-preflight --repo pikasTech/unidesk --base master --head <head-branch> [--issue N] --dry-run [--endpoint URL]",
|
|
],
|
|
boundary: [
|
|
"P0 adapter is contract/dry-run only and never starts a service.",
|
|
"GH_TOKEN and GITHUB_TOKEN values are not read or printed; only key presence is reported.",
|
|
"No dry-run command writes GitHub, registry, deploy, filesystem credential, or service state.",
|
|
],
|
|
reference: "docs/reference/auth-broker.md",
|
|
};
|
|
}
|
|
|
|
export function runAuthBrokerCommand(args: string[]): Record<string, unknown> {
|
|
if (args.some((arg) => arg === "help" || arg === "--help" || arg === "-h")) return authBrokerHelp() as Record<string, unknown>;
|
|
const options = parseOptions(args);
|
|
if (options.command === "contract") return contractResult(options);
|
|
|
|
const envCoverage = runnerEnvTokenCoverage();
|
|
const broker = brokerCoverage(options.endpoint);
|
|
if (broker.ok !== true) return brokerNeededResult(options);
|
|
|
|
if (options.command === "health") {
|
|
return {
|
|
ok: true,
|
|
dryRun: true,
|
|
mutation: false,
|
|
service: "auth-broker",
|
|
phase: "p0",
|
|
brokerCoverage: broker,
|
|
tokenCoverage: envCoverage,
|
|
serviceRegistration: serviceRegistrationContract(),
|
|
healthRequest: {
|
|
method: "GET",
|
|
path: "/health",
|
|
wouldCallBroker: false,
|
|
},
|
|
capabilities: [...DEFAULT_CAPABILITIES],
|
|
redaction: { valuesRead: false, valuesPrinted: false },
|
|
};
|
|
}
|
|
|
|
return readyContract(options);
|
|
}
|