@usefidel/contracts 0.1.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/README.md +52 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/run-errors.d.ts +59 -0
- package/dist/run-errors.js +283 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @usefidel/contracts
|
|
2
|
+
|
|
3
|
+
Shared contracts between Fidel surfaces. Currently the **run-error taxonomy** only:
|
|
4
|
+
the set of typed error codes a validation run can fail with, their user-facing
|
|
5
|
+
messages, whether each is retryable, and a helper for presenting a failed run.
|
|
6
|
+
|
|
7
|
+
This package exists so that Fidel's web app, browser extension, GitHub Action and
|
|
8
|
+
CI runner all describe the same failure with the same code and the same wording,
|
|
9
|
+
rather than each keeping its own drifting copy.
|
|
10
|
+
|
|
11
|
+
## Why it is public
|
|
12
|
+
|
|
13
|
+
It is published publicly so build environments can install it without
|
|
14
|
+
authenticating to a private registry. **Public availability is not a licence.**
|
|
15
|
+
See below.
|
|
16
|
+
|
|
17
|
+
It contains no proprietary logic — no matching, scoring, or diffing code. Error
|
|
18
|
+
codes and their messages only.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install @usefidel/contracts
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Use
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { RUN_ERROR_CODES, resolveRunDisplay } from '@usefidel/contracts';
|
|
30
|
+
// or
|
|
31
|
+
import { resolveRunDisplay } from '@usefidel/contracts/run-errors';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Ships ESM with TypeScript declarations. No runtime dependencies.
|
|
35
|
+
|
|
36
|
+
## Versioning
|
|
37
|
+
|
|
38
|
+
Semver. The error-code set is additive within a major version: new codes may
|
|
39
|
+
appear in a minor release, and existing codes are neither removed nor given new
|
|
40
|
+
meanings without a major bump.
|
|
41
|
+
|
|
42
|
+
## Licence
|
|
43
|
+
|
|
44
|
+
**UNLICENSED — all rights reserved.** Being downloadable from npm does not grant
|
|
45
|
+
permission to use, copy, modify, or redistribute this package. It is published for
|
|
46
|
+
Fidel's own build and deployment pipelines. If you want to use it, ask first:
|
|
47
|
+
support@usefidel.com
|
|
48
|
+
|
|
49
|
+
## Contributing
|
|
50
|
+
|
|
51
|
+
Not open to outside contributions. Issues and pull requests have nowhere to go —
|
|
52
|
+
the source lives in a private repository.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @usefidel/contracts — the entire public surface.
|
|
2
|
+
//
|
|
3
|
+
// SCOPE IS DELIBERATELY NARROW (plan 046, decision D5). This package exists so
|
|
4
|
+
// `usefidel/fidel-web` can render run errors without importing from the monorepo.
|
|
5
|
+
// It carries CONTRACTS ONLY: shapes, codes, and the presentation helpers that
|
|
6
|
+
// turn a code into user-facing copy.
|
|
7
|
+
//
|
|
8
|
+
// It must never carry:
|
|
9
|
+
// - matching-engine code of any kind
|
|
10
|
+
// - backend implementation (Supabase queries, edge-function logic, Lambda calls)
|
|
11
|
+
// - credentials, endpoints, or anything environment-specific
|
|
12
|
+
//
|
|
13
|
+
// The narrowness is the security property. This package is installable by a
|
|
14
|
+
// contractor; everything in it is readable by one. Widening it widens that.
|
|
15
|
+
//
|
|
16
|
+
// Adding an export here is an architectural change, not a convenience — it needs
|
|
17
|
+
// the same review as any cross-surface contract.
|
|
18
|
+
export { RUN_ERROR_CODES, ERROR_CODE_META, mapSnapshotErrorToCode, mapFigmaErrorToCode, resolveRunDisplay, } from './run-errors.js';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-errors.ts — Canonical run error taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for all typed error codes, their human-readable
|
|
5
|
+
* messages, retryability, and display logic.
|
|
6
|
+
*
|
|
7
|
+
* VENDOR-SYNC: This file is vendored to:
|
|
8
|
+
* - lambda/lib/run-errors.ts (exact copy — Lambda is self-contained)
|
|
9
|
+
* - github-action/src/run-errors.ts (exact copy — github-action can't import Supabase _shared)
|
|
10
|
+
* - runner/src/run-errors.js (intentional-diff: plain JS, no TypeScript)
|
|
11
|
+
*
|
|
12
|
+
* The webapp imports via `@fidel/shared/run-errors` alias → extension/src/lib/run-errors.ts.
|
|
13
|
+
* The extension copy is itself vendored from here (registered in vendor-sync.config.json).
|
|
14
|
+
*
|
|
15
|
+
* When editing this file, run `npx tsx scripts/check-vendor-sync.ts` and
|
|
16
|
+
* update all consumer copies.
|
|
17
|
+
*/
|
|
18
|
+
export declare const RUN_ERROR_CODES: readonly ["TARGET_AUTH_WALL", "TARGET_UNREACHABLE", "TARGET_TIMEOUT", "TARGET_CSP_BLOCKED", "FIGMA_ACCESS_DENIED", "FIGMA_TOKEN_EXPIRED", "FIGMA_NOT_FOUND", "FIGMA_RATE_LIMITED", "PIPELINE_TIMEOUT", "PIPELINE_ERROR", "ZERO_ELEMENTS_MATCHED", "PERSIST_FAILED", "SESSION_EXPIRED", "INVALID_REFERENCE_URL", "INVALID_TARGET_URL", "DESIGN_SYSTEM_NOT_AVAILABLE", "DESIGN_SYSTEM_CONTEXT_NOT_AVAILABLE", "DESIGN_SYSTEM_INCOMPATIBLE", "FLOW_STEP_FAILED", "UNKNOWN_ERROR"];
|
|
19
|
+
export type RunErrorCode = (typeof RUN_ERROR_CODES)[number];
|
|
20
|
+
export interface RunErrorMeta {
|
|
21
|
+
retryable: boolean;
|
|
22
|
+
/** Full sentence shown in error banners / report pages. No HTTP jargon. */
|
|
23
|
+
userMessage: string;
|
|
24
|
+
/** Short label (2–4 words) for badges or compact lists. */
|
|
25
|
+
shortLabel: string;
|
|
26
|
+
}
|
|
27
|
+
export declare const ERROR_CODE_META: Record<RunErrorCode, RunErrorMeta>;
|
|
28
|
+
export interface ErrorMapping {
|
|
29
|
+
code: RunErrorCode;
|
|
30
|
+
message: string;
|
|
31
|
+
retryable: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Map a raw Snapshot Lambda error string to a typed RunErrorCode.
|
|
35
|
+
*
|
|
36
|
+
* Input format from Snapshot Lambda: "SNAPSHOT_ERROR:<status>:<detail>"
|
|
37
|
+
* where detail contains Chromium net::ERR_* codes.
|
|
38
|
+
*/
|
|
39
|
+
export declare function mapSnapshotErrorToCode(rawError: string | undefined): ErrorMapping;
|
|
40
|
+
/**
|
|
41
|
+
* Map a raw Figma fetch error string to a typed RunErrorCode.
|
|
42
|
+
*/
|
|
43
|
+
export declare function mapFigmaErrorToCode(rawError: string): ErrorMapping;
|
|
44
|
+
export interface RunDisplay {
|
|
45
|
+
title: string;
|
|
46
|
+
description: string;
|
|
47
|
+
retryable: boolean;
|
|
48
|
+
action?: 'reconnect_figma' | 'upgrade' | 'check_url' | 'reconnect_environment';
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Derive a user-facing display object from a validation_runs row.
|
|
52
|
+
* Used by webapp, GitHub Action comment formatter, and the runner.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveRunDisplay(run: {
|
|
55
|
+
status: string;
|
|
56
|
+
error_code?: string | null;
|
|
57
|
+
error_message?: string | null;
|
|
58
|
+
is_retryable?: boolean | null;
|
|
59
|
+
}): RunDisplay;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-errors.ts — Canonical run error taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for all typed error codes, their human-readable
|
|
5
|
+
* messages, retryability, and display logic.
|
|
6
|
+
*
|
|
7
|
+
* VENDOR-SYNC: This file is vendored to:
|
|
8
|
+
* - lambda/lib/run-errors.ts (exact copy — Lambda is self-contained)
|
|
9
|
+
* - github-action/src/run-errors.ts (exact copy — github-action can't import Supabase _shared)
|
|
10
|
+
* - runner/src/run-errors.js (intentional-diff: plain JS, no TypeScript)
|
|
11
|
+
*
|
|
12
|
+
* The webapp imports via `@fidel/shared/run-errors` alias → extension/src/lib/run-errors.ts.
|
|
13
|
+
* The extension copy is itself vendored from here (registered in vendor-sync.config.json).
|
|
14
|
+
*
|
|
15
|
+
* When editing this file, run `npx tsx scripts/check-vendor-sync.ts` and
|
|
16
|
+
* update all consumer copies.
|
|
17
|
+
*/
|
|
18
|
+
// ── Error code registry ────────────────────────────────────────────────────────
|
|
19
|
+
export const RUN_ERROR_CODES = [
|
|
20
|
+
'TARGET_AUTH_WALL',
|
|
21
|
+
'TARGET_UNREACHABLE',
|
|
22
|
+
'TARGET_TIMEOUT',
|
|
23
|
+
'TARGET_CSP_BLOCKED',
|
|
24
|
+
'FIGMA_ACCESS_DENIED',
|
|
25
|
+
'FIGMA_TOKEN_EXPIRED',
|
|
26
|
+
'FIGMA_NOT_FOUND',
|
|
27
|
+
'FIGMA_RATE_LIMITED',
|
|
28
|
+
'PIPELINE_TIMEOUT',
|
|
29
|
+
'PIPELINE_ERROR',
|
|
30
|
+
'ZERO_ELEMENTS_MATCHED',
|
|
31
|
+
'PERSIST_FAILED',
|
|
32
|
+
'SESSION_EXPIRED',
|
|
33
|
+
// Phase 34-B PR 7-4: URL↔URL input validation. Each side fails its own code
|
|
34
|
+
// so the webapp's error card can highlight the correct field (matches the
|
|
35
|
+
// figmaUrl-only INVALID handling pre-34-B).
|
|
36
|
+
'INVALID_REFERENCE_URL',
|
|
37
|
+
'INVALID_TARGET_URL',
|
|
38
|
+
// Phase 34-C PR 7-4: design_system_vs_live mode prereqs.
|
|
39
|
+
// DESIGN_SYSTEM_NOT_AVAILABLE — no theme_registries row for the team's
|
|
40
|
+
// selected repo (intake never completed, or the row was deleted). Repo
|
|
41
|
+
// name omitted from the user message per security H-4 (error_message
|
|
42
|
+
// is owner-readable AND projected through get_shared_run; including the
|
|
43
|
+
// repo name would leak business intent through the share view).
|
|
44
|
+
'DESIGN_SYSTEM_NOT_AVAILABLE',
|
|
45
|
+
// Plan 021 Phase 3: caller supplied an explicit designSystemContextId that
|
|
46
|
+
// resolved to nothing (invalid id, archived, or belongs to another team).
|
|
47
|
+
// Distinct from DESIGN_SYSTEM_NOT_AVAILABLE (no theme data at all) — this
|
|
48
|
+
// means the repo/theme IS available, but the SPECIFIC brand context the
|
|
49
|
+
// caller asked for isn't. Message is repo/brand-identity-free for the same
|
|
50
|
+
// H-4 reason as DESIGN_SYSTEM_NOT_AVAILABLE. Never silently substituted for
|
|
51
|
+
// another context — the caller must re-pick.
|
|
52
|
+
'DESIGN_SYSTEM_CONTEXT_NOT_AVAILABLE',
|
|
53
|
+
// Plan 031: the theme exists (unlike DESIGN_SYSTEM_NOT_AVAILABLE) but the
|
|
54
|
+
// compatibility preflight (assessThemeValidatability in web-validate/
|
|
55
|
+
// design-system.ts) determined it can't produce a meaningful validation —
|
|
56
|
+
// unsupported UI framework (Chakra/MUI/etc.), a parse that never resolved
|
|
57
|
+
// concrete token values, or zero declared tokens. Distinct from both
|
|
58
|
+
// DESIGN_SYSTEM_NOT_AVAILABLE (no theme data at all) and
|
|
59
|
+
// DESIGN_SYSTEM_CONTEXT_NOT_AVAILABLE (theme exists, wrong brand context)
|
|
60
|
+
// — this means the RIGHT theme is present but structurally can't produce
|
|
61
|
+
// signal. Message is repo/brand-identity-free for the same H-4 reason
|
|
62
|
+
// (projected through get_shared_run to anon share viewers).
|
|
63
|
+
'DESIGN_SYSTEM_INCOMPATIBLE',
|
|
64
|
+
// W4 4c: prototype-flow validation failure — reserved for v2 flow validation.
|
|
65
|
+
// Indicates a deterministic failure during a single step of flow traversal
|
|
66
|
+
// (e.g. a transition target is missing or the step response is invalid).
|
|
67
|
+
// retryable: false — this is a structural design problem, not a transient error.
|
|
68
|
+
'FLOW_STEP_FAILED',
|
|
69
|
+
'UNKNOWN_ERROR',
|
|
70
|
+
];
|
|
71
|
+
export const ERROR_CODE_META = {
|
|
72
|
+
TARGET_AUTH_WALL: {
|
|
73
|
+
retryable: false,
|
|
74
|
+
userMessage: "This page is behind a login or password. Fidel can't validate protected pages yet - try a public preview URL.",
|
|
75
|
+
shortLabel: 'Login required',
|
|
76
|
+
},
|
|
77
|
+
TARGET_UNREACHABLE: {
|
|
78
|
+
retryable: false,
|
|
79
|
+
userMessage: "We couldn't reach that page. Check the URL is correct and publicly reachable from the internet.",
|
|
80
|
+
shortLabel: 'Page unreachable',
|
|
81
|
+
},
|
|
82
|
+
TARGET_TIMEOUT: {
|
|
83
|
+
retryable: true,
|
|
84
|
+
userMessage: 'The page took too long to load. Try again, or try a lighter version of the page.',
|
|
85
|
+
shortLabel: 'Page timed out',
|
|
86
|
+
},
|
|
87
|
+
TARGET_CSP_BLOCKED: {
|
|
88
|
+
retryable: false,
|
|
89
|
+
userMessage: 'The page blocked our request. It may have bot protection or strict security rules.',
|
|
90
|
+
shortLabel: 'Request blocked',
|
|
91
|
+
},
|
|
92
|
+
FIGMA_ACCESS_DENIED: {
|
|
93
|
+
retryable: false,
|
|
94
|
+
userMessage: 'Figma file not accessible. Make sure you have View or Edit access and the file is not restricted.',
|
|
95
|
+
shortLabel: 'Figma access denied',
|
|
96
|
+
},
|
|
97
|
+
FIGMA_TOKEN_EXPIRED: {
|
|
98
|
+
retryable: false,
|
|
99
|
+
userMessage: 'Your Figma session has expired. Sign out and sign back in to reconnect Figma.',
|
|
100
|
+
shortLabel: 'Figma session expired',
|
|
101
|
+
},
|
|
102
|
+
FIGMA_NOT_FOUND: {
|
|
103
|
+
retryable: false,
|
|
104
|
+
userMessage: "Figma file not found. Check the URL — the file may have been deleted or moved.",
|
|
105
|
+
shortLabel: 'Figma file not found',
|
|
106
|
+
},
|
|
107
|
+
FIGMA_RATE_LIMITED: {
|
|
108
|
+
retryable: true,
|
|
109
|
+
userMessage: 'Figma rate limit hit. Wait 1–2 minutes, then try again.',
|
|
110
|
+
shortLabel: 'Figma rate limit',
|
|
111
|
+
},
|
|
112
|
+
PIPELINE_TIMEOUT: {
|
|
113
|
+
retryable: true,
|
|
114
|
+
userMessage: 'Validation took too long to respond. Try again in a moment.',
|
|
115
|
+
shortLabel: 'Validation timed out',
|
|
116
|
+
},
|
|
117
|
+
PIPELINE_ERROR: {
|
|
118
|
+
retryable: true,
|
|
119
|
+
userMessage: "Validation failed. Try again — if it keeps failing, contact support.",
|
|
120
|
+
shortLabel: 'Validation failed',
|
|
121
|
+
},
|
|
122
|
+
ZERO_ELEMENTS_MATCHED: {
|
|
123
|
+
retryable: false,
|
|
124
|
+
userMessage: 'No page elements were found during capture. Make sure the page loads correctly and is not blank.',
|
|
125
|
+
shortLabel: 'No elements found',
|
|
126
|
+
},
|
|
127
|
+
PERSIST_FAILED: {
|
|
128
|
+
retryable: true,
|
|
129
|
+
userMessage: 'The results were generated but could not be saved. Try again in a moment.',
|
|
130
|
+
shortLabel: 'Save failed',
|
|
131
|
+
},
|
|
132
|
+
SESSION_EXPIRED: {
|
|
133
|
+
retryable: false,
|
|
134
|
+
userMessage: "This site's saved session has expired. Reconnect it to validate again.",
|
|
135
|
+
shortLabel: 'Session expired',
|
|
136
|
+
},
|
|
137
|
+
INVALID_REFERENCE_URL: {
|
|
138
|
+
retryable: false,
|
|
139
|
+
userMessage: 'The reference URL is invalid. It must start with https:// and point to a real page.',
|
|
140
|
+
shortLabel: 'Invalid reference URL',
|
|
141
|
+
},
|
|
142
|
+
INVALID_TARGET_URL: {
|
|
143
|
+
retryable: false,
|
|
144
|
+
userMessage: 'The target URL is invalid. It must start with https:// and point to a real page.',
|
|
145
|
+
shortLabel: 'Invalid target URL',
|
|
146
|
+
},
|
|
147
|
+
DESIGN_SYSTEM_NOT_AVAILABLE: {
|
|
148
|
+
retryable: false,
|
|
149
|
+
userMessage: "Design system unavailable. Connect a repo with a parsed theme to use this validation mode.",
|
|
150
|
+
shortLabel: 'Design system unavailable',
|
|
151
|
+
},
|
|
152
|
+
DESIGN_SYSTEM_CONTEXT_NOT_AVAILABLE: {
|
|
153
|
+
retryable: false,
|
|
154
|
+
userMessage: "Design system unavailable. The selected brand context is not available. Choose another or reconnect it in Settings.",
|
|
155
|
+
shortLabel: 'Brand context unavailable',
|
|
156
|
+
},
|
|
157
|
+
DESIGN_SYSTEM_INCOMPATIBLE: {
|
|
158
|
+
retryable: false,
|
|
159
|
+
userMessage: "This design system can't be checked yet. We support Tailwind + shadcn today and are adding more — reconnect a supported design system, or check back soon.",
|
|
160
|
+
shortLabel: 'Design system not supported yet',
|
|
161
|
+
},
|
|
162
|
+
FLOW_STEP_FAILED: {
|
|
163
|
+
retryable: false,
|
|
164
|
+
userMessage: 'A flow validation step failed. Check that all prototype transitions in the Figma file point to valid frames.',
|
|
165
|
+
shortLabel: 'Flow step failed',
|
|
166
|
+
},
|
|
167
|
+
UNKNOWN_ERROR: {
|
|
168
|
+
retryable: true,
|
|
169
|
+
userMessage: 'Something went wrong during validation. Try again — if it keeps failing, contact support.',
|
|
170
|
+
shortLabel: 'Unknown error',
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Map a raw Snapshot Lambda error string to a typed RunErrorCode.
|
|
175
|
+
*
|
|
176
|
+
* Input format from Snapshot Lambda: "SNAPSHOT_ERROR:<status>:<detail>"
|
|
177
|
+
* where detail contains Chromium net::ERR_* codes.
|
|
178
|
+
*/
|
|
179
|
+
export function mapSnapshotErrorToCode(rawError) {
|
|
180
|
+
if (!rawError) {
|
|
181
|
+
return { code: 'TARGET_UNREACHABLE', ...meta('TARGET_UNREACHABLE') };
|
|
182
|
+
}
|
|
183
|
+
if (!rawError.includes('SNAPSHOT_ERROR')) {
|
|
184
|
+
// Not a structured snapshot error — treat as transient pipeline error
|
|
185
|
+
if (rawError.toLowerCase().includes('timeout') || rawError.toLowerCase().includes('timed out')) {
|
|
186
|
+
return { code: 'PIPELINE_TIMEOUT', ...meta('PIPELINE_TIMEOUT') };
|
|
187
|
+
}
|
|
188
|
+
return { code: 'PIPELINE_ERROR', ...meta('PIPELINE_ERROR') };
|
|
189
|
+
}
|
|
190
|
+
if (rawError.includes('ERR_INVALID_AUTH_CREDENTIALS')) {
|
|
191
|
+
return { code: 'TARGET_AUTH_WALL', ...meta('TARGET_AUTH_WALL') };
|
|
192
|
+
}
|
|
193
|
+
if (rawError.includes('ERR_NAME_NOT_RESOLVED')) {
|
|
194
|
+
return { code: 'TARGET_UNREACHABLE', message: "We couldn't find that domain. Check the URL for typos.", retryable: false };
|
|
195
|
+
}
|
|
196
|
+
if (rawError.includes('ERR_CONNECTION_REFUSED') ||
|
|
197
|
+
rawError.includes('ERR_CONNECTION_RESET') ||
|
|
198
|
+
rawError.includes('ERR_CONNECTION_CLOSED') ||
|
|
199
|
+
rawError.includes('ERR_ADDRESS_UNREACHABLE')) {
|
|
200
|
+
return { code: 'TARGET_UNREACHABLE', message: "The page didn't respond. Check that the URL is reachable from the public internet.", retryable: false };
|
|
201
|
+
}
|
|
202
|
+
if (rawError.includes('ERR_CONNECTION_TIMED_OUT') ||
|
|
203
|
+
rawError.includes('ERR_TIMED_OUT') ||
|
|
204
|
+
rawError.includes('Timeout')) {
|
|
205
|
+
return { code: 'TARGET_TIMEOUT', ...meta('TARGET_TIMEOUT') };
|
|
206
|
+
}
|
|
207
|
+
if (rawError.includes('ERR_CERT_') || rawError.includes('ERR_SSL_')) {
|
|
208
|
+
return { code: 'TARGET_UNREACHABLE', message: "The page has an SSL certificate problem we can't bypass. Check the URL or the certificate.", retryable: false };
|
|
209
|
+
}
|
|
210
|
+
if (rawError.includes('ERR_TOO_MANY_REDIRECTS')) {
|
|
211
|
+
return { code: 'TARGET_UNREACHABLE', message: 'The page is stuck in a redirect loop. Try linking directly to the final URL.', retryable: false };
|
|
212
|
+
}
|
|
213
|
+
if (rawError.includes('ERR_HTTP_RESPONSE_CODE_FAILURE') || rawError.includes('ERR_ABORTED')) {
|
|
214
|
+
return { code: 'TARGET_UNREACHABLE', message: "The page wouldn't load. Double-check the URL and make sure the page is live.", retryable: false };
|
|
215
|
+
}
|
|
216
|
+
if (rawError.includes('ERR_BLOCKED_BY_CLIENT') || rawError.includes('ERR_BLOCKED_BY_RESPONSE')) {
|
|
217
|
+
return { code: 'TARGET_CSP_BLOCKED', ...meta('TARGET_CSP_BLOCKED') };
|
|
218
|
+
}
|
|
219
|
+
if (rawError.includes('SNAPSHOT_ERROR:400')) {
|
|
220
|
+
// Lambda SSRF guard rejected the URL (private IP, localhost, etc.)
|
|
221
|
+
return { code: 'TARGET_UNREACHABLE', message: "That URL isn't reachable from the public internet. Use a live or staging URL with a public domain.", retryable: false };
|
|
222
|
+
}
|
|
223
|
+
if (rawError.includes('SNAPSHOT_ERROR:408') || rawError.includes('SNAPSHOT_ERROR:504')) {
|
|
224
|
+
return { code: 'TARGET_TIMEOUT', ...meta('TARGET_TIMEOUT') };
|
|
225
|
+
}
|
|
226
|
+
if (rawError.includes('SNAPSHOT_ERROR:422')) {
|
|
227
|
+
return { code: 'TARGET_UNREACHABLE', message: "We couldn't load that page. Double-check the URL is correct and publicly reachable.", retryable: false };
|
|
228
|
+
}
|
|
229
|
+
if (rawError.includes('SNAPSHOT_ERROR:5')) {
|
|
230
|
+
// 5xx from snapshot lambda
|
|
231
|
+
return { code: 'PIPELINE_ERROR', ...meta('PIPELINE_ERROR') };
|
|
232
|
+
}
|
|
233
|
+
return { code: 'TARGET_UNREACHABLE', message: "We couldn't load that page. Double-check the URL is correct and publicly reachable.", retryable: false };
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Map a raw Figma fetch error string to a typed RunErrorCode.
|
|
237
|
+
*/
|
|
238
|
+
export function mapFigmaErrorToCode(rawError) {
|
|
239
|
+
if (rawError.includes('FIGMA_ACCESS_DENIED')) {
|
|
240
|
+
return { code: 'FIGMA_ACCESS_DENIED', ...meta('FIGMA_ACCESS_DENIED') };
|
|
241
|
+
}
|
|
242
|
+
if (rawError.includes('FIGMA_RATE_LIMIT') || rawError.includes('FIGMA_RATE_LIMITED')) {
|
|
243
|
+
return { code: 'FIGMA_RATE_LIMITED', ...meta('FIGMA_RATE_LIMITED') };
|
|
244
|
+
}
|
|
245
|
+
if (rawError.includes('FIGMA_NOT_FOUND')) {
|
|
246
|
+
return { code: 'FIGMA_NOT_FOUND', ...meta('FIGMA_NOT_FOUND') };
|
|
247
|
+
}
|
|
248
|
+
if (rawError.includes('FIGMA_SESSION_EXPIRED') || rawError.includes('FIGMA_REFRESH_FAILED') || rawError.includes('session expired')) {
|
|
249
|
+
return { code: 'FIGMA_TOKEN_EXPIRED', ...meta('FIGMA_TOKEN_EXPIRED') };
|
|
250
|
+
}
|
|
251
|
+
// Generic Figma error
|
|
252
|
+
return { code: 'FIGMA_ACCESS_DENIED', message: 'Figma file not accessible. Ensure you have View or Edit access.', retryable: false };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Derive a user-facing display object from a validation_runs row.
|
|
256
|
+
* Used by webapp, GitHub Action comment formatter, and the runner.
|
|
257
|
+
*/
|
|
258
|
+
export function resolveRunDisplay(run) {
|
|
259
|
+
if (run.status !== 'error') {
|
|
260
|
+
return { title: 'Validation complete', description: '', retryable: false };
|
|
261
|
+
}
|
|
262
|
+
const code = (run.error_code ?? 'UNKNOWN_ERROR');
|
|
263
|
+
const known = ERROR_CODE_META[code] ?? ERROR_CODE_META['UNKNOWN_ERROR'];
|
|
264
|
+
const retryable = run.is_retryable ?? known.retryable;
|
|
265
|
+
let action;
|
|
266
|
+
if (code === 'FIGMA_TOKEN_EXPIRED')
|
|
267
|
+
action = 'reconnect_figma';
|
|
268
|
+
else if (code === 'SESSION_EXPIRED')
|
|
269
|
+
action = 'reconnect_environment';
|
|
270
|
+
else if (code === 'TARGET_AUTH_WALL' || code === 'TARGET_UNREACHABLE' || code === 'TARGET_CSP_BLOCKED')
|
|
271
|
+
action = 'check_url';
|
|
272
|
+
return {
|
|
273
|
+
title: known.shortLabel,
|
|
274
|
+
description: known.userMessage,
|
|
275
|
+
retryable,
|
|
276
|
+
action,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
// ── Internal helper ────────────────────────────────────────────────────────────
|
|
280
|
+
function meta(code) {
|
|
281
|
+
const m = ERROR_CODE_META[code];
|
|
282
|
+
return { message: m.userMessage, retryable: m.retryable };
|
|
283
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"//repository": [
|
|
3
|
+
"`repository` points at usefidel/fidel-contracts-publisher, the founder-only repo",
|
|
4
|
+
"that publishes this package. That repo is PRIVATE, so this link 404s for the",
|
|
5
|
+
"public \u2014 it records provenance, not a browsable source location. The package",
|
|
6
|
+
"CONTENTS are public; publication AUTHORITY is not.",
|
|
7
|
+
"The source of truth still lives in the monorepo at packages/contracts/."
|
|
8
|
+
],
|
|
9
|
+
"name": "@usefidel/contracts",
|
|
10
|
+
"version": "0.1.0",
|
|
11
|
+
"description": "Shared, code-free contracts between Fidel surfaces. Run-error taxonomy only.",
|
|
12
|
+
"license": "UNLICENSED",
|
|
13
|
+
"private": false,
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./run-errors": {
|
|
24
|
+
"types": "./dist/run-errors.d.ts",
|
|
25
|
+
"import": "./dist/run-errors.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"sideEffects": false,
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/usefidel/fidel-contracts-publisher.git"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.json",
|
|
41
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"check:canonical": "node scripts/check-canonical-sync.mjs"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"typescript": "^5.6.3",
|
|
47
|
+
"vitest": "^3.2.4"
|
|
48
|
+
}
|
|
49
|
+
}
|