@gfdelarue/testkit 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/command-catalog.d.ts +0 -127
- package/dist/cli/command-catalog.js +1 -1
- package/dist/cli/command-registry.d.ts +1 -0
- package/dist/cli/command-registry.js +1 -1
- package/dist/cli/entrypoint.js +1 -1
- package/dist/cli/framework/execute.d.ts +1 -0
- package/dist/cli/framework/execute.js +6 -6
- package/dist/cli/index.js +1 -1
- package/dist/cli/version-drift.js +7 -7
- package/dist/config/authoring/index.d.ts +0 -5
- package/dist/config/loader/index.d.ts +0 -1
- package/dist/config/loader/index.js +1 -1
- package/dist/evidence/bundles.d.ts +2 -3
- package/dist/evidence/bundles.js +1 -1
- package/dist/evidence/contracts.d.ts +0 -8
- package/dist/evidence/index.d.ts +1 -1
- package/dist/evidence/index.js +1 -1
- package/dist/process/execa-bundle.json +1 -1
- package/package.json +3 -10
- package/testkit-config-schema.md +0 -23
- package/dist/app/cloud/campaigns.d.ts +0 -6
- package/dist/app/cloud/campaigns.js +0 -1
- package/dist/app/cloud/control-actions.d.ts +0 -12
- package/dist/app/cloud/control-actions.js +0 -2
- package/dist/app/cloud/control-links.d.ts +0 -5
- package/dist/app/cloud/control-links.js +0 -1
- package/dist/app/cloud/control-read.d.ts +0 -24
- package/dist/app/cloud/control-read.js +0 -1
- package/dist/app/cloud/control-resources.d.ts +0 -3
- package/dist/app/cloud/control-resources.js +0 -1
- package/dist/app/cloud/control.d.ts +0 -34
- package/dist/app/cloud/control.js +0 -5
- package/dist/app/cloud/credentials.d.ts +0 -11
- package/dist/app/cloud/credentials.js +0 -2
- package/dist/app/cloud/index.d.ts +0 -22
- package/dist/app/cloud/index.js +0 -2
- package/dist/app/cloud/login.d.ts +0 -63
- package/dist/app/cloud/login.js +0 -2
- package/dist/app/cloud/migration/archive.d.ts +0 -13
- package/dist/app/cloud/migration/archive.js +0 -1
- package/dist/app/cloud/migration/commands.d.ts +0 -8
- package/dist/app/cloud/migration/commands.js +0 -1
- package/dist/app/cloud/migration/evidence.d.ts +0 -13
- package/dist/app/cloud/migration/evidence.js +0 -2
- package/dist/app/cloud/migration/files.d.ts +0 -16
- package/dist/app/cloud/migration/files.js +0 -2
- package/dist/app/cloud/migration/index.d.ts +0 -6
- package/dist/app/cloud/migration/index.js +0 -1
- package/dist/app/cloud/migration/journal.d.ts +0 -64
- package/dist/app/cloud/migration/journal.js +0 -1
- package/dist/app/cloud/migration/mapping-dispositions.d.ts +0 -3
- package/dist/app/cloud/migration/mapping-dispositions.js +0 -1
- package/dist/app/cloud/migration/mapping.d.ts +0 -11
- package/dist/app/cloud/migration/mapping.js +0 -2
- package/dist/app/cloud/migration/model.d.ts +0 -175
- package/dist/app/cloud/migration/model.js +0 -1
- package/dist/app/cloud/migration/plan.d.ts +0 -7
- package/dist/app/cloud/migration/plan.js +0 -1
- package/dist/app/cloud/migration/progress.d.ts +0 -19
- package/dist/app/cloud/migration/progress.js +0 -1
- package/dist/app/cloud/migration/reconcile.d.ts +0 -22
- package/dist/app/cloud/migration/reconcile.js +0 -1
- package/dist/app/cloud/migration/reconciliation-spool.d.ts +0 -4
- package/dist/app/cloud/migration/reconciliation-spool.js +0 -1
- package/dist/app/cloud/migration/state.d.ts +0 -15
- package/dist/app/cloud/migration/state.js +0 -1
- package/dist/app/cloud/migration/transfer.d.ts +0 -6
- package/dist/app/cloud/migration/transfer.js +0 -1
- package/dist/cli/cloud-apply-help.d.ts +0 -1
- package/dist/cli/cloud-apply-help.js +0 -56
- package/dist/cli/cloud-selection.d.ts +0 -2
- package/dist/cli/cloud-selection.js +0 -3
- package/dist/cli/commands/cloud/apply.d.ts +0 -5
- package/dist/cli/commands/cloud/apply.js +0 -1
- package/dist/cli/commands/cloud/campaign/create.d.ts +0 -5
- package/dist/cli/commands/cloud/campaign/create.js +0 -1
- package/dist/cli/commands/cloud/download.d.ts +0 -5
- package/dist/cli/commands/cloud/download.js +0 -1
- package/dist/cli/commands/cloud/list.d.ts +0 -5
- package/dist/cli/commands/cloud/list.js +0 -1
- package/dist/cli/commands/cloud/login.d.ts +0 -13
- package/dist/cli/commands/cloud/login.js +0 -2
- package/dist/cli/commands/cloud/logout.d.ts +0 -7
- package/dist/cli/commands/cloud/logout.js +0 -1
- package/dist/cli/commands/cloud/migrate/apply.d.ts +0 -2
- package/dist/cli/commands/cloud/migrate/apply.js +0 -1
- package/dist/cli/commands/cloud/migrate/plan.d.ts +0 -7
- package/dist/cli/commands/cloud/migrate/plan.js +0 -1
- package/dist/cli/commands/cloud/migrate/progress.d.ts +0 -2
- package/dist/cli/commands/cloud/migrate/progress.js +0 -2
- package/dist/cli/commands/cloud/migrate/reconcile.d.ts +0 -2
- package/dist/cli/commands/cloud/migrate/reconcile.js +0 -1
- package/dist/cli/commands/cloud/policy/preview.d.ts +0 -5
- package/dist/cli/commands/cloud/policy/preview.js +0 -1
- package/dist/cli/commands/cloud/project/create.d.ts +0 -5
- package/dist/cli/commands/cloud/project/create.js +0 -1
- package/dist/cli/commands/cloud/publish.d.ts +0 -7
- package/dist/cli/commands/cloud/publish.js +0 -1
- package/dist/cli/commands/cloud/session/revoke.d.ts +0 -8
- package/dist/cli/commands/cloud/session/revoke.js +0 -1
- package/dist/cli/commands/cloud/show.d.ts +0 -5
- package/dist/cli/commands/cloud/show.js +0 -1
- package/dist/cli/commands/cloud/snapshot.d.ts +0 -5
- package/dist/cli/commands/cloud/snapshot.js +0 -1
- package/dist/cli/commands/cloud/status.d.ts +0 -33
- package/dist/cli/commands/cloud/status.js +0 -1
- package/dist/cloud/account.d.ts +0 -34
- package/dist/cloud/account.js +0 -1
- package/dist/cloud/archive-contract.d.ts +0 -1
- package/dist/cloud/archive-contract.js +0 -1
- package/dist/cloud/archive.d.ts +0 -12
- package/dist/cloud/archive.js +0 -1
- package/dist/cloud/campaigns.d.ts +0 -200
- package/dist/cloud/campaigns.js +0 -2
- package/dist/cloud/client.d.ts +0 -68
- package/dist/cloud/client.js +0 -4
- package/dist/cloud/download.d.ts +0 -5
- package/dist/cloud/download.js +0 -1
- package/dist/cloud/historical-contract.d.ts +0 -224
- package/dist/cloud/historical-contract.js +0 -1
- package/dist/cloud/historical-json.d.ts +0 -16
- package/dist/cloud/historical-json.js +0 -1
- package/dist/cloud/historical.d.ts +0 -69
- package/dist/cloud/historical.js +0 -1
- package/dist/cloud/pagination.d.ts +0 -5
- package/dist/cloud/pagination.js +0 -1
- package/dist/cloud/transport.d.ts +0 -18
- package/dist/cloud/transport.js +0 -1
- package/dist/config/loader/cloud.d.ts +0 -2
- package/dist/config/loader/cloud.js +0 -1
- package/docs/cloud-control.md +0 -108
- package/docs/testkit-cloud-boundary.md +0 -628
- package/node_modules/buffer-crc32/LICENSE +0 -19
- package/node_modules/buffer-crc32/README.md +0 -55
- package/node_modules/buffer-crc32/dist/index.cjs +0 -306
- package/node_modules/buffer-crc32/dist/index.d.cts +0 -35
- package/node_modules/buffer-crc32/dist/index.d.mts +0 -35
- package/node_modules/buffer-crc32/dist/index.mjs +0 -304
- package/node_modules/buffer-crc32/index.d.ts +0 -35
- package/node_modules/buffer-crc32/package.json +0 -54
- package/node_modules/yazl/LICENSE +0 -21
- package/node_modules/yazl/README.md +0 -457
- package/node_modules/yazl/index.js +0 -826
- package/node_modules/yazl/package.json +0 -34
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{isCloudAccountToken as y}from"../../cloud/account.js";import{isUnknownRecord as t}from"../../kernel/unknown-values.js";import{invalidResponse as c}from"../../cloud/transport.js";export async function allItems(i,s){const r=[],e=new Set;let o="";do{const a=new URL(s,i.origin);a.searchParams.set("limit","100"),o&&a.searchParams.set("cursor",o);const n=await i.request("GET",a.href);if(!t(n)||!Array.isArray(n.items)||!n.items.every(t)||typeof n.nextCursor!="string")throw c("resource page");if(r.push(...n.items),o=n.nextCursor,o&&e.has(o))throw c("repeated page cursor");e.add(o)}while(o);return r}export async function resourceDashboardLinks(i,s,r,e){if(!["project","projects","campaign","campaigns"].includes(s)||!t(r))return r;const o=e.TESTKIT_CLOUD_TOKEN;if(o!==void 0&&!y(o))return r;const a=await allItems(i,"/api/v1/organisations"),n=s==="campaign"||s==="campaigns",d=Array.isArray(r.items)?r.items[0]:r;if(n&&d===void 0)return r;if(n&&(!t(d)||typeof d.projectId!="string"))throw c("campaign project");const u=n&&t(d)?await i.request("GET",`/api/v1/projects/${encodeURIComponent(String(d.projectId))}`):void 0;function m(p){if(!t(p))throw c("dashboard resource");const g=n?u:p;if(!t(g)||typeof g.slug!="string")throw c("project slug");const f=a.find(h=>t(h.organisation)&&h.organisation.id===g.organisationId);if(!f||!t(f.organisation)||typeof f.organisation.slug!="string")throw c("organisation slug");if(n&&typeof p.id!="string")throw c("campaign identity");return{...p,dashboardUrl:cloudDashboardUrl(i.origin,f.organisation.slug,g.slug,n?p.id:void 0)}}return Array.isArray(r.items)?{...r,items:r.items.map(m)}:m(r)}export function cloudDashboardUrl(i,s,r,e){const o=`/dashboard/${encodeURIComponent(s)}/${encodeURIComponent(r)}`;return new URL(e?`${o}/campaigns/${encodeURIComponent(e)}`:o,i).href}export async function createdResourceDashboardLink(i,s,r,e){if(!t(r))return r;const o=s==="project"?r.project:{...r,id:r.campaignId};try{const a=await resourceDashboardLinks(i,s,o,e);return t(a)&&typeof a.dashboardUrl=="string"?{...r,dashboardUrl:a.dashboardUrl}:r}catch{return r}}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export { cloudDashboardUrl } from "./control-links.js";
|
|
2
|
-
import { type ControlOptions } from "./control.js";
|
|
3
|
-
export interface ReadOptions extends ControlOptions {
|
|
4
|
-
limit?: number;
|
|
5
|
-
cursor?: string;
|
|
6
|
-
branch?: string;
|
|
7
|
-
decision?: string;
|
|
8
|
-
pr?: number;
|
|
9
|
-
sort?: string;
|
|
10
|
-
direction?: string;
|
|
11
|
-
flaky?: boolean;
|
|
12
|
-
receipt?: string;
|
|
13
|
-
caseId?: string;
|
|
14
|
-
testPath?: string;
|
|
15
|
-
page?: number;
|
|
16
|
-
campaign?: string;
|
|
17
|
-
filePath?: string;
|
|
18
|
-
rootCauseKey?: string;
|
|
19
|
-
issueReference?: string;
|
|
20
|
-
view?: string;
|
|
21
|
-
order?: string;
|
|
22
|
-
}
|
|
23
|
-
export declare function listCloudResources(resource: string, options: ReadOptions): Promise<unknown>;
|
|
24
|
-
export declare function showCloudResource(resource: string, id: string | undefined, options: ReadOptions): Promise<unknown>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{resourceDashboardLinks as p}from"./control-links.js";export{cloudDashboardUrl}from"./control-links.js";import{validateCloudListFilters as w}from"./control-resources.js";import{controlClient as m,resolveOrganisation as t,resolveProject as n}from"./control.js";export async function listCloudResources(i,e){w(i,{...e});const a=await m(e,i!=="sessions");let r;switch(i){case"artifacts":if(!e.receipt)throw new Error("Artifact listing requires --receipt");r=`/api/v1/receipts/${encodeURIComponent(e.receipt)}/artifacts`;break;case"metrics":r=`/api/v1/projects/${encodeURIComponent(await n(a,e))}/metrics`;break;case"case-occurrences":case"case-history":if(!e.caseId)throw new Error("Case history requires --case");r=`/api/v1/projects/${encodeURIComponent(await n(a,e))}/cases/${encodeURIComponent(e.caseId)}/${i==="case-history"?"history":"occurrences"}`;break;case"file-history":if(!e.testPath)throw new Error("File history requires --path");r=`/api/v1/projects/${encodeURIComponent(await n(a,e))}/file-history`;break;case"sessions":r="/api/v1/auth/cli/sessions";break;case"organisations":r="/api/v1/organisations";break;case"projects":r="/api/v1/projects";break;case"members":case"invitations":case"providers":case"policies":case"audit":r=`/api/v1/organisations/${encodeURIComponent(await t(a,e.organisation,e.choose))}/${i}`;break;case"notifications":case"tokens":case"campaigns":case"runs":case"tests":r=`/api/v1/projects/${encodeURIComponent(await n(a,e))}/${i}`;break;case"findings":r=`/api/v1/projects/${encodeURIComponent(await n(a,e))}/campaigns/${e.campaign?`${encodeURIComponent(e.campaign)}/`:""}findings`;break;case"cases":r=`/api/v1/projects/${encodeURIComponent(await n(a,e))}/cases`;break;default:throw new Error(`Unknown resource: ${i}`)}const s=new URL(r,a.origin),c={limit:e.limit,cursor:e.cursor,branch:e.branch,decision:e.decision,pr:e.pr,sort:e.sort,direction:e.direction,flaky:e.flaky,path:e.testPath,page:e.page,rootCauseKey:e.rootCauseKey,issueReference:e.issueReference,view:e.view,order:e.order};for(const[d,o]of Object.entries(c))o!==void 0&&s.searchParams.set(d,String(o));return i==="projects"&&e.organisation&&s.searchParams.set("organisationId",await t(a,e.organisation,e.choose)),p(a,i,await a.request("GET",s.href),e.env??process.env)}export async function showCloudResource(i,e,a){const r=await m(a),s=()=>{if(!e)throw new Error(`cloud show ${i} requires an ID`);return encodeURIComponent(e)};let c;switch(i){case"account":c="/api/v1/account";break;case"organisation":c=`/api/v1/organisations/${encodeURIComponent(await t(r,e??a.organisation,a.choose))}`;break;case"project":c=`/api/v1/projects/${encodeURIComponent(await n(r,{...a,project:e??a.project}))}`;break;case"policy":c=`/api/v1/organisations/${encodeURIComponent(await t(r,a.organisation,a.choose))}/policies/${s()}`;break;case"campaign":c=`/api/v1/projects/${encodeURIComponent(await n(r,a))}/campaigns/${s()}`;break;case"finding":if(!a.campaign)throw new Error("Finding detail requires --campaign");c=`/api/v1/projects/${encodeURIComponent(await n(r,a))}/campaigns/${encodeURIComponent(a.campaign)}/findings/${s()}`;break;case"receipt":c=`/api/v1/receipts/${s()}`;break;case"run":c=`/api/v1/runs/${s()}`;break;case"case":c=`/api/v1/projects/${encodeURIComponent(await n(r,a))}/cases/${s()}`;break;case"test-file":c=`/api/v1/projects/${encodeURIComponent(await n(r,a))}/test-file?path=${s()}`;break;case"project-policy":c=`/api/v1/projects/${encodeURIComponent(await n(r,a))}/policy`;break;case"performance":c=`/api/v1/projects/${encodeURIComponent(await n(r,a))}/performance`;break;case"connection":c=`/api/v1/project-connections/${s()}`;break;default:throw new Error(`Unknown resource: ${i}`)}return p(r,i,await r.request("GET",c),a.env??process.env)}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
export declare const cloudListResources: Readonly<Record<string, readonly string[]>>;
|
|
2
|
-
export declare const cloudShowResources: readonly ["account", "organisation", "project", "policy", "campaign", "finding", "receipt", "run", "case", "test-file", "performance", "project-policy", "connection"];
|
|
3
|
-
export declare function validateCloudListFilters(resource: string, options: Record<string, unknown>): void;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const e=["limit","cursor"];export const cloudListResources={organisations:e,projects:e,members:e,invitations:e,providers:e,policies:e,notifications:e,tokens:e,sessions:[],campaigns:e,findings:[...e,"campaign","rootCauseKey","issueReference","view","order"],runs:["limit","branch","decision","pr","sort","direction","page"],tests:["flaky"],audit:e,cases:e,artifacts:[...e,"receipt"],"case-history":[...e,"caseId"],"file-history":[...e,"testPath"],"case-occurrences":[...e,"caseId"],metrics:[...e,"testPath"]},cloudShowResources=["account","organisation","project","policy","campaign","finding","receipt","run","case","test-file","performance","project-policy","connection"];export function validateCloudListFilters(s,n){const o=Object.hasOwn(cloudListResources,s)?cloudListResources[s]:void 0;if(!o)throw new Error(`Unknown resource: ${s}`);const c=new Set(Object.values(cloudListResources).flat()),r={caseId:"case",testPath:"path",rootCauseKey:"root-cause-key",issueReference:"issue-reference"};for(const t of c){const i=n[t];if(i!==void 0&&i!==!1&&!o.includes(t))throw new Error(`--${r[t]??t} is not supported by cloud list ${s}`)}}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { CloudAccountClient } from "../../cloud/account.js";
|
|
2
|
-
export interface ControlOptions {
|
|
3
|
-
origin?: string;
|
|
4
|
-
env?: NodeJS.ProcessEnv;
|
|
5
|
-
organisation?: string;
|
|
6
|
-
project?: string;
|
|
7
|
-
browser?: boolean;
|
|
8
|
-
signal?: AbortSignal;
|
|
9
|
-
secretOutput?: string;
|
|
10
|
-
notify?: (message: string) => void;
|
|
11
|
-
choose?: (label: string, choices: readonly {
|
|
12
|
-
id: string;
|
|
13
|
-
name: string;
|
|
14
|
-
}[]) => Promise<string>;
|
|
15
|
-
}
|
|
16
|
-
export declare function controlClient(options?: ControlOptions, refreshCredential?: boolean): Promise<CloudAccountClient>;
|
|
17
|
-
export declare function resolveOrganisation(client: CloudAccountClient, selection?: string, choose?: ControlOptions["choose"]): Promise<string>;
|
|
18
|
-
export declare function resolveProject(client: CloudAccountClient, options: ControlOptions): Promise<string>;
|
|
19
|
-
export declare function projectsForRepository(projects: Record<string, unknown>[], origin: string): Record<string, unknown>[];
|
|
20
|
-
export declare function applyAccountCommand(command: Record<string, unknown>, options?: ControlOptions): Promise<unknown>;
|
|
21
|
-
export declare function waitForConnection(client: CloudAccountClient, initial: unknown, options: ControlOptions): Promise<unknown>;
|
|
22
|
-
export interface CreateProjectOptions extends ControlOptions {
|
|
23
|
-
name?: string;
|
|
24
|
-
repository?: string;
|
|
25
|
-
provider?: string;
|
|
26
|
-
host?: string;
|
|
27
|
-
publishDecisions?: boolean;
|
|
28
|
-
requestKey?: string;
|
|
29
|
-
}
|
|
30
|
-
export declare function createCloudProject(options: CreateProjectOptions): Promise<unknown>;
|
|
31
|
-
export declare function parseRepository(value: string, host?: string): {
|
|
32
|
-
repository: string;
|
|
33
|
-
host: string;
|
|
34
|
-
};
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import{setTimeout as v}from"node:timers/promises";import c from"node:fs";import $ from"node:path";import{randomUUID as I}from"node:crypto";import{execFileSync as R}from"node:child_process";import{CloudAccountClient as E}from"../../cloud/account.js";import{invalidResponse as u}from"../../cloud/transport.js";import{isUnknownRecord as p}from"../../kernel/unknown-values.js";import{cloudToken as k}from"./credentials.js";import{openBrowser as C,selectedCloudOrigin as S}from"./login.js";import{allItems as l,createdResourceDashboardLink as x}from"./control-links.js";export async function controlClient(o={},r=!0){const e=S(o);return new E(e,await k(e,o.env??process.env,r))}async function m(o,r,e,n){const t=r?o.filter(a=>a.id===r||a.slug===r||a.name===r):o;if(t.length>1&&!r&&n){const a=t.map(s=>{if(typeof s.id!="string"||!s.id)throw u(`${e} identity`);return{id:s.id,name:typeof s.name=="string"?s.name:s.id}}),d=await n(e,a);if(!a.some(s=>s.id===d))throw new Error(`Invalid ${e} selection`);return d}if(t.length!==1)throw new Error(`${e} ${r?`"${r}" `:""}${t.length?"is ambiguous":"was not found"}. Select an explicit ID from cloud list ${e==="organisation"?"organisations":"projects"}.`);const i=t[0]?.id;if(typeof i!="string"||!i)throw u(`${e} identity`);return i}export async function resolveOrganisation(o,r,e){const n=await l(o,"/api/v1/organisations");return m(n.map(t=>{if(!p(t.organisation))throw u("organisation");return t.organisation}),r,"organisation",e)}export async function resolveProject(o,r){const e=r.project??(r.env??process.env).TESTKIT_CLOUD_PROJECT_ID;if(e?.startsWith("prj_"))return e;const n=r.organisation?`?organisationId=${encodeURIComponent(await resolveOrganisation(o,r.organisation,r.choose))}`:"",t=await l(o,`/api/v1/projects${n}`),i=e?void 0:g(),a=i?projectsForRepository(t,i):[];return m(a.length?a:t,e,"project",r.choose)}function g(){try{return R("git",["remote","get-url","origin"],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}catch{return}}export function projectsForRepository(o,r){let e;try{e=parseRepository(r)}catch{return[]}return o.filter(n=>{const t=n.repository;return p(t)&&t.host===e.host&&`${String(t.owner)}/${String(t.name)}`===e.repository})}export async function applyAccountCommand(o,r={}){const e=o.operation==="token.create";if(r.secretOutput!==void 0&&!e)throw new Error("--secret-output is only supported for token.create.");const n=r.secretOutput?$.resolve(r.secretOutput):void 0;if(e&&!n)throw new Error("Credential creation requires --secret-output <new-file> to save the issued token privately.");const t=n?c.openSync(n,"wx",384):void 0;let i=!1,a=!1,d=!1;try{const s=await controlClient(r);d=e;const f=await s.request("POST","/api/v1/account/commands",o);if(e&&t!==void 0&&n!==void 0){if(i=!0,!p(f)||typeof f.value!="string")throw u("issued credential");c.writeFileSync(t,`${f.value}
|
|
2
|
-
`),c.fsyncSync(t);const y=c.fstatSync(t,{bigint:!0}),h=c.lstatSync(n,{bigint:!0});if(!h.isFile()||h.ino!==y.ino||h.dev!==y.dev)throw new Error("Credential output was replaced while the command was running");c.closeSync(t),a=!0;const w={...f};return delete w.value,{...w,secretFile:n}}return await waitForConnection(s,f,r)}catch(s){throw d&&n!==void 0?new Error(`Cloud ${i?"issued the credential, but saving it failed":"may have issued the credential before the request failed"}. Retry the same command and idempotency key with --secret-output pointing to a new file to recover it. Output path: ${n}.`,{cause:s}):s}finally{if(t!==void 0&&!a){const s=c.fstatSync(t,{bigint:!0});c.closeSync(t),!i&&s.size===0n&&n!==void 0&&b(n,s)}}}function b(o,r){try{const e=c.lstatSync(o,{bigint:!0});e.isFile()&&e.size===0n&&e.ino===r.ino&&e.dev===r.dev&&c.unlinkSync(o)}catch(e){if(!p(e)||e.code!=="ENOENT")throw e}}export async function waitForConnection(o,r,e){if(!p(r)||typeof r.operationId!="string"||r.status==="complete")return r;let n=!1;for(;;){e.signal?.throwIfAborted();const t=await O(o,r.operationId);if(t.status==="complete")return t;if(t.status!=="pending"&&t.status!=="processing")throw new Error(`Repository connection ${r.operationId} is ${String(t.status)}. Inspect with cloud show connection ${r.operationId}; retry the create command with a new request key after checking access.`);if(t.status==="pending"&&!n){if(typeof t.verificationUrl!="string")throw u("repository approval link");const i=new URL(t.verificationUrl);if(i.origin!==o.origin||i.username||i.password||i.pathname!==`/cli/projects/${r.operationId}/authorize`)throw u("repository approval link");if(e.notify?.(`Open ${i.href}
|
|
3
|
-
Waiting for repository authorization. Operation: ${r.operationId}
|
|
4
|
-
`),(e.env??process.env).CI)throw new Error(`Repository authorization requires a person. Open ${i.href}, then retry the same request key. Operation: ${r.operationId}`);e.browser!==!1&&C(i.href),n=!0}if((e.env??process.env).CI)throw new Error(`Repository connection ${r.operationId} is processing. Inspect with cloud show connection ${r.operationId}, then retry the same request key to resume.`);if(typeof t.expiresAt!="string"||!Number.isFinite(Date.parse(t.expiresAt)))throw u("connection expiry");if(Date.now()>=Date.parse(t.expiresAt))throw new Error(`Repository connection ${r.operationId} expired. Start a new request.`);await v(2e3,void 0,{signal:e.signal})}}async function O(o,r){const e=await o.request("GET",`/api/v1/project-connections/${encodeURIComponent(r)}`);if(!p(e)||e.operationId!==r||typeof e.status!="string")throw u("repository connection");return e}export async function createCloudProject(o){const r=o.repository??g();if(!r)throw new Error("Pass --repository owner/repo when outside Git or when origin is unavailable.");const e=parseRepository(r,o.host),n=await controlClient(o),t=await resolveOrganisation(n,o.organisation,o.choose),i=o.requestKey??I();o.notify?.(`Request key: ${i}. Reuse --request-key ${i} to resume this operation.
|
|
5
|
-
`);const a=await applyAccountCommand({schemaVersion:1,idempotencyKey:i,operation:"project.create",organisationId:t,input:{name:o.name??e.repository.split("/")[1],repository:e.repository,host:e.host,provider:o.provider??(e.host==="github.com"?"github":"gitea"),publishDecisions:o.publishDecisions??!1}},o);return x(n,"project",a,o.env??process.env)}export function parseRepository(o,r){let e=o.trim(),n="github.com";if(e.includes("://")){const t=new URL(e);if(!["https:","http:","ssh:","git:"].includes(t.protocol)||t.search||t.hash)throw new Error("Repository must be owner/repo or a Git remote URL.");n=t.protocol==="ssh:"||t.protocol==="git:"?t.hostname:t.host,e=t.pathname.replace(/^\//u,"")}else{const t=/^(?:[^@/]+@)?([^/:]+):(.+)$/.exec(e);t?.[1]&&t[2]&&(n=t[1].toLowerCase(),e=t[2])}if(e=e.replace(/\/+$/u,"").replace(/\.git$/u,""),!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(e)||e.split("/").some(t=>t==="."||t===".."))throw new Error("Repository must be owner/repo or a Git remote URL.");return{host:r??n,repository:e}}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { type AccountSession } from "../../cloud/account.js";
|
|
2
|
-
export interface SavedCredential extends AccountSession {
|
|
3
|
-
origin: string;
|
|
4
|
-
token: string;
|
|
5
|
-
}
|
|
6
|
-
export declare function readCredential(origin: string, env?: NodeJS.ProcessEnv): SavedCredential | null;
|
|
7
|
-
export declare function saveCredential(credential: SavedCredential, env?: NodeJS.ProcessEnv): void;
|
|
8
|
-
export declare function removeCredential(origin: string, env?: NodeJS.ProcessEnv): void;
|
|
9
|
-
export declare function cloudToken(origin: string, env?: NodeJS.ProcessEnv, refresh?: boolean): Promise<string>;
|
|
10
|
-
export declare function shouldRefreshCredential(credential: AccountSession, now: number): boolean;
|
|
11
|
-
export declare function canUseCredentialAfterRefreshFailure(credential: AccountSession, error: unknown, now: number): boolean;
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import s from"node:fs";import p from"node:os";import c from"node:path";import{createHash as y,randomUUID as T}from"node:crypto";import{CloudAccountClient as w,cloudOrigin as l,isCloudAccountToken as E,parseSession as O}from"../../cloud/account.js";import{CloudResponseError as h}from"../../cloud/client.js";import{isUnknownRecord as u}from"../../kernel/unknown-values.js";function d(o,e){const r=e.XDG_CONFIG_HOME??(process.platform==="win32"?e.APPDATA:void 0);if(r&&!c.isAbsolute(r))throw new Error("Cloud credential configuration directory must be absolute");const t=r||c.join(p.homedir(),".config"),n=y("sha256").update(l(o)).digest("hex");return c.join(t,"testkit","cloud",`${n}.json`)}function m(o,e){const r=c.dirname(o),t=c.dirname(r),n=c.dirname(t);for(const i of[n,t,r]){let a;try{a=s.lstatSync(i)}catch(f){if(!u(f)||f.code!=="ENOENT")throw f;if(!e)return!1;s.mkdirSync(i,{recursive:i===n,mode:448}),a=s.lstatSync(i)}if(!a.isDirectory()||a.isSymbolicLink())throw new Error("Cloud credential directory must be a real directory, not a symlink");if(i!==n&&process.platform!=="win32"&&(a.mode&63)!==0)throw new Error("Cloud credential directory must be private (mode 0700)")}return!0}export function readCredential(o,e=process.env){const r=d(o,e);if(!m(r,!1))return null;let t;try{t=s.openSync(r,s.constants.O_RDONLY|s.constants.O_NOFOLLOW)}catch(n){if(u(n)&&n.code==="ENOENT")return null;throw n}try{const n=s.fstatSync(t);if(!n.isFile()||n.size>16384||process.platform!=="win32"&&(n.mode&63)!==0)throw new Error("Cloud credential file must be a private regular file");const i=JSON.parse(s.readFileSync(t,"utf8"));if(!u(i)||i.origin!==l(o)||!E(i.token))throw new Error("Invalid Cloud credential file; run testkit cloud login --force");return{...O(i),origin:i.origin,token:i.token}}finally{s.closeSync(t)}}export function saveCredential(o,e=process.env){const r=d(o.origin,e);m(r,!0);const t=`${r}.${T()}.tmp`;try{s.writeFileSync(t,`${JSON.stringify(o)}
|
|
2
|
-
`,{flag:"wx",mode:384}),s.renameSync(t,r)}finally{s.rmSync(t,{force:!0})}}export function removeCredential(o,e=process.env){const r=d(o,e);m(r,!1)&&s.rmSync(r,{force:!0})}export async function cloudToken(o,e=process.env,r=!0){if(e.TESTKIT_CLOUD_TOKEN!==void 0){if(!e.TESTKIT_CLOUD_TOKEN.trim())throw new Error("TESTKIT_CLOUD_TOKEN is empty");return e.TESTKIT_CLOUD_TOKEN}const t=readCredential(o,e);if(!t||Date.parse(t.expiresAt)<=Date.now())throw new Error(e.CI?"Set TESTKIT_CLOUD_TOKEN to an automation token for CI.":`Run testkit cloud login --url ${l(o)} to connect your account, or set TESTKIT_CLOUD_TOKEN for automation.`);if(r&&shouldRefreshCredential(t,Date.now())){let n;try{n=await new w(o,t.token).refresh()}catch(i){if(!canUseCredentialAfterRefreshFailure(t,i,Date.now()))throw i}n&&saveCredential({...t,...n},e)}return t.token}export function shouldRefreshCredential(o,e){const r=Date.parse(o.expiresAt);return r>e&&r-e<10080*6e4&&r<Date.parse(o.maximumExpiresAt)}export function canUseCredentialAfterRefreshFailure(o,e,r){return Date.parse(o.expiresAt)<=r?!1:e instanceof h?e.status>=500||e.status===429:u(e)&&e.code==="cloud_transport_error"}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { type CampaignSelection, type CampaignResult } from "../../cloud/campaigns.js";
|
|
2
|
-
import { CloudClient, type CloudReceipt } from "../../cloud/client.js";
|
|
3
|
-
import type { ControlOptions } from "./control.js";
|
|
4
|
-
export interface CloudEnvironmentOptions extends Pick<ControlOptions, "browser" | "signal" | "secretOutput" | "notify"> {
|
|
5
|
-
env?: NodeJS.ProcessEnv;
|
|
6
|
-
origin?: string;
|
|
7
|
-
projectId?: string;
|
|
8
|
-
campaignId?: string;
|
|
9
|
-
productDir?: string;
|
|
10
|
-
}
|
|
11
|
-
export interface PublishRunToCloudOptions extends CloudEnvironmentOptions {
|
|
12
|
-
productDir: string;
|
|
13
|
-
runId?: null | string;
|
|
14
|
-
}
|
|
15
|
-
export declare function publishRunToCloud(options: Readonly<PublishRunToCloudOptions>): Promise<{
|
|
16
|
-
receipt: CloudReceipt;
|
|
17
|
-
association?: CampaignResult;
|
|
18
|
-
dashboardUrl: string;
|
|
19
|
-
}>;
|
|
20
|
-
export declare function applyCloudMutationFile(filePath: string, options?: Readonly<CloudEnvironmentOptions>): Promise<unknown>;
|
|
21
|
-
export declare function readCloudSnapshot(options?: Readonly<CloudEnvironmentOptions & CampaignSelection>): Promise<unknown>;
|
|
22
|
-
export declare function cloudClientFromEnvironment(options?: Readonly<CloudEnvironmentOptions>): Promise<CloudClient>;
|
package/dist/app/cloud/index.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{associateReceipt as s,checkVerificationSubmission as m,readCampaignReportFile as l}from"./campaigns.js";import{requireCloudId as u,parseCampaignReport as f}from"../../cloud/campaigns.js";import{isUnknownRecord as I}from"../../kernel/unknown-values.js";import c from"node:fs";import a from"node:path";import{prepareVerifiedRunBundleUpload as w}from"../../evidence/index.js";import{CloudClient as g}from"../../cloud/client.js";import{applyAccountCommand as y}from"./control.js";import{selectedCloudOrigin as C}from"./login.js";import{cloudToken as h}from"./credentials.js";export async function publishRunToCloud(e){if(e.campaignId!==void 0&&(u(e.campaignId,"campaignId"),!e.runId))throw new Error("Campaign publication requires an explicit --run ID");const r=w(e.productDir,e.runId??null),n=await cloudClientFromEnvironment(e),t=await n.publish(r,i=>{e.notify?.(i)});j(e.productDir,r.runId,t);const o=e.campaignId===void 0?void 0:await s(n,t,e.campaignId);return{receipt:t,dashboardUrl:new URL(`/receipts/${encodeURIComponent(t.id)}`,n.origin).href,...o?{association:o}:{}}}export async function applyCloudMutationFile(e,r={}){if(r.campaignId!==void 0){if(r.secretOutput!==void 0)throw new Error("--secret-output is only supported for token.create.");const d=await cloudClientFromEnvironment(r),p=f(l(e),d.projectId,u(r.campaignId,"campaignId"));return await m(d,p,a.resolve(r.productDir??process.cwd())),d.report(p)}const n=a.resolve(e),t=JSON.parse(c.readFileSync(n,"utf8"));if(!I(t))throw new Error("Cloud mutation file must contain a JSON object");if(typeof t.operation=="string")return y(t,r);if(r.secretOutput!==void 0)throw new Error("--secret-output is only supported for token.create.");const o=await cloudClientFromEnvironment(r),i={...t,projectId:typeof t.projectId=="string"?t.projectId:o.projectId};if(i.projectId!==o.projectId)throw new Error("Cloud mutation file projectId does not match TESTKIT_CLOUD_PROJECT_ID");return o.apply(i)}export async function readCloudSnapshot(e={}){return(await cloudClientFromEnvironment(e)).snapshot(e)}export async function cloudClientFromEnvironment(e={}){const r=e.env??process.env,n=C(e);return new g({origin:n,projectId:e.projectId??r.TESTKIT_CLOUD_PROJECT_ID??"",token:await h(n,r)})}function j(e,r,n){const t=a.join(e,".testkit","cloud","receipts");c.mkdirSync(t,{recursive:!0});const o=a.join(t,`${r}.json`),i=`${o}.${String(process.pid)}.${String(Date.now())}.tmp`;c.writeFileSync(i,`${JSON.stringify(n,null,2)}
|
|
2
|
-
`,{mode:384}),c.renameSync(i,o)}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
export interface LoginOptions {
|
|
2
|
-
origin?: string;
|
|
3
|
-
env?: NodeJS.ProcessEnv;
|
|
4
|
-
force?: boolean;
|
|
5
|
-
browser?: boolean;
|
|
6
|
-
signal?: AbortSignal;
|
|
7
|
-
notify: (message: string) => void;
|
|
8
|
-
}
|
|
9
|
-
export declare function selectedCloudOrigin(options: {
|
|
10
|
-
origin?: string;
|
|
11
|
-
env?: NodeJS.ProcessEnv;
|
|
12
|
-
}): string;
|
|
13
|
-
export declare function loginToCloud(options: LoginOptions): Promise<{
|
|
14
|
-
user: {
|
|
15
|
-
id: string;
|
|
16
|
-
email: string;
|
|
17
|
-
name: string;
|
|
18
|
-
};
|
|
19
|
-
expiresAt: string;
|
|
20
|
-
maximumExpiresAt: string;
|
|
21
|
-
origin: string;
|
|
22
|
-
reused: boolean;
|
|
23
|
-
}>;
|
|
24
|
-
export declare function cloudLoginStatus(options: {
|
|
25
|
-
origin?: string;
|
|
26
|
-
env?: NodeJS.ProcessEnv;
|
|
27
|
-
}): Promise<{
|
|
28
|
-
origin: string;
|
|
29
|
-
source: "environment";
|
|
30
|
-
authenticated: null;
|
|
31
|
-
message: string;
|
|
32
|
-
} | {
|
|
33
|
-
message?: undefined;
|
|
34
|
-
origin: string;
|
|
35
|
-
source: "none";
|
|
36
|
-
authenticated: boolean;
|
|
37
|
-
} | {
|
|
38
|
-
user: {
|
|
39
|
-
id: string;
|
|
40
|
-
email: string;
|
|
41
|
-
name: string;
|
|
42
|
-
};
|
|
43
|
-
expiresAt: string;
|
|
44
|
-
maximumExpiresAt: string;
|
|
45
|
-
message?: undefined;
|
|
46
|
-
origin: string;
|
|
47
|
-
source: "account";
|
|
48
|
-
authenticated: boolean;
|
|
49
|
-
} | {
|
|
50
|
-
message?: undefined;
|
|
51
|
-
origin: string;
|
|
52
|
-
source: "account";
|
|
53
|
-
authenticated: boolean;
|
|
54
|
-
}>;
|
|
55
|
-
export declare function logoutOfCloud(options: {
|
|
56
|
-
origin?: string;
|
|
57
|
-
env?: NodeJS.ProcessEnv;
|
|
58
|
-
}): Promise<{
|
|
59
|
-
origin: string;
|
|
60
|
-
loggedOut: boolean;
|
|
61
|
-
environmentTokenActive: boolean;
|
|
62
|
-
}>;
|
|
63
|
-
export declare function openBrowser(url: string): void;
|
package/dist/app/cloud/login.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{spawn as p}from"node:child_process";import{setTimeout as w}from"node:timers/promises";import{CloudAccountClient as c,cloudOrigin as v}from"../../cloud/account.js";import{CloudResponseError as l}from"../../cloud/client.js";import{readCredential as u,removeCredential as g,saveCredential as h,canUseCredentialAfterRefreshFailure as C}from"./credentials.js";import{isUnknownRecord as x}from"../../kernel/unknown-values.js";export function selectedCloudOrigin(e){return v(e.origin??(e.env??process.env).TESTKIT_CLOUD_URL??"https://testkit.cloud")}export async function loginToCloud(e){const t=selectedCloudOrigin(e),o=e.env??process.env;let n;try{n=u(t,o)}catch(r){if(!e.force)throw r;n=null}if(n&&!e.force)try{const r=new c(t,n.token),m=Date.parse(n.expiresAt)===Date.parse(n.maximumExpiresAt)?await r.session():await r.refresh();return h({...n,...m},o),{origin:t,...m,reused:!0}}catch(r){if(C(n,r,Date.now()))return e.notify("Cloud session refresh is temporarily unavailable. Using the saved, unexpired session."),{origin:t,user:n.user,expiresAt:n.expiresAt,maximumExpiresAt:n.maximumExpiresAt,reused:!0};if(!(r instanceof l)||r.status!==401)throw r}const i=new c(t),a=await i.login();e.notify(`Open ${a.verificationUrl}
|
|
2
|
-
Check code ${a.userCode}, then choose Allow CLI access. Waiting for approval...`),e.browser!==!1&&!o.CI&&openBrowser(a.verificationUrl);const d=Date.now()+a.expiresIn*1e3;let s=a.interval*1e3,f=!1;try{for(;Date.now()<d;){e.signal?.throwIfAborted();try{const r=await i.poll(a.deviceCode);return e.signal?.throwIfAborted(),h({...r,origin:t,token:a.deviceCode},o),f=!0,n&&await i.revoke(n.token).catch(()=>{e.notify("The previous Cloud session could not be revoked. It will expire automatically.")}),{origin:t,...r,reused:!1}}catch(r){if(r instanceof l&&r.status>=500||x(r)&&r.code==="cloud_transport_error"){await w(Math.min(s,Math.max(1,d-Date.now())),void 0,{signal:e.signal});continue}if(!(r instanceof l)||!["authorization_pending","slow_down"].includes(r.code??""))throw r;r.code==="slow_down"&&(s=Math.min(s+1e3,3e4))}await w(Math.min(s,Math.max(1,d-Date.now())),void 0,{signal:e.signal})}throw new Error("CLI login expired. Run testkit cloud login again.")}finally{f||await i.revoke(a.deviceCode).catch(()=>{})}}export async function cloudLoginStatus(e){const t=selectedCloudOrigin(e),o=e.env??process.env;if(o.TESTKIT_CLOUD_TOKEN!==void 0)return{origin:t,source:"environment",authenticated:null,message:"Commands use TESTKIT_CLOUD_TOKEN. Select a project to check its permissions."};const n=u(t,o);if(!n)return{origin:t,source:"none",authenticated:!1};try{return{origin:t,source:"account",authenticated:!0,...await new c(t,n.token).session()}}catch(i){if(!(i instanceof l)||i.status!==401)throw i;return{origin:t,source:"account",authenticated:!1}}}export async function logoutOfCloud(e){const t=selectedCloudOrigin(e),o=e.env??process.env,n=u(t,o);return n&&(await new c(t).revoke(n.token),g(t,o)),{origin:t,loggedOut:!0,environmentTokenActive:o.TESTKIT_CLOUD_TOKEN!==void 0}}export function openBrowser(e){const t=process.platform==="darwin"?"open":process.platform==="win32"?"rundll32":"xdg-open",o=process.platform==="win32"?["url.dll,FileProtocolHandler",e]:[e],n=p(t,o,{detached:!0,stdio:"ignore",shell:!1});n.on("error",()=>{}),n.unref()}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { MigrationPlan, PlannedRun } from "./model.js";
|
|
2
|
-
interface ArchiveIdentity {
|
|
3
|
-
bytes: number;
|
|
4
|
-
sha256: string;
|
|
5
|
-
}
|
|
6
|
-
/** The migration output lock and sequential run transfer own this single archive slot. */
|
|
7
|
-
export declare function prepareMigrationArchive(plan: MigrationPlan, run: PlannedRun, maxBytes: number, expected?: ArchiveIdentity): Promise<{
|
|
8
|
-
path: string;
|
|
9
|
-
bytes: number;
|
|
10
|
-
sha256: string;
|
|
11
|
-
}>;
|
|
12
|
-
export declare function removeMigrationArchive(plan: MigrationPlan, runId: string): Promise<void>;
|
|
13
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import p from"node:crypto";import r from"node:fs";import a from"node:path";import{writeUploadArchive as l}from"../../../cloud/archive.js";import{canonicalHistoricalJson as w}from"../../../cloud/historical-json.js";import{digest as S}from"./files.js";function u(o,t){const n=r.lstatSync(o.outputDir);if(!n.isDirectory()||n.isSymbolicLink())throw new Error("Migration output directory is unsafe");const i=a.join(o.outputDir,"archives");r.existsSync(i)||r.mkdirSync(i,{mode:448});const e=r.lstatSync(i);if(!e.isDirectory()||e.isSymbolicLink())throw new Error("Migration archive directory is unsafe");return a.join(i,`${S(t)}.zip`)}function m(o){if(process.platform==="win32")return;const t=r.openSync(o,"r");try{r.fsyncSync(t)}finally{r.closeSync(t)}}async function d(o,t,n){if(t.bytes>n)throw new Error("Journaled migration archive exceeds the upload limit");try{const i=r.lstatSync(o);if(!i.isFile()||i.isSymbolicLink()||i.size!==t.bytes)throw new Error("Archive size or file type changed");const e=r.openSync(o,r.constants.O_RDONLY|r.constants.O_NOFOLLOW),f=r.createReadStream(o,{fd:e}),c=p.createHash("sha256");let s=0;for await(const y of f){const h=y;if(s+=h.length,s>t.bytes)throw new Error("Archive grew during verification");c.update(h)}if(s!==t.bytes||c.digest("hex")!==t.sha256)throw new Error("Archive SHA-256 or size changed")}catch(i){throw new Error("Journaled migration archive is missing or corrupt; restore the original archive before resuming",{cause:i})}}export async function prepareMigrationArchive(o,t,n,i){const e=u(o,t.runId),f=a.dirname(e),c=`${e}.tmp`;if(r.readdirSync(f).some(s=>s!==a.basename(e)&&s!==a.basename(c)))throw new Error("Another migration archive occupies the active run slot; resume its run before packaging another");if(i)return await d(e,i,n),{path:e,...i};r.rmSync(e,{force:!0}),r.rmSync(c,{force:!0});try{const s=await l({manifest:t.request.manifest,files:t.files},n,c,w),y=r.openSync(c,r.constants.O_RDWR|r.constants.O_NOFOLLOW);try{r.fsyncSync(y)}finally{r.closeSync(y)}return r.renameSync(c,e),m(f),m(o.outputDir),{path:e,...s}}finally{r.rmSync(c,{force:!0})}}export async function removeMigrationArchive(o,t){const n=u(o,t);await r.promises.rm(n,{force:!0}),await r.promises.rm(`${n}.tmp`,{force:!0}),m(a.dirname(n))}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { type HistoricalBegin, type HistoricalRecord } from "../../../cloud/historical-contract.js";
|
|
2
|
-
import type { MigrationPlan, PlannedRecord } from "./model.js";
|
|
3
|
-
import type { MigrationJournal } from "./state.js";
|
|
4
|
-
export declare function dependencyOrder(records: PlannedRecord[]): PlannedRecord[];
|
|
5
|
-
/** The upper version bound makes chunk boundaries independent of resumed state. */
|
|
6
|
-
export declare function batchRecords(records: HistoricalRecord[]): HistoricalRecord[][];
|
|
7
|
-
export declare function beginCommand(plan: MigrationPlan, planDigest: string): HistoricalBegin;
|
|
8
|
-
export declare function plannedRecords(plan: MigrationPlan, journal: MigrationJournal, kind: "case" | "finding"): HistoricalRecord[];
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{HISTORICAL_BODY_LIMIT as u}from"../../../cloud/historical-contract.js";import{canonicalHistoricalJson as f,historicalDigest as p}from"../../../cloud/historical-json.js";import{expectedFindingData as h}from"./reconcile.js";export function dependencyOrder(i){const s=new Map(i.map(o=>[o.source.id,o])),n=new Set,t=new Set,e=[],c=o=>{const r=o.source.id;if(n.has(r))return;if(t.has(r))throw new Error(`Cyclic finding relationship: ${r}`);t.add(r);const a=o.relationship?.targetId,d=a?s.get(a):void 0;d&&c(d),t.delete(r),n.add(r),e.push(o)};return i.forEach(c),e}export function batchRecords(i){const s=[];let n=[];const t=e=>e.length<=25&&Buffer.byteLength(f({schemaVersion:1,idempotencyKey:"x".repeat(128),expectedVersion:Number.MAX_SAFE_INTEGER,records:e}))<=u;for(const e of i){if(!t([e]))throw new Error(`Record cannot fit a historical batch: ${e.source.id}`);t([...n,e])||(s.push(n),n=[]),n.push(e)}return n.length&&s.push(n),s}export function beginCommand(i,s){const n=[...i.runs.filter(t=>!Object.hasOwn(i.reusedRuns,t.runId)).map(t=>t.request.source),...i.records.map(t=>t.source)];return{schemaVersion:1,idempotencyKey:`migration-${s}`,namespace:i.namespace,inventoryDigest:p(n),inventory:n}}export function plannedRecords(i,s,n){const t=i.records.filter(e=>e.source.kind===n);return(n==="finding"?dependencyOrder(t):t).map(e=>({source:e.source,sourceRecord:e.sourceRecord,observedAt:e.observedAt,...e.case?{case:e.case}:{finding:h(e,s)},...e.relationship?{relationship:e.relationship}:{},...e.verify!==void 0?{verify:e.verify}:{}}))}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { MigrationMapping, PlannedRun, SourceFile } from "./model.js";
|
|
2
|
-
export declare function referencedRuns(...documents: unknown[]): string[];
|
|
3
|
-
export declare function verifyCitedHashes(value: unknown, sourceFiles: SourceFile[]): void;
|
|
4
|
-
export declare function prepareHistoricalRun(options: {
|
|
5
|
-
sourceDir: string;
|
|
6
|
-
outputDir: string;
|
|
7
|
-
runId: string;
|
|
8
|
-
mapping: MigrationMapping;
|
|
9
|
-
sourceFiles: SourceFile[];
|
|
10
|
-
env: NodeJS.ProcessEnv;
|
|
11
|
-
}): PlannedRun;
|
|
12
|
-
/** Keep a cited source manifest distinct from the manifest describing derived files. */
|
|
13
|
-
export declare function includeOriginalManifest(run: PlannedRun, sourceDir: string, outputDir: string, sourceFiles: SourceFile[], env: NodeJS.ProcessEnv): void;
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import $ from"node:fs";import H from"node:path";import{canonicalHistoricalJson as R,historicalDigest as F,HistoricalJsonNumber as Z,parseHistoricalJson as J}from"../../../cloud/historical-json.js";import"../../../cloud/historical-contract.js";import{stableJson as q}from"../../../evidence/storage.js";import{array as O,digest as w,object as f,readSource as P,readSourceJson as W,sanitize as U,text as b}from"./files.js";export function referencedRuns(...e){const i=new Set;function n(t){if(typeof t=="string")for(const c of t.matchAll(/\.testkit\/results\/runs\/([A-Za-z0-9._-]+)\//gu))i.add(c[1]);else if(Array.isArray(t))t.forEach(n);else if(t&&typeof t=="object")for(const[c,m]of Object.entries(t)){if(c==="runId")i.add(b(m,"runId",256));else if(c.toLowerCase().endsWith("runids"))for(const l of O(m,c))i.add(b(l,c,256));n(m)}}if(e.forEach(n),i.size>1e4)throw new Error("Referenced run inventory exceeds 10,000 entries");for(const t of i)if(!/^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/u.test(t))throw new Error("Unsafe original run identity");return[...i].sort()}function B(e,i){if(e instanceof Z){if(!/^(?:0|[1-9]\d*)$/u.test(e.lexeme))throw new Error(`${i} must be an integer`);e=Number(e.lexeme)}if(typeof e!="number"||!Number.isSafeInteger(e)||e<0)throw new Error(`${i} must be a bounded integer`);return e}function z(e,i){const n=b(e,i,128);if(!/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,9})?(?:Z|[+-]\d\d:\d\d)$/u.test(n)||!Number.isFinite(Date.parse(n)))throw new Error(`Invalid historical ${i}`);return n}export function verifyCitedHashes(e,i){if(Array.isArray(e)){e.forEach(t=>{verifyCitedHashes(t,i)});return}if(!e||typeof e!="object")return;const n=e;if(typeof n.path=="string"&&n.path.startsWith(".testkit/results/runs/")&&typeof n.sha256=="string"&&i.find(t=>t.path===n.path)?.sha256!==n.sha256)throw new Error(`Cited artifact hash does not match retained evidence: ${n.path}`);Object.values(n).forEach(t=>{verifyCitedHashes(t,i)})}function T(e,i){let n;if(e.repository)n=f(e.repository,"git.repository");else{const t=new URL(b(e.remoteUrl,"git.remoteUrl"));if(!["https:","http:"].includes(t.protocol)||t.search||t.hash)throw new Error("Historical remote URL requires explicit repository mapping");const c=t.pathname.replace(/\.git$/u,"").split("/").filter(Boolean);if(c.length!==2)throw new Error("Historical repository path is ambiguous");n={host:t.host,owner:c[0],name:c[1]}}for(const t of["host","owner","name"])if(b(n[t],`git.repository.${t}`).toLowerCase()!==i[t].toLowerCase())throw new Error("Historical repository does not match the selected repository mapping");e.repository=i,delete e.remoteUrl,delete e.repoRoot,delete e.worktreePath}export function prepareHistoricalRun(e){const{sourceDir:i,outputDir:n,runId:t,mapping:c,sourceFiles:m,env:l}=e,j=`.testkit/results/runs/${t}`,I=W(i,`${j}/manifest.json`,m,8*1024**2),x=f(I,"manifest"),a={...x,schemaVersion:B(x.schemaVersion,"manifest schemaVersion")};if(a.runId!==t||a.algorithm!=="sha256"||![1,2].includes(a.schemaVersion))throw new Error("Unsupported historical manifest schema or identity");const p=O(a.files,"manifest.files").map(r=>{const s=f(r,"manifest entry"),d=b(s.path,"manifest path"),D=B(s.bytes,"manifest entry bytes");if(d==="manifest.json"||!/^[a-f0-9]{64}$/u.test(String(s.sha256)))throw new Error("Invalid manifest entry");return{path:d,bytes:D,sha256:String(s.sha256)}});if(p.length<2||p.length>1e4||new Set(p.map(r=>r.path)).size!==p.length)throw new Error("Invalid or duplicate manifest entries");if(w(q(p))!==a.digest)throw new Error("Original manifest file-list digest mismatch");if(p.reduce((r,s)=>r+s.bytes,0)>2*1024**3)throw new Error("Historical bundle exceeds 2 GiB");const u=[],V=[];let E="",g;for(const r of p.sort((s,d)=>s.path==="run.json"?-1:d.path==="run.json"?1:s.path.localeCompare(d.path))){const s=P(i,`${j}/${r.path}`,m);if(s.length!==r.bytes||w(s)!==r.sha256)throw new Error(`Evidence hash/size mismatch: ${r.path}`);let d=s;const D=[];if(r.path==="run.json"||r.path==="provenance.json"){if(s.length>8*1024**2)throw new Error("Historical run/provenance exceeds 8 MiB");const o=f(J(new TextDecoder("utf-8",{fatal:!0}).decode(s)),r.path);if(r.path==="run.json"){const h=f(o.run,"run");if(h.id!==t||B(o.schemaVersion,"run schemaVersion")!==(a.schemaVersion===1?16:19))throw new Error("Unsupported historical run schema or identity");const v=z(h.startedAt,"startedAt"),S=h.finishedAt==null?v:z(h.finishedAt,"finishedAt"),A=Date.parse(v),L=Date.parse(S);if(o.generatedAt!==void 0&&o.generatedAt!==""&&z(o.generatedAt,"generatedAt"),!Number.isFinite(A)||!Number.isFinite(L)||L<A||A>Date.now())throw new Error("Invalid historical observation times");if(!["passed","failed","cancelled","interrupted","incomplete"].includes(String(h.status)))throw new Error("Unsupported historical run status");E=S;const N=f(o.git,"run.git");T(N,c.repository),g=N,o.product&&delete f(o.product,"product").directory,delete o.host}else{if(o.runId!==t||B(o.schemaVersion,"provenance schemaVersion")!==(a.schemaVersion===1?1:3))throw new Error("Unsupported historical provenance schema or identity");const h=f(o.source,"provenance.source");if(delete h.root,h.git){const S=f(h.git,"provenance.source.git");T(S,c.repository);for(const A of["branch","commitSha"])if(S[A]!=null&&S[A]!==g?.[A])throw new Error("Historical provenance disagrees with run identity")}const v=o.runtime?f(o.runtime,"runtime"):void 0;v&&delete v.hostname,v?.controller&&delete f(v.controller,"controller").hostname}if(d=Buffer.from(R(U(o,l))+`
|
|
2
|
-
`),d.length>8*1024**2)throw new Error("Derived run/provenance exceeds 8 MiB");D.push("Normalize repository from original metadata; remove machine paths and hostname; sanitize text; canonical JSON; preserve original schema, run identity and observation times")}else try{const o=new TextDecoder("utf-8",{fatal:!0}).decode(s);if(!o.includes("\0")){const h=String(U(o,l));h!==o&&(d=Buffer.from(h),D.push("Redact sensitive text and the configured Cloud credential"))}}catch{}const M=H.join(n,"evidence",t,r.path);let k=n;for(const o of["evidence",t,...r.path.split("/").slice(0,-1)]){k=H.join(k,o),$.existsSync(k)||$.mkdirSync(k,{mode:448});const h=$.lstatSync(k);if(h.isSymbolicLink()||!h.isDirectory())throw new Error("Derived evidence directory must not contain symlinks or files")}$.writeFileSync(M,d,{mode:384,flag:"wx"}),u.push({path:r.path,absolutePath:M,bytes:d.length,sha256:w(d)}),V.push({path:r.path,originalPath:`${j}/${r.path}`,originalBytes:r.bytes,originalSha256:r.sha256,...d.equals(s)?{}:{transformation:D.join("; ")}})}if(!u.some(r=>r.path==="run.json")||!u.some(r=>r.path==="provenance.json"))throw new Error("Historical bundle lacks run.json or provenance.json");const y={runId:t,observedAt:E,originalManifestSha256:b(m.find(r=>r.path===`${j}/manifest.json`)?.sha256,"original manifest SHA256",64),originalManifestDigest:b(a.digest,"manifest.digest",64),converterVersion:"testkit-legacy-v1",schemaTuple:{manifest:B(a.schemaVersion,"manifest.schemaVersion"),run:a.schemaVersion===1?16:19,provenance:a.schemaVersion===1?1:3}},C=u.map(({path:r,bytes:s,sha256:d})=>({path:r,bytes:s,sha256:d})).sort((r,s)=>r.path.localeCompare(s.path)),_={algorithm:"sha256",schemaVersion:a.schemaVersion,runId:t,createdAt:b(a.createdAt,"manifest.createdAt"),files:C,digest:w(q(C)),redaction:U(a.redaction??{},l),storage:{fileCount:u.length,logicalBytes:u.reduce((r,s)=>r+s.bytes,0),deduplicatedBytes:0,objectBytesWritten:0}};return{runId:t,observedAt:E,request:{schemaVersion:1,source:{kind:"run",id:Buffer.byteLength(t)<=128?t:`run_${w(t)}`,digest:F(y)},sourceRecord:y,manifest:_,artifacts:V},files:u}}export function includeOriginalManifest(e,i,n,t,c){const m=`.testkit/results/runs/${e.runId}/manifest.json`,l=P(i,m,t,8*1024**2);if(w(l)!==e.request.sourceRecord.originalManifestSha256)throw new Error(`Original manifest changed during planning: ${e.runId}`);const j=J(new TextDecoder("utf-8",{fatal:!0}).decode(l)),I=U(j,c),x=R(j)!==R(I),a=x?Buffer.from(R(I)):l;if(e.files.length>=1e4||e.files.reduce((g,y)=>g+y.bytes,a.length)>2*1024**3)throw new Error(`Cited original manifest exceeds derived evidence limits: ${e.runId}`);const p=H.join(n,"evidence",e.runId),u=$.lstatSync(p);if(!u.isDirectory()||u.isSymbolicLink())throw new Error("Derived evidence directory is unsafe");const V=H.join(p,"manifest.json");$.writeFileSync(V,a,{mode:384,flag:"wx"}),e.files.push({path:"manifest.json",absolutePath:V,bytes:a.length,sha256:w(a)}),e.request.artifacts.push({path:"manifest.json",originalPath:m,originalBytes:l.length,originalSha256:w(l),...x?{transformation:"Redact sensitive original manifest fields and Cloud credential"}:{}});const E=e.files.map(({path:g,bytes:y,sha256:C})=>({path:g,bytes:y,sha256:C})).sort((g,y)=>g.path.localeCompare(y.path));e.request.manifest.files=E,e.request.manifest.digest=w(q(E)),e.request.manifest.storage={...f(e.request.manifest.storage,"derived manifest storage"),fileCount:E.length,logicalBytes:E.reduce((g,y)=>g+y.bytes,0)}}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import type { SourceFile } from "./model.js";
|
|
3
|
-
export declare const digest: (bytes: crypto.BinaryLike) => string;
|
|
4
|
-
export declare function object(value: unknown, label: string): Record<string, unknown>;
|
|
5
|
-
export declare function text(value: unknown, label: string, max?: number): string;
|
|
6
|
-
export declare function array(value: unknown, label: string): unknown[];
|
|
7
|
-
export declare function plain(value: unknown): unknown;
|
|
8
|
-
export declare function contained(root: string, relative: string): string;
|
|
9
|
-
export declare function readBoundedFile(filename: string, maxBytes?: number): Buffer;
|
|
10
|
-
export declare function readSource(root: string, relative: string, inventory: SourceFile[], maxBytes?: number): Buffer;
|
|
11
|
-
export declare function readSourceJson(root: string, relative: string, inventory: SourceFile[], maxBytes?: number): unknown;
|
|
12
|
-
export declare function sanitize(value: unknown, env: NodeJS.ProcessEnv): unknown;
|
|
13
|
-
export declare function writeDocument(filename: string, value: unknown): void;
|
|
14
|
-
export declare function readDocument(filename: string): unknown;
|
|
15
|
-
export declare function separateOutput(source: string, output: string): void;
|
|
16
|
-
export declare function withOutputLock<T>(output: string, work: () => Promise<T>): Promise<T>;
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import y from"node:crypto";import i from"node:fs";import d from"node:os";import u from"node:path";import{canonicalHistoricalJson as m,parseHistoricalJson as h,HistoricalJsonNumber as l}from"../../../cloud/historical-json.js";import{redactSensitiveText as w}from"../../../kernel/redaction.js";export const digest=r=>y.createHash("sha256").update(r).digest("hex");export function object(r,n){if(!r||typeof r!="object"||Array.isArray(r))throw new Error(`${n} must be an object`);return r}export function text(r,n,e=4096){if(typeof r!="string"||!r.trim()||r.includes("\0")||Buffer.byteLength(r)>e)throw new Error(`${n} must be a nonblank string of at most ${String(e)} bytes`);return r}export function array(r,n){if(!Array.isArray(r))throw new Error(`${n} must be an array`);return r}export function plain(r){if(r instanceof l){const n=Number(r.lexeme);if(!Number.isFinite(n)||Number.isInteger(n)&&!Number.isSafeInteger(n))throw new Error("Numeric field exceeds supported range");return n}return Array.isArray(r)?r.map(plain):r&&typeof r=="object"?Object.fromEntries(Object.entries(r).map(([n,e])=>[n,plain(e)])):r}export function contained(r,n){if(u.isAbsolute(n)||u.win32.isAbsolute(n)||/[\\:\0]/u.test(n)||n.split("/").some(o=>!o||o==="."||o===".."))throw new Error("Source reference must be a normalized relative path");const e=u.resolve(r,n);let t=r;for(const o of n.split("/"))if(t=u.join(t,o),i.lstatSync(t).isSymbolicLink())throw new Error(`Symlink is not allowed: ${n}`);if(!i.statSync(e).isFile())throw new Error(`Source reference is not a file: ${n}`);return e}export function readBoundedFile(r,n=128*1024**2){const e=i.lstatSync(r);if(!e.isFile()||e.isSymbolicLink()||e.size>n)throw new Error("File must be regular and within its byte limit");const t=i.openSync(r,i.constants.O_RDONLY|i.constants.O_NOFOLLOW);try{const o=i.fstatSync(t);if(!o.isFile()||o.ino!==e.ino||o.dev!==e.dev||o.size!==e.size)throw new Error("Source changed during read");const s=Buffer.alloc(e.size+1);let c=0;for(;c<s.length;){const f=i.readSync(t,s,c,s.length-c,null);if(!f)break;c+=f}const a=i.fstatSync(t);if(c!==e.size||a.size!==e.size||a.mtimeMs!==e.mtimeMs||a.ctimeMs!==e.ctimeMs)throw new Error("Source changed during read");return s.subarray(0,c)}finally{i.closeSync(t)}}export function readSource(r,n,e,t=128*1024**2){const o=contained(r,n),s=readBoundedFile(o,t),c={path:n,bytes:s.length,sha256:digest(s)},a=e.find(f=>f.path===n);if(a&&(a.sha256!==c.sha256||a.bytes!==c.bytes))throw new Error(`Source changed during inventory: ${n}`);return a||e.push(c),s}export function readSourceJson(r,n,e,t=32*1024**2){return h(new TextDecoder("utf-8",{fatal:!0}).decode(readSource(r,n,e,t)),{maxBytes:t})}export function sanitize(r,n){const e=n.TESTKIT_CLOUD_TOKEN;function t(o){if(typeof o=="string"){const s=o.replace(/[a-z][a-z0-9+.-]*:\/\/[^\s"'<>]+/giu,a=>{try{const f=new URL(a);return f.username="",f.password="",f.search="",f.hash="",f.toString()}catch{return"<redacted-url>"}});let c=w(s).replace(/\bBearer\s+[^\s"']+/giu,"Bearer <redacted>").replace(/\btkc_cli_[A-Za-z0-9_-]{43}\b/gu,"<redacted>");return e&&(e.length>=12?c=c.replaceAll(e,"<redacted>"):c===e&&(c="<redacted>")),c}return o instanceof l?o:Array.isArray(o)?o.map(t):o&&typeof o=="object"?Object.fromEntries(Object.entries(o).map(([s,c])=>[s,/^(?:password|(?:access[_-]?|refresh[_-]?)?token|(?:client[_-]?)?secret|private[_-]?key|authorization|cookie|session|api[_-]?key)$/iu.test(s)?"<redacted>":t(c)])):o}return t(r)}export function writeDocument(r,n){const e=m(n)+`
|
|
2
|
-
`;if(Buffer.byteLength(e)>128*1024**2)throw new Error("Migration document exceeds 128 MiB");i.mkdirSync(u.dirname(r),{recursive:!0,mode:448});const t=`${r}.${y.randomUUID()}.tmp`,o=i.openSync(t,"wx",384);try{try{i.writeFileSync(o,e),i.fsyncSync(o)}finally{i.closeSync(o)}i.renameSync(t,r)}finally{i.rmSync(t,{force:!0})}if(process.platform!=="win32"){const s=i.openSync(u.dirname(r),"r");try{i.fsyncSync(s)}finally{i.closeSync(s)}}}export function readDocument(r){const n=h(new TextDecoder("utf-8",{fatal:!0}).decode(readBoundedFile(r)),{maxBytes:134217728});function e(t,o=""){return o==="sourceRecord"||o==="record"?t:t instanceof l?plain(t):Array.isArray(t)?t.map(s=>e(s)):t&&typeof t=="object"?Object.fromEntries(Object.entries(t).map(([s,c])=>[s,e(c,s)])):t}return e(n)}export function separateOutput(r,n){const e=i.realpathSync(r);let t=u.resolve(n);const o=[];for(;!i.existsSync(t);)o.unshift(u.basename(t)),t=u.dirname(t);const s=u.join(i.realpathSync(t),...o);for(const[c,a]of[[e,s],[s,e]]){const f=u.relative(c,a);if(!f||!f.startsWith(`..${u.sep}`)&&f!==".."&&!u.isAbsolute(f))throw new Error("Migration output must be outside and separate from the source root")}}export async function withOutputLock(r,n){i.mkdirSync(r,{recursive:!0,mode:448});const e=u.join(r,".migration-lock"),t=u.join(r,".migration-lock-acquire");let o;try{o=i.openSync(t,"wx",384)}catch(a){throw object(a,"lock acquisition error").code==="EEXIST"?new Error("Migration lock acquisition is already running. If its process crashed, verify no migration is acquiring this directory before manually removing .migration-lock-acquire",{cause:a}):a}const s=i.fstatSync(o),c=y.randomUUID();try{if(i.writeFileSync(o,JSON.stringify({pid:process.pid,host:d.hostname(),owner:c})),i.existsSync(e)){const f=object(JSON.parse(readBoundedFile(e,4096).toString("utf8")),"lock");let p=!1;if(f.host===d.hostname()&&Number.isSafeInteger(f.pid)&&Number(f.pid)>0)try{process.kill(Number(f.pid),0)}catch(S){p=object(S,"process error").code==="ESRCH"}if(!p)throw new Error("Migration output is locked by another process");i.unlinkSync(e)}const a=i.openSync(e,"wx",384);try{i.writeFileSync(a,JSON.stringify({pid:process.pid,host:d.hostname(),owner:c}))}finally{i.closeSync(a)}}finally{if(i.closeSync(o),i.existsSync(t)){const a=i.lstatSync(t);a.ino===s.ino&&a.dev===s.dev&&i.unlinkSync(t)}}try{return await n()}finally{i.existsSync(e)&&object(JSON.parse(readBoundedFile(e,4096).toString("utf8")),"lock").owner===c&&i.unlinkSync(e)}}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { type MigrationProgressCallback } from "./progress.js";
|
|
2
|
-
export type { MigrationProgress, MigrationProgressCallback } from "./progress.js";
|
|
3
|
-
import type { MigrationResult } from "./model.js";
|
|
4
|
-
export { planMigration } from "./plan.js";
|
|
5
|
-
export declare function applyMigration(planPath: string, env?: NodeJS.ProcessEnv, onProgress?: MigrationProgressCallback): Promise<MigrationResult>;
|
|
6
|
-
export declare function reconcileMigration(planPath: string, env?: NodeJS.ProcessEnv, onProgress?: MigrationProgressCallback): Promise<MigrationResult>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import w from"node:path";import{cloudToken as v}from"../credentials.js";import{HistoricalClient as O}from"../../../cloud/historical.js";import{sanitize as h,withOutputLock as $,writeDocument as j}from"./files.js";import{reconcileState as E}from"./reconcile.js";import{loadPlan as M,openJournal as k,performOperation as q,verifyPlanFiles as P,upgradeJournal as x}from"./state.js";import{transfer as R}from"./transfer.js";import{operationIs as V}from"./journal.js";import{createMigrationProgress as C}from"./progress.js";export{planMigration}from"./plan.js";export function applyMigration(m,i=process.env,c){return S(m,i,!0,c)}export function reconcileMigration(m,i=process.env,c){return S(m,i,!1,c)}async function S(m,i,c,r){const n=C(r,i),u=w.resolve(m),a=w.join(w.dirname(u),"report.json"),o={complete:!1,planPath:u,reportPath:a,problems:[]};try{n.emit("source-verification",{operation:"source-verification"});const{plan:t,planDigest:y}=M(u);return n.emit("source-verification",{totalRuns:t.runs.length,operation:"source-verification"}),await $(t.outputDir,async()=>{let d={planDigest:y,target:t.target,sourceSummary:t.sourceSummary,dispositions:t.dispositions??[]};try{if(P(t),!t.ready||t.problems.length)throw o.problems=t.problems,new Error("Migration plan has unresolved decisions");const e=k(t,y),s=new O({...t.target,token:await v(t.target.origin,i)});if(n.emit("source-verification",{completedRuns:Object.keys(e.receiptIds).length,operation:"source-verification"}),c){const l=e.schemaVersion===1;x(t,e),l&&e.upgradedFrom===1&&n.emit("journal-upgrade",{operation:"journal-upgrade",detail:"Schema 1 to schema 2; saved requests retained"}),await R(t,e,s,i,n)}e.importId&&(o.importId=e.importId),n.emit("reconciliation",{completedRuns:Object.keys(e.receiptIds).length,operation:"reconcile"});let I=0,p=await E(t,e,s,{onVerified(l){I+=l,n.emit("reconciliation",{verifiedBytes:I,operation:"reconcile"})}});if(c&&p.complete&&e.importId){const l=e.importId,b=e.operations.some(g=>g.key==="complete"&&g.state!=="accepted");n.emit("reconciliation",{operation:"complete-import",retries:n.current.retries+(b?1:0)}),await q(t,e,"complete","complete",{importId:l},()=>s.complete(l),i),n.emit("reconciliation",{operation:"verify-completion"}),p=await p.verifyCompletion()}o.problems.push(...p.problems);const f=e.importId?!!(await s.page(e.importId)).import.completedAt:!1;f||o.problems.push({kind:"import",id:e.importId??"missing",field:"completion",detail:"Server import is not complete"}),o.complete=p.complete&&f,n.emit(o.complete?"completion":"failed",{...o.problems[0]?{operation:"reconcile",detail:o.problems[0].detail}:{}}),d={...d,items:p.items,counts:p.counts,campaignVersion:p.campaignVersion,receiptIds:e.receiptIds,caseIds:e.caseIds,operations:e.operations.map(({key:l,state:b,error:g})=>({key:l,state:b,...g?{error:g}:{}}))}}catch(e){n.fail(e),o.problems.push({kind:"migration",id:"operation",field:c?"apply":"reconcile",detail:String(h(e instanceof Error?e.message:String(e),i))});try{const s=k(t,y);s.importId&&(o.importId=s.importId),d={...d,localOutcomes:D(t,s),receiptIds:s.receiptIds,caseIds:s.caseIds,operations:s.operations.map(({key:I,state:p,error:f})=>({key:I,state:p,...f?{error:f}:{}}))}}catch(s){d.journalError=String(h(s instanceof Error?s.message:String(s),i))}}return j(a,h({...d,...o},i)),o})}catch(t){return n.fail(t),o.problems.push({kind:"migration",id:"plan",field:"preflight",detail:String(h(t instanceof Error?t.message:String(t),i))}),o}}function D(m,i){const c=new Map;for(const r of m.runs)c.set(`run:${r.request.source.id}`,{...r.request.source,status:i.receiptIds[r.runId]?Object.hasOwn(m.reusedRuns,r.runId)?"reused":"accepted":"pending",...i.receiptIds[r.runId]?{cloudId:i.receiptIds[r.runId]}:{}});for(const r of m.records)c.set(`${r.source.kind}:${r.source.id}`,{...r.source,originalId:r.originalId,status:"pending"});for(const r of i.operations){const n=JSON.parse(r.request);for(const a of n.records??[]){const o=c.get(`${a.source.kind}:${a.source.id}`);o&&(o.status=r.state==="accepted"?"accepted":r.state)}const u=V(r,"batch")?r.result:void 0;for(const a of u?.items??[]){const o=c.get(`${a.kind}:${a.id}`);o&&c.set(`${a.kind}:${a.id}`,{...o,...a})}}return[...c.values()]}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { type HistoricalBatch, type HistoricalBatchResult, type HistoricalBegin, type HistoricalEvidence, type HistoricalImport, type HistoricalPage, type HistoricalReceipt, type HistoricalUploadPlan } from "../../../cloud/historical-contract.js";
|
|
2
|
-
import type { MigrationPlan } from "./model.js";
|
|
3
|
-
export interface OperationResults {
|
|
4
|
-
begin: HistoricalImport;
|
|
5
|
-
stage: HistoricalUploadPlan;
|
|
6
|
-
upload: {
|
|
7
|
-
uploaded: true;
|
|
8
|
-
};
|
|
9
|
-
archive: {
|
|
10
|
-
uploaded: true;
|
|
11
|
-
};
|
|
12
|
-
receipt: HistoricalReceipt;
|
|
13
|
-
batch: HistoricalBatchResult;
|
|
14
|
-
complete: HistoricalPage;
|
|
15
|
-
}
|
|
16
|
-
export interface OperationRequests {
|
|
17
|
-
begin: HistoricalBegin;
|
|
18
|
-
stage: HistoricalEvidence;
|
|
19
|
-
upload: {
|
|
20
|
-
url: string;
|
|
21
|
-
path: string;
|
|
22
|
-
bytes: number;
|
|
23
|
-
sha256: string;
|
|
24
|
-
};
|
|
25
|
-
archive: {
|
|
26
|
-
url: string;
|
|
27
|
-
bytes: number;
|
|
28
|
-
sha256: string;
|
|
29
|
-
manifestDigest: string;
|
|
30
|
-
};
|
|
31
|
-
receipt: {
|
|
32
|
-
url: string;
|
|
33
|
-
};
|
|
34
|
-
batch: HistoricalBatch;
|
|
35
|
-
complete: {
|
|
36
|
-
importId: string;
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
export type OperationKind = keyof OperationResults;
|
|
40
|
-
export interface MigrationOperation<K extends OperationKind = OperationKind> {
|
|
41
|
-
kind: K;
|
|
42
|
-
key: string;
|
|
43
|
-
request: string;
|
|
44
|
-
state: "pending" | "accepted" | "rejected" | "unknown";
|
|
45
|
-
result?: OperationResults[K];
|
|
46
|
-
error?: string;
|
|
47
|
-
}
|
|
48
|
-
export interface MigrationJournal {
|
|
49
|
-
schemaVersion: 1 | 2;
|
|
50
|
-
upgradedFrom?: 1;
|
|
51
|
-
legacyStages?: string[];
|
|
52
|
-
planDigest: string;
|
|
53
|
-
importId?: string;
|
|
54
|
-
operations: MigrationOperation[];
|
|
55
|
-
receiptIds: Record<string, string>;
|
|
56
|
-
caseIds: Record<string, string>;
|
|
57
|
-
}
|
|
58
|
-
export declare function operationIs<K extends OperationKind>(operation: MigrationOperation, kind: K): operation is MigrationOperation<K>;
|
|
59
|
-
export declare function emptyJournal(plan: MigrationPlan, planDigest: string): MigrationJournal;
|
|
60
|
-
/** Durable results are decoded against their saved command and the reviewed plan at first read. */
|
|
61
|
-
export declare function decodeJournal(value: unknown, plan: MigrationPlan, planDigest: string): MigrationJournal;
|
|
62
|
-
export declare function stagingRequest(plan: MigrationPlan, journal: MigrationJournal, runId: string): HistoricalEvidence;
|
|
63
|
-
/** openJournal has already validated this request against the plan and staging result. */
|
|
64
|
-
export declare function savedArchiveRequest(journal: MigrationJournal, key: string): OperationRequests["archive"] | undefined;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{parseHistoricalBatchResult as N,parseHistoricalImport as A,parseHistoricalPage as C,parseHistoricalReceipt as D,parseHistoricalResponse as O,parseHistoricalUploadPlan as z}from"../../../cloud/historical-contract.js";import{canonicalHistoricalJson as F,historicalDigest as S}from"../../../cloud/historical-json.js";import{MAX_CLOUD_ARCHIVE_BYTES as E}from"../../../cloud/archive-contract.js";import{array as H,object as q,text as v}from"./files.js";import{batchRecords as _,beginCommand as B,plannedRecords as J}from"./commands.js";function t(l,e){if(!l)throw new Error(`Invalid migration journal: ${e}`)}function I(l,e){const u=v(l,e,128);return t(/^[A-Za-z0-9_-]+$/u.test(u),e),u}function m(l,e,u){t(F(l)===F(e),u)}function $(l,e){const u=v(l,"operation URL"),n=new URL(u,e.target.origin);return t(n.origin===e.target.origin&&!n.username&&!n.password&&!n.hash&&!n.search,"operation URL target"),u}export function operationIs(l,e){return l.kind===e}export function emptyJournal(l,e){return{schemaVersion:1,planDigest:e,operations:[],receiptIds:Object.assign(Object.create(null),l.reusedRuns),caseIds:Object.create(null)}}export function decodeJournal(l,e,u){const n=q(l,"journal");t((n.schemaVersion===1||n.schemaVersion===2)&&n.planDigest===u,"journal belongs to another plan"),n.completed!==void 0&&t(typeof n.completed=="boolean","legacy completion state");const j=n.importId===void 0?void 0:I(n.importId,"importId"),o=emptyJournal(e,u);o.schemaVersion=n.schemaVersion,n.schemaVersion===1?(t(n.upgradedFrom===void 0&&n.legacyStages===void 0,"legacy journal has upgrade metadata"),delete o.legacyStages):(t(n.upgradedFrom===void 0||n.upgradedFrom===1,"journal upgrade origin"),n.upgradedFrom===1&&(o.upgradedFrom=1),o.legacyStages=H(n.legacyStages,"legacy stages").map(g=>v(g,"legacy stage")),t(new Set(o.legacyStages).size===o.legacyStages.length,"duplicate legacy stage"),t(o.upgradedFrom===1||o.legacyStages.length===0,"legacy stages require an upgraded journal"));const R=new Set,h=B(e,u);let x=e.campaignVersion;for(const g of H(n.operations,"operations")){const r=q(g,"operation"),a=v(r.key,"operation key");t(!R.has(a),"duplicate operation key"),R.add(a),t(r.state==="pending"||r.state==="accepted"||r.state==="rejected"||r.state==="unknown","operation state");const V=v(r.request,"operation request",2*1024**2),f=q(O(V),"operation request"),U=r.state,L={key:a,request:V,state:U,...r.error===void 0?{}:{error:v(r.error,"operation error",65536)}},y=(i,s)=>{t(r.kind===void 0||r.kind===i,"operation kind"),t(r.state!=="accepted"||s!==void 0,"accepted operation lacks result"),o.operations.push({...L,kind:i,...s===void 0?{}:{result:s}})};if(a==="begin"){m(f,h,"begin request differs from plan");const i=r.result===void 0?void 0:A(r.result,e.target.projectId,e.target.campaignId);i&&(I(i.id,"begin import identity"),t(i.namespace===h.namespace&&i.inventoryDigest===h.inventoryDigest&&i.total===h.inventory.length,"begin inventory identity"),r.state==="accepted"&&(o.importId=i.id)),y("begin",i);continue}if(t(o.importId,"operation precedes accepted begin"),a.startsWith("stage:")){const i=e.runs.find(p=>a===`stage:${p.runId}`);t(i&&!Object.hasOwn(e.reusedRuns,i.runId),"stage run identity");const s=stagingRequest(e,o,i.runId);m(f,s,"stage request differs from plan");const c=r.result===void 0?void 0:z(r.result,s);if(c){$(c.completeUrl,e);for(const p of c.uploads)$(p.url,e);c.archive&&$(c.archive.url,e)}y("stage",c);continue}if(a.startsWith("archive:")){t(o.schemaVersion===2,"archive operation requires journal schema 2");const i=e.runs.find(p=>a.startsWith(`archive:${p.runId}:`));t(i,"archive run identity");const s=o.operations.find(p=>p.key===`stage:${i.runId}`);t(s&&operationIs(s,"stage")&&s.result,"archive lacks staging result");const c=$(f.url,e);t(a===`archive:${i.runId}:${S(c)}`,"archive operation key"),t(new URL(c,e.target.origin).pathname===`/api/v1/publications/${s.result.publicationId}/archive`,"archive publication identity"),t(s.result.archive?s.result.archive.url===c:s.result.status==="accepted","archive target identity"),t(stagingRequest(e,o,i.runId).evidenceFormat==="zip-v1","archive transport identity"),t(Number.isSafeInteger(f.bytes)&&Number(f.bytes)>0&&Number(f.bytes)<=(s.result.archive?.maxBytes??E),"archive byte bound"),t(typeof f.sha256=="string"&&/^[a-f0-9]{64}$/.test(f.sha256),"archive digest"),m(f,{url:c,bytes:f.bytes,sha256:f.sha256,manifestDigest:i.request.manifest.digest},"archive request identity"),r.result!==void 0&&m(r.result,{uploaded:!0},"archive result"),y("archive",r.result===void 0?void 0:{uploaded:!0});continue}if(a.startsWith("upload:")||a.startsWith("receipt:")){const i=a.startsWith("upload:")?"upload":"receipt",s=e.runs.find(d=>a.startsWith(`${i}:${d.runId}:`));t(s,"evidence operation run identity");const c=o.operations.find(d=>operationIs(d,"stage")&&d.key===`stage:${s.runId}`)?.result;t(c,"evidence operation lacks a decoded staging result");const p=$(f.url,e);if(i==="upload"){const d=Number(a.split(":").at(-2)),b=s.request.manifest.files.at(d),k=b&&s.files.find(W=>W.path===b.path);t(Number.isSafeInteger(d)&&d>=0&&k,"upload file identity"),t(a===`upload:${s.runId}:${String(d)}:${S(p)}`,"upload operation key"),m(f,{url:p,path:k.absolutePath,bytes:k.bytes,sha256:k.sha256},"upload request differs from plan"),r.result!==void 0&&m(r.result,{uploaded:!0},"upload result"),y("upload",r.result===void 0?void 0:{uploaded:!0})}else{t(a===`receipt:${s.runId}:${S(p)}`&&p===c.completeUrl,"receipt completion identity"),m(f,{url:p},"receipt request");const d=r.result===void 0?void 0:D(r.result,e.target.projectId,{importId:o.importId,evidence:s.request,reused:c.reused});d&&(I(d.id,"receipt identity"),t(d.repository.toLowerCase()===`${e.repository.owner}/${e.repository.name}`.toLowerCase(),"receipt repository"),r.state==="accepted"&&(o.receiptIds[s.runId]=d.id)),y("receipt",d)}continue}const w=/^(case|finding)s-(\d+)$/u.exec(a);if(w){const i=w[1]==="case"?"case":"finding",s=Number(w[2]),c=_(J(e,o,i))[s];t(c,"batch position");const p={schemaVersion:1,idempotencyKey:`migration-${u}-${i}-${String(s)}`,expectedVersion:x,records:c};m(f,p,"batch request differs from plan or prior version");const d=r.result===void 0?void 0:N(r.result,o.importId,p);if(d&&(t(d.rejected?r.state==="rejected":r.state!=="rejected","batch result state"),r.state==="accepted"&&(x=d.version,i==="case")))for(const b of d.items)o.caseIds[b.id]=I(b.cloudId,"case outcome identity");y("batch",d);continue}if(a==="complete"){m(f,{importId:o.importId},"completion request identity");const i=r.result===void 0?void 0:C(r.result,e.target.projectId,e.target.campaignId,o.importId);i&&(t(i.import.namespace===h.namespace&&i.import.inventoryDigest===h.inventoryDigest&&i.import.total===h.inventory.length,"completed inventory identity"),t(i.import.completedAt&&Object.values(i.counts).every(s=>Object.entries(s).every(([c,p])=>["accepted","reused"].includes(c)||p===0)),"incomplete completion result")),y("complete",i);continue}throw new Error(`Invalid migration journal: unknown operation ${a}`)}j!==void 0&&t(j===o.importId,"saved import identity differs from accepted begin");for(const g of o.legacyStages??[])t(o.operations.some(r=>r.kind==="stage"&&r.key===`stage:${g}`),"legacy stage is absent");for(const g of["receiptIds","caseIds"])for(const[r,a]of Object.entries(q(n[g],g)))t(o[g][r]===I(a,g),`${g} differs from accepted operations`);return o}export function stagingRequest(l,e,u){const n=l.runs.find(j=>j.runId===u);return t(n,"stage run identity"),e.schemaVersion===1||e.legacyStages?.includes(u)?n.request:{...n.request,evidenceFormat:"zip-v1"}}export function savedArchiveRequest(l,e){const u=l.operations.find(n=>operationIs(n,"archive")&&n.key===e);return u?O(u.request):void 0}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import type { PlannedRecord } from "./model.js";
|
|
2
|
-
/** Describe projections while keeping the complete sanitized source record. */
|
|
3
|
-
export declare function describeFields(source: Record<string, unknown>, record: PlannedRecord, kind: "bug" | "observation" | "case"): NonNullable<PlannedRecord["fieldDispositions"]>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export function describeFields(n,s,c){const t=c==="case"?{id:"source.id",summary:"case.title",classification:"case.classification",state:"case.state",fingerprints:"case.fingerprint",lifecycle:"case.firstSeenAt,case.lastSeenAt",issue:"case.issueNumber,case.issueRepository"}:{id:"source.id",title:"finding.title",deduplicationKey:"finding.rootCauseKey",classification:"finding.classification",contract:"finding.expectedBehavior,finding.authority",expected:"finding.expectedBehavior",actual:"finding.observedBehavior",impact:"finding.impact",severity:"finding.severity",reproduction:"finding.reproduction",evidenceLimitations:"finding.limitations",surface:"finding.surface",issueReferences:"finding.issueReferences",evidence:"evidence",runIds:"evidence",artifacts:"evidence",observedAt:"observedAt",recordedAt:"observedAt",regressionCaseIds:"regressionSourceIds"},r=Object.keys(n).sort().map(e=>({field:e,disposition:t[e]?"mapped":"retained",reason:t[e]?"Projected through the explicit mapping or supported legacy fields; full original retained in sourceRecord":"No historical Cloud field represents this detail; retained in sourceRecord",...t[e]?{target:t[e]}:{}}));if(c==="bug"&&n.classification===void 0&&s.finding&&r.push({field:"classification",disposition:"mapped",target:"finding.classification",reason:`The legacy bugs collection supplies the default product_bug classification; selected value: ${s.finding.classification}`}),c==="bug"){const e=n.evidence,d=new Set(s.evidence?.map(a=>a.runId));for(const[a,i]of(e?.artifacts??[]).entries()){const f=typeof i=="string"?i:i.path,o=(typeof i=="string"?void 0:i.runId)??/^(?:\.testkit\/results\/runs\/)?([^/]+)\//.exec(f)?.[1];o&&!d.has(o)&&r.push({field:`evidence.artifacts[${String(a)}]`,disposition:"retained",reason:"Supporting artifact is outside the confirming run set; retained in sourceRecord and excluded from verification evidence"})}}return r}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { MigrationMapping, MigrationProblem, PlannedRecord } from "./model.js";
|
|
2
|
-
export declare function decodeMapping(value: unknown): MigrationMapping;
|
|
3
|
-
export declare function mapRecords(registryLossless: unknown, regressionsLossless: unknown, mapping: MigrationMapping, runTimes: ReadonlyMap<string, string>): {
|
|
4
|
-
records: PlannedRecord[];
|
|
5
|
-
problems: MigrationProblem[];
|
|
6
|
-
sourceSummary: {
|
|
7
|
-
bugs: number;
|
|
8
|
-
observations: number;
|
|
9
|
-
cases: number;
|
|
10
|
-
};
|
|
11
|
-
};
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{HISTORICAL_SOURCE_LIMIT as N}from"../../../cloud/historical-contract.js";import{canonicalHistoricalJson as j,historicalDigest as D}from"../../../cloud/historical-json.js";import{array as R,object as u,plain as x,text as F}from"./files.js";import{describeFields as L}from"./mapping-dispositions.js";const $=["product_bug","testkit_failure","environment_failure","stale_assertion","unproven","intended_behavior","observation"],T=["untriaged","regression","flaky","expected","quarantined"],B=["expectedBehavior","authority","observedBehavior","impact","severity","reproduction","limitations"];function t(i,n,s){if(!i)throw Object.assign(new Error(s),{field:n})}function w(i,n,s){t(Object.keys(i).every(o=>n.includes(o)),s,"Unknown mapping field")}function A(i,n,s){return t(typeof i=="string"&&n.includes(i),s,"An explicit supported mapping is required"),i}function c(i,n,s=4096){let o;try{o=F(i,n,s)}catch{throw Object.assign(new Error(`Requires nonblank text of at most ${String(s)} bytes`),{field:n})}return t(!/[\ud800-\udfff]/u.test(o)&&Array.from(o).every(d=>d.charCodeAt(0)>=32||`
|
|
2
|
-
\r `.includes(d)),n,"Unsupported control character"),o}function V(i){return c(typeof i=="string"?i.replace(/\u001b\[[0-9;:]*m/gu,""):i,"summary")}function k(i,n){const s=c(i,n,128);return t(/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(s)&&Number.isFinite(Date.parse(s)),n,"Requires an original RFC3339 observation time or an explicit evidence rule"),s}function q(i,n,s=32){const o=R(i,n);return t(o.length<=s,n,`At most ${String(s)} entries are supported`),o.map(d=>c(d,n))}function M(i){return q(i,"issueReferences").map(n=>{const s=URL.parse(n);return t(s&&["https:","http:"].includes(s.protocol)&&!s.username&&!s.password,"issueReferences","Issue references require HTTP URLs without credentials"),n})}function E(i){const n=u(i,"relationship");w(n,["kind","targetId"],"relationship"),A(n.kind,["independent","known","duplicate"],"relationship.kind"),n.kind==="independent"?t(n.targetId===void 0,"relationship.targetId","Independent findings cannot have a relationship target"):t(/^[A-Za-z0-9_-]{1,128}$/.test(c(n.targetId,"relationship.targetId",128)),"relationship.targetId","Requires a Cloud finding ID")}export function decodeMapping(i){const n=u(x(i),"mapping");w(n,["schemaVersion","namespace","repository","findings","cases","runReceipts"],"mapping"),t(n.schemaVersion===1,"schemaVersion","Requires mapping schema 1"),c(n.namespace,"namespace",128);const s=u(n.repository,"repository");w(s,["host","owner","name"],"repository");for(const r of["host","owner","name"])c(s[r],`repository.${r}`,256);const o=u(n.findings??{},"findings");for(const[r,a]of Object.entries(o)){c(r,"finding mapping ID");const e=u(a,"finding mapping");w(e,["title","rootCauseKey","classification","observedAt","observationRule","observationRationale","verify","relationship","regressionCaseIds","issueReferences","fields"],"finding mapping");for(const f of["title","rootCauseKey"])e[f]!==void 0&&c(e[f],f,256);if(e.classification!==void 0&&A(e.classification,$,"classification"),e.observedAt!==void 0&&k(e.observedAt,"observedAt"),e.observationRationale!==void 0&&c(e.observationRationale,"observationRationale"),e.observationRule!==void 0&&A(e.observationRule,["second_confirmation","last_evidence"],"observationRule"),t(e.observedAt===void 0||e.observationRule===void 0,"observedAt","Choose an explicit time or an evidence rule, not both"),e.verify!==void 0&&t(typeof e.verify=="boolean","verify","Requires a boolean"),e.relationship!==void 0&&E(e.relationship),e.regressionCaseIds!==void 0&&q(e.regressionCaseIds,"regressionCaseIds"),e.issueReferences!==void 0&&M(e.issueReferences),e.fields!==void 0){const f=u(e.fields,"fields");w(f,[...B],"fields");for(const[l,y]of Object.entries(f))c(y,l)}}const d=u(n.cases??{},"cases");for(const r of Object.values(d)){const a=u(r,"case mapping");w(a,["sourceId","engineId","profileId","failureKey","testType","classification"],"case mapping");for(const e of["engineId","profileId"])c(a[e],e);for(const e of["sourceId","failureKey","testType"])a[e]!==void 0&&c(a[e],e,e==="sourceId"?128:4096);a.classification!==void 0&&A(a.classification,T,"case.classification")}if(n.runReceipts!==void 0)for(const[r,a]of Object.entries(u(n.runReceipts,"runReceipts")))c(r,"runId",128),c(a,"receiptId",128);return{...n,findings:o,cases:d}}function U(i,n){const s=u(i.evidence??{},"evidence"),o=q(n?s.reproducingRunIds??[]:i.runIds??[],"runIds",1e4),d=new Set(n?[]:o);for(const e of R(s.runs??[],"evidence.runs"))d.add(c(u(e,"evidence run").runId,"runId",128));const r=[];for(const e of R(n?s.artifacts??[]:i.artifacts??[],"artifacts")){const f=typeof e=="string"?{path:e}:u(e,"artifact"),l=c(f.path,"artifact.path"),y=/^(?:\.testkit\/results\/runs\/)?([^/]+)\/(.+)$/.exec(l),g=typeof f.runId=="string"?f.runId:y?.[1];if(t(g,"artifact.runId","Artifact requires an explicit run identity"),n&&!o.includes(g))continue;const v=`.testkit/results/runs/${g}/`,p=l.startsWith(v)?l.slice(v.length):l.startsWith(`${g}/`)?l.slice(g.length+1):l;t(!p.startsWith("/")&&!p.includes("\\")&&p.split("/").every(C=>C&&C!=="."&&C!==".."),"artifact.path","Artifact path must be normalized and relative"),r.push({runId:g,path:p}),d.add(g)}for(const e of o)t(d.has(e),"evidence","Confirming run requires source evidence"),r.some(f=>f.runId===e)||r.push({runId:e,path:"run.json"});const a=[...new Map(r.map(e=>[`${e.runId}\0${e.path}`,e])).values()];return t(a.length<=64,"evidence","At most 64 evidence references can be mapped"),a}function H(i,n,s,o){const d=i.observedAt??i.recordedAt;if(d!==void 0){const e=k(d,"observedAt");return t(n.observedAt===void 0||Date.parse(n.observedAt)===Date.parse(e),"observedAt","Mapping cannot replace an original observation time"),{observedAt:e,observation:{origin:"source",rationale:"Preserved original observation time"}}}if(n.observedAt!==void 0)return{observedAt:k(n.observedAt,"observedAt"),observation:{origin:"mapping",rationale:n.observationRationale??"Explicit observation time supplied by the mapping"}};t(n.observationRule,"observedAt","Missing observation time requires an explicit mapping or evidence rule");const r=[...new Set(s.map(e=>e.runId))].map(e=>({runId:e,observedAt:k(o.get(e),"evidence observation time")}));r.sort((e,f)=>Date.parse(e.observedAt)-Date.parse(f.observedAt));const a=n.observationRule==="second_confirmation"?r[1]:r.at(-1);return{observedAt:k(a?.observedAt,"observedAt"),observation:{origin:n.observationRule,rationale:n.observationRationale??(n.observationRule==="second_confirmation"?"The second distinct confirming run establishes the two-run observation":"The latest cited evidence run establishes the observation"),runIds:r.map(e=>e.runId)}}}function z(i,n){if(i==null)return;const s=typeof i=="string"?i:j(i);return c(s,n)}function P(i,n,s,o,d){const r=c(i.id,"id",128);t(/^[A-Za-z0-9_-]{1,128}$/.test(r),"id","Finding IDs must use letters, numbers, underscores or hyphens");const a=c(s.rootCauseKey??i.deduplicationKey,"rootCauseKey",256);t(i.deduplicationKey===void 0||s.rootCauseKey===void 0||s.rootCauseKey===i.deduplicationKey,"rootCauseKey","Original deduplication keys must be preserved");const e={title:c(s.title??i.title,"title",256),rootCauseKey:a,classification:A(s.classification??i.classification??(o?"product_bug":void 0),$,"classification")},f=u(i.contract??{},"contract"),l=i.reproduction&&typeof i.reproduction=="object"?Object.fromEntries(Object.entries(u(i.reproduction,"reproduction")).filter(([v])=>v!=="variants")):i.reproduction,y={expectedBehavior:f.expected??i.expected,authority:f.sources,observedBehavior:i.actual,impact:i.impact,severity:i.severity,reproduction:l,limitations:i.evidenceLimitations};for(const v of B){const p=z(s.fields?.[v]??y[v],v);p!==void 0&&(e[v]=p)}i.surface!==void 0&&(e.surface=c(i.surface,"surface")),(s.issueReferences!==void 0||i.issueReferences!==void 0)&&(e.issueReferences=M(s.issueReferences??i.issueReferences));const g=U(i,o);return{originalId:r,source:{kind:"finding",id:r,digest:D(n)},sourceRecord:n,...H(i,s,g,d),finding:e,verify:s.verify??o,relationship:s.relationship??{kind:"independent"},evidence:g,regressionSourceIds:q(s.regressionCaseIds??i.regressionCaseIds??[],"regressionCaseIds")}}function W(i,n,s){const o=c(i.id,"id");t(s,"case mapping","Historical engine and profile identity require an explicit case mapping");const d=c(s.sourceId??o,"sourceId",128),r=R(i.fingerprints,"fingerprints");t(r.length===1,"fingerprints","A case requires exactly one unambiguous fingerprint");const a=u(r[0],"fingerprint");t(s.failureKey!==void 0||a.errorIncludes===void 0&&a.metricKey===void 0,"failureKey","Selector-based fingerprints require an explicit failure key mapping");const e=[a.service,s.testType??a.type,s.engineId,s.profileId,a.path,s.failureKey??a.failureKey].map(v=>{const p=c(v,"fingerprint");return t(!p.includes("::"),"fingerprint","Fingerprint components cannot contain ::"),p}),f=u(i.lifecycle,"lifecycle"),l=k(f.firstSeenAt,"firstSeenAt"),y=k(f.lastSeenAt,"lastSeenAt");t(Date.parse(l)<=Date.parse(y),"lifecycle","Last observation cannot precede first observation");const g={id:d,service:e[0],testType:e[1],engineId:e[2],profileId:e[3],path:e[4],failureKey:e[5],fingerprint:[...e.slice(0,5),"failure",e[5]].join("::"),title:V(i.summary),classification:A(s.classification??i.classification,T,"case.classification"),state:A(i.state,["active","resolved","fixed_pending_verification"],"case.state"),firstSeenAt:l,lastSeenAt:y};if(i.issue!==void 0&&i.issue!==null){const v=u(i.issue,"issue"),p=x(v.number);t(Number.isSafeInteger(p)&&Number(p)>0&&Number(p)<=2147483647,"issue.number","Requires a valid issue number"),g.issueNumber=Number(p),g.issueRepository=c(v.repository,"issue.repository")}return{originalId:o,source:{kind:"case",id:d,digest:D(n)},sourceRecord:n,observedAt:y,observation:{origin:"lifecycle",rationale:"Preserved original lastSeenAt lifecycle time"},case:g}}export function mapRecords(i,n,s,o){const d=u(i,"registry"),r=u(n,"regressions");t(x(d.schemaVersion)===1,"registry.schemaVersion","Requires campaign schema 1"),t(x(r.schemaVersion)===3,"regressions.schemaVersion","Requires regression schema 3");const a=R(d.bugs,"bugs"),e=R(d.classifiedNonBugs,"classifiedNonBugs"),f=R(r.cases,"cases"),l=[],y=[],g=new Set,v=new Set,p=new Set;for(const[b,I]of[["case",f],["bug",a],["observation",e]])for(const K of I){let S="unknown";try{const m=u(K,b);S=c(m.id,"id"),t(Buffer.byteLength(j(K))<=N,"sourceRecord","Source record exceeds 128 KiB");const h=b==="case"?W(m,K,Object.hasOwn(s.cases,S)?s.cases[S]:void 0):P(m,K,Object.hasOwn(s.findings,S)?s.findings[S]:{},b==="bug",o),O=`${h.source.kind}\0${h.source.id}`;if(t(!g.has(O),"source.id","Duplicate source identity or alias collision"),g.add(O),h.finding&&(t(!v.has(h.finding.rootCauseKey),"rootCauseKey","Multiple findings share a root-cause key"),v.add(h.finding.rootCauseKey)),h.case){const _=h.case.fingerprint;t(!p.has(_),"fingerprint","Multiple source cases share a mapped fingerprint"),p.add(_)}h.fieldDispositions=L(m,h,b),l.push(h)}catch(m){y.push({kind:b,id:S,field:m.field??"record",detail:m instanceof Error?m.message:"Invalid source record"})}}const C=new Map(l.filter(b=>b.case).map(b=>[b.originalId,b.source.id]));for(const b of l)if(b.finding){for(const I of b.regressionSourceIds??[])C.has(I)||y.push({kind:"finding",id:b.originalId,field:"regressionCaseIds",detail:`Referenced case is not mapped: ${I}`});b.regressionSourceIds=(b.regressionSourceIds??[]).map(I=>C.get(I)??I)}return{records:l,problems:y,sourceSummary:{bugs:a.length,observations:e.length,cases:f.length}}}
|