@still-running/health-check 1.0.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/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/adapters/make.d.ts +14 -0
- package/dist/adapters/make.js +213 -0
- package/dist/adapters/n8n.d.ts +9 -0
- package/dist/adapters/n8n.js +273 -0
- package/dist/core/checks/others.d.ts +10 -0
- package/dist/core/checks/others.js +160 -0
- package/dist/core/checks/protections.d.ts +11 -0
- package/dist/core/checks/protections.js +96 -0
- package/dist/core/checks/zero-write.d.ts +27 -0
- package/dist/core/checks/zero-write.js +245 -0
- package/dist/core/model.d.ts +141 -0
- package/dist/core/model.js +19 -0
- package/dist/core/providers.d.ts +48 -0
- package/dist/core/providers.js +181 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +68 -0
- package/dist/package.json +1 -0
- package/package.json +34 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-neutral workflow model.
|
|
3
|
+
*
|
|
4
|
+
* Every adapter (n8n, Make, and whatever comes next) produces this shape and
|
|
5
|
+
* nothing else. Every check consumes this shape and nothing else. No check may
|
|
6
|
+
* import an adapter, and no check may branch on `platform` except to word a
|
|
7
|
+
* message. That rule is the whole architecture constraint: adding platform
|
|
8
|
+
* three is one new file in adapters/, zero changes in core/checks/.
|
|
9
|
+
*/
|
|
10
|
+
export type Platform = 'n8n' | 'make';
|
|
11
|
+
export type NodeRole = 'trigger' | 'write' | 'read' | 'gate' | 'transform' | 'loop' | 'alert' | 'error-handler' | 'note' | 'other';
|
|
12
|
+
export type WriteKind = 'append' | 'create' | 'update' | 'upsert' | 'delete' | 'send' | 'unknown';
|
|
13
|
+
export type AuthKind = 'oauth2' | 'api-key' | 'basic' | 'service-account' | 'unknown';
|
|
14
|
+
export interface CredentialRef {
|
|
15
|
+
/** Raw platform identifier, e.g. 'googleSheetsOAuth2Api' or 'account:google'. */
|
|
16
|
+
rawType: string;
|
|
17
|
+
/** Provider id resolved against the provider table, e.g. 'google'. null = unknown. */
|
|
18
|
+
providerId: string | null;
|
|
19
|
+
authKind: AuthKind;
|
|
20
|
+
/** The user's own label for the connection, if the export carries one. */
|
|
21
|
+
label?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Why a node might hand nothing to the step after it. This is the heart of the
|
|
25
|
+
* zero-write check: `null` means the node always emits something.
|
|
26
|
+
*/
|
|
27
|
+
export interface ZeroEmit {
|
|
28
|
+
/** Plain-language cause, shown to the user verbatim. */
|
|
29
|
+
cause: string;
|
|
30
|
+
/**
|
|
31
|
+
* 'structural' - the shape guarantees it can emit zero (a filter, a search)
|
|
32
|
+
* 'possible' - depends on code we cannot read (a Code node, an expression)
|
|
33
|
+
*/
|
|
34
|
+
certainty: 'structural' | 'possible';
|
|
35
|
+
}
|
|
36
|
+
export interface WorkflowNode {
|
|
37
|
+
id: string;
|
|
38
|
+
label: string;
|
|
39
|
+
/** Raw platform type string, kept for messages and for debugging. */
|
|
40
|
+
platformType: string;
|
|
41
|
+
role: NodeRole;
|
|
42
|
+
writeKind?: WriteKind;
|
|
43
|
+
/** What the write lands in, when we can name it: 'Google Sheets', 'Airtable'. */
|
|
44
|
+
writeTarget?: string;
|
|
45
|
+
zeroEmit: ZeroEmit | null;
|
|
46
|
+
credentials: CredentialRef[];
|
|
47
|
+
errorHandling: {
|
|
48
|
+
/** An error branch or error route leaves this node. */
|
|
49
|
+
hasErrorBranch: boolean;
|
|
50
|
+
retries: boolean;
|
|
51
|
+
continueOnFail: boolean;
|
|
52
|
+
/** n8n: forces an empty item downstream instead of stopping. */
|
|
53
|
+
alwaysOutputData: boolean;
|
|
54
|
+
};
|
|
55
|
+
disabled: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface WorkflowEdge {
|
|
58
|
+
from: string;
|
|
59
|
+
to: string;
|
|
60
|
+
/** 'main' | 'error' | a branch label such as 'false' or a Make route filter name. */
|
|
61
|
+
channel: string;
|
|
62
|
+
/**
|
|
63
|
+
* A condition sitting on this edge that can stop everything downstream.
|
|
64
|
+
* Make puts filters on the link; n8n puts them in nodes. Both normalise here.
|
|
65
|
+
*/
|
|
66
|
+
gate: {
|
|
67
|
+
label: string;
|
|
68
|
+
cause: string;
|
|
69
|
+
} | null;
|
|
70
|
+
}
|
|
71
|
+
export interface Cadence {
|
|
72
|
+
kind: 'schedule' | 'event' | 'manual' | 'unknown';
|
|
73
|
+
/** Human text: 'every 15 minutes', 'on webhook call'. */
|
|
74
|
+
description: string;
|
|
75
|
+
/** Seconds between expected runs, when the export states it. */
|
|
76
|
+
intervalSeconds: number | null;
|
|
77
|
+
/**
|
|
78
|
+
* True when the export tells us how often this SHOULD run. If false, nobody
|
|
79
|
+
* can tell the difference between idle and dead.
|
|
80
|
+
*/
|
|
81
|
+
expectedIntervalKnown: boolean;
|
|
82
|
+
}
|
|
83
|
+
export interface WorkflowErrorPolicy {
|
|
84
|
+
/** A workflow-level handler: n8n settings.errorWorkflow, or an Error Trigger. */
|
|
85
|
+
workflowLevelHandler: boolean;
|
|
86
|
+
/** Make: metadata.scenario.dlq — are failed runs even stored? null = unknown. */
|
|
87
|
+
storesFailedRuns: boolean | null;
|
|
88
|
+
maxErrors: number | null;
|
|
89
|
+
notes: string[];
|
|
90
|
+
}
|
|
91
|
+
export interface Workflow {
|
|
92
|
+
platform: Platform;
|
|
93
|
+
name: string;
|
|
94
|
+
nodes: WorkflowNode[];
|
|
95
|
+
edges: WorkflowEdge[];
|
|
96
|
+
triggerIds: string[];
|
|
97
|
+
cadence: Cadence;
|
|
98
|
+
errorPolicy: WorkflowErrorPolicy;
|
|
99
|
+
/** Adapter-level caveats, e.g. 'this export carries no connection data'. */
|
|
100
|
+
parseNotes: string[];
|
|
101
|
+
}
|
|
102
|
+
export type Severity = 'critical' | 'high' | 'medium' | 'low' | 'info';
|
|
103
|
+
export interface Finding {
|
|
104
|
+
checkId: 'zero-write' | 'credential-expiry' | 'no-cadence' | 'error-handling';
|
|
105
|
+
severity: Severity;
|
|
106
|
+
/** The node this is about. Findings always name a node — no vague advice. */
|
|
107
|
+
nodeId: string | null;
|
|
108
|
+
nodeLabel: string | null;
|
|
109
|
+
title: string;
|
|
110
|
+
/** What happens if this goes quiet. The spec's requirement, not decoration. */
|
|
111
|
+
ifItGoesQuiet: string;
|
|
112
|
+
detail: string;
|
|
113
|
+
/** Only for things we cannot know from static JSON. Keeps us honest. */
|
|
114
|
+
howToCheck?: string;
|
|
115
|
+
sources?: string[];
|
|
116
|
+
}
|
|
117
|
+
export interface Protection {
|
|
118
|
+
kind: 'guarded-write' | 'error-handler' | 'known-cadence' | 'stores-failed-runs' | 'alert-branch' | 'retries';
|
|
119
|
+
nodeId: string | null;
|
|
120
|
+
nodeLabel: string | null;
|
|
121
|
+
title: string;
|
|
122
|
+
detail: string;
|
|
123
|
+
}
|
|
124
|
+
export interface AnalysisResult {
|
|
125
|
+
platform: Platform;
|
|
126
|
+
workflowName: string;
|
|
127
|
+
nodeCount: number;
|
|
128
|
+
findings: Finding[];
|
|
129
|
+
/** What is holding it together. Shown when little or nothing is wrong. */
|
|
130
|
+
protections: Protection[];
|
|
131
|
+
parseNotes: string[];
|
|
132
|
+
/** Counters only. These three numbers are the Phase E scoreboard. */
|
|
133
|
+
stats: {
|
|
134
|
+
writeNodes: number;
|
|
135
|
+
oauthCredentials: number;
|
|
136
|
+
triggers: number;
|
|
137
|
+
/** Credentials that do not expire on a clock. A zero here is information. */
|
|
138
|
+
staticKeyCredentials: number;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
export declare const SEVERITY_ORDER: Record<Severity, number>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Platform-neutral workflow model.
|
|
4
|
+
*
|
|
5
|
+
* Every adapter (n8n, Make, and whatever comes next) produces this shape and
|
|
6
|
+
* nothing else. Every check consumes this shape and nothing else. No check may
|
|
7
|
+
* import an adapter, and no check may branch on `platform` except to word a
|
|
8
|
+
* message. That rule is the whole architecture constraint: adding platform
|
|
9
|
+
* three is one new file in adapters/, zero changes in core/checks/.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.SEVERITY_ORDER = void 0;
|
|
13
|
+
exports.SEVERITY_ORDER = {
|
|
14
|
+
critical: 0,
|
|
15
|
+
high: 1,
|
|
16
|
+
medium: 2,
|
|
17
|
+
low: 3,
|
|
18
|
+
info: 4,
|
|
19
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider expiry table.
|
|
3
|
+
*
|
|
4
|
+
* This is a DATA ASSET, not a switch statement. It is the seed of the paid
|
|
5
|
+
* product: the free tool reads it to say "this could expire", the paid product
|
|
6
|
+
* reads the same table plus a live connection to say "this expires Tuesday".
|
|
7
|
+
*
|
|
8
|
+
* Rules for editing this file:
|
|
9
|
+
* 1. Every `window` and every `condition` needs a `sources` URL. No folklore.
|
|
10
|
+
* 2. If we cannot know the condition from a pasted JSON, say so in `certainty:
|
|
11
|
+
* 'conditional'` and give `howToCheck`. Never predict what we cannot see.
|
|
12
|
+
* 3. Adding a provider must never require touching a check.
|
|
13
|
+
*/
|
|
14
|
+
import type { AuthKind } from './model.js';
|
|
15
|
+
export interface ExpiryRule {
|
|
16
|
+
/** The situation in which this window applies, in plain words. */
|
|
17
|
+
condition: string;
|
|
18
|
+
/** 'certain' - true for every connection of this type.
|
|
19
|
+
* 'conditional' - depends on something not visible in the export. */
|
|
20
|
+
certainty: 'certain' | 'conditional';
|
|
21
|
+
/** e.g. '7 days', '60 days', 'never'. */
|
|
22
|
+
window: string;
|
|
23
|
+
detail: string;
|
|
24
|
+
/** How the reader confirms it themselves, in under a minute. */
|
|
25
|
+
howToCheck?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface Provider {
|
|
28
|
+
id: string;
|
|
29
|
+
displayName: string;
|
|
30
|
+
/** Substring matchers against the raw credential type, lower-cased. */
|
|
31
|
+
match: {
|
|
32
|
+
n8n: string[];
|
|
33
|
+
make: string[];
|
|
34
|
+
};
|
|
35
|
+
defaultAuthKind: AuthKind;
|
|
36
|
+
/** Can the platform silently refresh this without a human? */
|
|
37
|
+
autoRefreshable: boolean;
|
|
38
|
+
rules: ExpiryRule[];
|
|
39
|
+
sources: string[];
|
|
40
|
+
/** Complaint Mine Log row numbers backing this entry. Evidence, not vibes. */
|
|
41
|
+
complaintLogRows?: number[];
|
|
42
|
+
}
|
|
43
|
+
export declare const PROVIDERS: Provider[];
|
|
44
|
+
/** Credential types that never expire on a clock — they get revoked instead. */
|
|
45
|
+
export declare const STATIC_KEY_HINTS: string[];
|
|
46
|
+
export declare function resolveProvider(rawType: string, platform: 'n8n' | 'make'): Provider | null;
|
|
47
|
+
/** Best-effort auth kind when no provider matches. */
|
|
48
|
+
export declare function guessAuthKind(rawType: string): AuthKind;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Provider expiry table.
|
|
4
|
+
*
|
|
5
|
+
* This is a DATA ASSET, not a switch statement. It is the seed of the paid
|
|
6
|
+
* product: the free tool reads it to say "this could expire", the paid product
|
|
7
|
+
* reads the same table plus a live connection to say "this expires Tuesday".
|
|
8
|
+
*
|
|
9
|
+
* Rules for editing this file:
|
|
10
|
+
* 1. Every `window` and every `condition` needs a `sources` URL. No folklore.
|
|
11
|
+
* 2. If we cannot know the condition from a pasted JSON, say so in `certainty:
|
|
12
|
+
* 'conditional'` and give `howToCheck`. Never predict what we cannot see.
|
|
13
|
+
* 3. Adding a provider must never require touching a check.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.STATIC_KEY_HINTS = exports.PROVIDERS = void 0;
|
|
17
|
+
exports.resolveProvider = resolveProvider;
|
|
18
|
+
exports.guessAuthKind = guessAuthKind;
|
|
19
|
+
exports.PROVIDERS = [
|
|
20
|
+
{
|
|
21
|
+
id: 'google',
|
|
22
|
+
displayName: 'Google',
|
|
23
|
+
match: {
|
|
24
|
+
n8n: [
|
|
25
|
+
'googlesheetsoauth2',
|
|
26
|
+
'googledriveoauth2',
|
|
27
|
+
'gmailoauth2',
|
|
28
|
+
'googlecalendaroauth2',
|
|
29
|
+
'googledocsoauth2',
|
|
30
|
+
'googleoauth2',
|
|
31
|
+
'gsuiteadminoauth2',
|
|
32
|
+
'googlebigqueryoauth2',
|
|
33
|
+
'googleanalyticsoauth2',
|
|
34
|
+
],
|
|
35
|
+
make: ['account:google', 'google'],
|
|
36
|
+
},
|
|
37
|
+
defaultAuthKind: 'oauth2',
|
|
38
|
+
autoRefreshable: true,
|
|
39
|
+
rules: [
|
|
40
|
+
{
|
|
41
|
+
condition: 'The Google Cloud project behind this connection has its OAuth consent screen set to "Testing" with an External user type',
|
|
42
|
+
certainty: 'conditional',
|
|
43
|
+
window: '7 days',
|
|
44
|
+
detail: 'Google issues a refresh token that expires 7 days after consent. When it dies the platform gets invalid_grant, the trigger stops firing, and nothing throws a run-level error because there is no run.',
|
|
45
|
+
howToCheck: 'Google Cloud Console -> APIs & Services -> OAuth consent screen. If Publishing status says "Testing", this connection dies every 7 days. If it says "In production", it does not.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
condition: 'The consent screen is published to production and verified',
|
|
49
|
+
certainty: 'conditional',
|
|
50
|
+
window: 'indefinite, with five exceptions',
|
|
51
|
+
detail: 'Effectively permanent unless: the token goes unused for six months, the user revokes access, the user changes their password while Gmail scopes are granted, the per-user token cap is exceeded, or the app loses verification for sensitive scopes.',
|
|
52
|
+
howToCheck: 'The six-month rule matters here: a workflow that stops running also stops refreshing, so a quiet workflow eventually becomes a dead credential.',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
sources: [
|
|
56
|
+
'https://developers.google.com/identity/protocols/oauth2',
|
|
57
|
+
'https://support.google.com/cloud/answer/15549945',
|
|
58
|
+
],
|
|
59
|
+
complaintLogRows: [9, 11, 12, 40, 41],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'microsoft',
|
|
63
|
+
displayName: 'Microsoft',
|
|
64
|
+
match: {
|
|
65
|
+
n8n: [
|
|
66
|
+
'microsoftoauth2',
|
|
67
|
+
'microsoftoutlookoauth2',
|
|
68
|
+
'microsoftexceloauth2',
|
|
69
|
+
'microsoftonedriveoauth2',
|
|
70
|
+
'microsoftsharepointoauth2',
|
|
71
|
+
'microsoftteamsoauth2',
|
|
72
|
+
'microsoftgraphsecurityoauth2',
|
|
73
|
+
'azure',
|
|
74
|
+
],
|
|
75
|
+
make: ['account:microsoft', 'microsoft', 'office365'],
|
|
76
|
+
},
|
|
77
|
+
defaultAuthKind: 'oauth2',
|
|
78
|
+
autoRefreshable: true,
|
|
79
|
+
rules: [
|
|
80
|
+
{
|
|
81
|
+
condition: 'Normal case — the workflow runs at least every 90 days',
|
|
82
|
+
certainty: 'certain',
|
|
83
|
+
window: 'effectively indefinite',
|
|
84
|
+
detail: 'Microsoft refresh tokens default to a 90-day inactivity limit and replace themselves every time they are used. A workflow that runs daily keeps resetting the clock, so this is lower risk than Google.',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
condition: 'The workflow stops running for 90 days',
|
|
88
|
+
certainty: 'certain',
|
|
89
|
+
window: '90 days of inactivity',
|
|
90
|
+
detail: 'The refresh token expires from disuse. Note the trap: a workflow that already went quiet for another reason quietly becomes unrecoverable too, so a short outage turns into a manual re-auth.',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
condition: 'The tenant applies a Conditional Access sign-in frequency policy',
|
|
94
|
+
certainty: 'conditional',
|
|
95
|
+
window: 'whatever the policy says',
|
|
96
|
+
detail: 'Since January 2021 refresh lifetimes are no longer configurable through token lifetime policies, but Conditional Access sign-in frequency still forces re-authentication on a schedule the client cannot see.',
|
|
97
|
+
howToCheck: 'Ask the tenant admin whether a sign-in frequency policy applies to this app. It is not visible from the automation side.',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
sources: [
|
|
101
|
+
'https://learn.microsoft.com/en-us/entra/identity-platform/refresh-tokens',
|
|
102
|
+
'https://learn.microsoft.com/en-us/entra/identity-platform/configurable-token-lifetimes',
|
|
103
|
+
],
|
|
104
|
+
complaintLogRows: [14],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'meta-whatsapp',
|
|
108
|
+
displayName: 'WhatsApp / Meta',
|
|
109
|
+
match: {
|
|
110
|
+
n8n: ['whatsapp', 'facebookgraph', 'facebookapp'],
|
|
111
|
+
make: ['account:whatsapp', 'account:facebook', 'whatsapp', 'facebook'],
|
|
112
|
+
},
|
|
113
|
+
defaultAuthKind: 'api-key',
|
|
114
|
+
// The decisive fact: this is a bearer token pasted by hand. Nothing refreshes it.
|
|
115
|
+
autoRefreshable: false,
|
|
116
|
+
rules: [
|
|
117
|
+
{
|
|
118
|
+
condition: 'The token was copied from the Meta app dashboard for testing',
|
|
119
|
+
certainty: 'conditional',
|
|
120
|
+
window: 'under 24 hours',
|
|
121
|
+
detail: 'Temporary access tokens expire in less than a day. Anyone who set this up while testing and never went back has a workflow that died the next morning.',
|
|
122
|
+
howToCheck: 'If the token came from the "Temporary access token" box on the app dashboard, it is already gone. Only a System User token survives.',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
condition: 'A System User token was generated with a 60-day expiry',
|
|
126
|
+
certainty: 'conditional',
|
|
127
|
+
window: '60 days',
|
|
128
|
+
detail: 'Meta lets you pick the expiry when generating a System User token. 60 days is the common choice and there is no warning before it lapses.',
|
|
129
|
+
howToCheck: 'Meta Business Settings -> Users -> System Users -> your user -> the token list shows the expiry you chose.',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
condition: 'A System User token was generated with no expiry',
|
|
133
|
+
certainty: 'conditional',
|
|
134
|
+
window: 'never',
|
|
135
|
+
detail: 'Permanent until someone revokes it manually.',
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
sources: [
|
|
139
|
+
'https://developers.facebook.com/blog/post/2022/12/05/auth-tokens/',
|
|
140
|
+
'https://community.n8n.io/t/whatsapp-token-expires/182022',
|
|
141
|
+
],
|
|
142
|
+
complaintLogRows: [13],
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
/** Credential types that never expire on a clock — they get revoked instead. */
|
|
146
|
+
exports.STATIC_KEY_HINTS = [
|
|
147
|
+
'apikey',
|
|
148
|
+
'api',
|
|
149
|
+
'token',
|
|
150
|
+
'httpheaderauth',
|
|
151
|
+
'httpbasicauth',
|
|
152
|
+
'httpqueryauth',
|
|
153
|
+
'smtp',
|
|
154
|
+
'postgres',
|
|
155
|
+
'mysql',
|
|
156
|
+
'redis',
|
|
157
|
+
'mongodb',
|
|
158
|
+
];
|
|
159
|
+
function resolveProvider(rawType, platform) {
|
|
160
|
+
const needle = rawType.toLowerCase();
|
|
161
|
+
for (const p of exports.PROVIDERS) {
|
|
162
|
+
for (const m of p.match[platform]) {
|
|
163
|
+
if (needle.includes(m))
|
|
164
|
+
return p;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
/** Best-effort auth kind when no provider matches. */
|
|
170
|
+
function guessAuthKind(rawType) {
|
|
171
|
+
const t = rawType.toLowerCase();
|
|
172
|
+
if (t.includes('oauth2') || t.includes('oauth'))
|
|
173
|
+
return 'oauth2';
|
|
174
|
+
if (t.includes('basicauth'))
|
|
175
|
+
return 'basic';
|
|
176
|
+
if (t.includes('serviceaccount') || t === 'googleapi')
|
|
177
|
+
return 'service-account';
|
|
178
|
+
if (exports.STATIC_KEY_HINTS.some((h) => t.includes(h)))
|
|
179
|
+
return 'api-key';
|
|
180
|
+
return 'unknown';
|
|
181
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stillrunning.dev — Workflow Health Check
|
|
3
|
+
*
|
|
4
|
+
* Public entry point. Everything here runs in the browser: no network calls, no
|
|
5
|
+
* storage, nothing leaves the page. The only thing the server ever learns is
|
|
6
|
+
* three counters (pastes, results shown, emails), sent separately.
|
|
7
|
+
*
|
|
8
|
+
* Check order is deliberately inverted from the original spec. Zero-write and
|
|
9
|
+
* credential expiry lead because they are the two nobody else does; error
|
|
10
|
+
* handling is last because six free tools already ship it.
|
|
11
|
+
*/
|
|
12
|
+
import type { AnalysisResult, Workflow } from './core/model.js';
|
|
13
|
+
export declare class UnknownFormatError extends Error {
|
|
14
|
+
constructor();
|
|
15
|
+
}
|
|
16
|
+
export declare function parseWorkflow(input: string | object): Workflow;
|
|
17
|
+
export declare function analyze(input: string | object): AnalysisResult;
|
|
18
|
+
export type { AnalysisResult, Finding, Protection, Severity, Workflow, WorkflowNode, CredentialRef, } from './core/model.js';
|
|
19
|
+
export { PROVIDERS } from './core/providers.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* stillrunning.dev — Workflow Health Check
|
|
4
|
+
*
|
|
5
|
+
* Public entry point. Everything here runs in the browser: no network calls, no
|
|
6
|
+
* storage, nothing leaves the page. The only thing the server ever learns is
|
|
7
|
+
* three counters (pastes, results shown, emails), sent separately.
|
|
8
|
+
*
|
|
9
|
+
* Check order is deliberately inverted from the original spec. Zero-write and
|
|
10
|
+
* credential expiry lead because they are the two nobody else does; error
|
|
11
|
+
* handling is last because six free tools already ship it.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.PROVIDERS = exports.UnknownFormatError = void 0;
|
|
15
|
+
exports.parseWorkflow = parseWorkflow;
|
|
16
|
+
exports.analyze = analyze;
|
|
17
|
+
const model_js_1 = require("./core/model.js");
|
|
18
|
+
const zero_write_js_1 = require("./core/checks/zero-write.js");
|
|
19
|
+
const others_js_1 = require("./core/checks/others.js");
|
|
20
|
+
const protections_js_1 = require("./core/checks/protections.js");
|
|
21
|
+
const n8n_js_1 = require("./adapters/n8n.js");
|
|
22
|
+
const make_js_1 = require("./adapters/make.js");
|
|
23
|
+
class UnknownFormatError extends Error {
|
|
24
|
+
constructor() {
|
|
25
|
+
super('That does not look like an n8n workflow or a Make blueprint. Export from n8n with "Download" or from Make with "Export Blueprint", then paste the whole file.');
|
|
26
|
+
this.name = 'UnknownFormatError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.UnknownFormatError = UnknownFormatError;
|
|
30
|
+
function parseWorkflow(input) {
|
|
31
|
+
const raw = typeof input === 'string' ? JSON.parse(input) : input;
|
|
32
|
+
if ((0, n8n_js_1.isN8nWorkflow)(raw))
|
|
33
|
+
return (0, n8n_js_1.parseN8n)(raw);
|
|
34
|
+
if ((0, make_js_1.isMakeBlueprint)(raw))
|
|
35
|
+
return (0, make_js_1.parseMake)(raw);
|
|
36
|
+
throw new UnknownFormatError();
|
|
37
|
+
}
|
|
38
|
+
/** Order matters: this is what the user reads top to bottom. */
|
|
39
|
+
const CHECKS = [
|
|
40
|
+
zero_write_js_1.checkZeroWrite,
|
|
41
|
+
others_js_1.checkCredentialExpiry,
|
|
42
|
+
others_js_1.checkCadence,
|
|
43
|
+
others_js_1.checkErrorHandling,
|
|
44
|
+
];
|
|
45
|
+
function analyze(input) {
|
|
46
|
+
const wf = parseWorkflow(input);
|
|
47
|
+
const findings = CHECKS.flatMap((c) => c(wf)).sort((a, b) => model_js_1.SEVERITY_ORDER[a.severity] - model_js_1.SEVERITY_ORDER[b.severity]);
|
|
48
|
+
return {
|
|
49
|
+
platform: wf.platform,
|
|
50
|
+
workflowName: wf.name,
|
|
51
|
+
nodeCount: wf.nodes.filter((n) => n.role !== 'note' && !n.disabled).length,
|
|
52
|
+
findings,
|
|
53
|
+
protections: (0, protections_js_1.checkProtections)(wf),
|
|
54
|
+
parseNotes: wf.parseNotes,
|
|
55
|
+
stats: {
|
|
56
|
+
writeNodes: wf.nodes.filter((n) => n.role === 'write' && !n.disabled).length,
|
|
57
|
+
oauthCredentials: wf.nodes
|
|
58
|
+
.flatMap((n) => n.credentials)
|
|
59
|
+
.filter((c) => c.authKind === 'oauth2').length,
|
|
60
|
+
triggers: wf.triggerIds.length,
|
|
61
|
+
staticKeyCredentials: wf.nodes
|
|
62
|
+
.flatMap((n) => n.credentials)
|
|
63
|
+
.filter((c) => c.authKind === 'api-key' || c.authKind === 'basic').length,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
var providers_js_1 = require("./core/providers.js");
|
|
68
|
+
Object.defineProperty(exports, "PROVIDERS", { enumerable: true, get: function () { return providers_js_1.PROVIDERS; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@still-running/health-check",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Static analysis of n8n workflow JSON and Make blueprint JSON — no execution, no network, no storage.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"require": "./dist/index.js",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/AliAlsamraay/stillrunning.git",
|
|
24
|
+
"directory": "packages/health-check"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"typecheck": "tsc -b",
|
|
31
|
+
"build": "tsc -p tsconfig.build.json && node -e \"require('fs').writeFileSync('dist/package.json', JSON.stringify({type:'commonjs'}))\"",
|
|
32
|
+
"prepublishOnly": "npm run build"
|
|
33
|
+
}
|
|
34
|
+
}
|