@ia-qa/self-healing 1.18.0 → 1.19.1
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 +98 -1
- package/TUTORIAL.md +58 -0
- package/dist/ai/models.d.ts +12 -0
- package/dist/ai/models.js +33 -1
- package/dist/ai/models.js.map +1 -1
- package/dist/ai/resolver.d.ts +29 -6
- package/dist/ai/resolver.js +42 -7
- package/dist/ai/resolver.js.map +1 -1
- package/dist/cli/args.js +2 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/diff.js +34 -10
- package/dist/cli/diff.js.map +1 -1
- package/dist/cli/fix.js +9 -2
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/index.js +47 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/map.d.ts +15 -4
- package/dist/cli/map.js +66 -13
- package/dist/cli/map.js.map +1 -1
- package/dist/cli/secret.d.ts +1 -0
- package/dist/cli/secret.js +172 -0
- package/dist/cli/secret.js.map +1 -0
- package/dist/cli/session.d.ts +24 -0
- package/dist/cli/session.js +119 -0
- package/dist/cli/session.js.map +1 -0
- package/dist/cli/skill.d.ts +37 -14
- package/dist/cli/skill.js +36 -30
- package/dist/cli/skill.js.map +1 -1
- package/dist/config.d.ts +23 -2
- package/dist/config.js +50 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/secretStore.d.ts +49 -0
- package/dist/secretStore.js +244 -0
- package/dist/secretStore.js.map +1 -0
- package/dist/sessionCheck.d.ts +96 -0
- package/dist/sessionCheck.js +182 -0
- package/dist/sessionCheck.js.map +1 -0
- package/dist/unrepaired.d.ts +73 -0
- package/dist/unrepaired.js +0 -0
- package/dist/unrepaired.js.map +1 -0
- package/dist/volatile.d.ts +8 -0
- package/dist/volatile.js +24 -7
- package/dist/volatile.js.map +1 -1
- package/dist/volatileBlindSpot.d.ts +67 -0
- package/dist/volatileBlindSpot.js +0 -0
- package/dist/volatileBlindSpot.js.map +1 -0
- package/package.json +1 -1
- package/skills/ia-qa-heal/SKILL.md +29 -3
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OS credential store, as a third `source` for a secret reference.
|
|
3
|
+
*
|
|
4
|
+
* **What this buys, precisely.** `source: "env"` protects three real things — the value never
|
|
5
|
+
* reaches a committed file, a transcript, or a tool argument. It does not protect the
|
|
6
|
+
* *process*: once a human exports the key, everything an agent launches inherits it, which is
|
|
7
|
+
* how `dotenv-cli -- ia-qa-discover-ai plan` works at all. Measured on a real shell: a spawned
|
|
8
|
+
* process read the whole 164-character key out of `process.env`.
|
|
9
|
+
*
|
|
10
|
+
* With `source: "keychain"` the value stops being **ambient**. Only this tool asks the OS for
|
|
11
|
+
* it, at the moment it needs it, and nothing else in the session inherits it.
|
|
12
|
+
*
|
|
13
|
+
* **What it does not buy, and must never be sold as.** It is not a barrier against an agent
|
|
14
|
+
* that decides to read the key: an agent can run the same OS command. What changes is that
|
|
15
|
+
* obtaining the value becomes an explicit, visible act in the transcript rather than an
|
|
16
|
+
* ambient inheritance — something a human reviewing the session can see, and something the
|
|
17
|
+
* skill files forbid by name. Anyone describing this as "the agent cannot read the key" is
|
|
18
|
+
* describing a different feature.
|
|
19
|
+
*
|
|
20
|
+
* **No home-made vault.** Where the OS has no store this refuses and names `env`, rather than
|
|
21
|
+
* inventing an encrypted file and calling it a keychain. A vault this package wrote would be
|
|
22
|
+
* a vault this package would have to defend.
|
|
23
|
+
*/
|
|
24
|
+
/** Everything lands under one service name, so `list` and `rm` need no bookkeeping per key. */
|
|
25
|
+
export declare const SERVICE = "ia-qa";
|
|
26
|
+
export type StoreKind = 'macos' | 'windows' | 'linux' | 'unsupported';
|
|
27
|
+
export declare function storeKind(platform?: NodeJS.Platform): StoreKind;
|
|
28
|
+
/** Where the names live. Names only — a value is never written here. */
|
|
29
|
+
export declare function indexPath(home?: string): string;
|
|
30
|
+
export type StoreResult = {
|
|
31
|
+
ok: true;
|
|
32
|
+
value?: string;
|
|
33
|
+
} | {
|
|
34
|
+
ok: false;
|
|
35
|
+
reason: string;
|
|
36
|
+
};
|
|
37
|
+
export declare function writeSecret(name: string, value: string, home?: string): StoreResult;
|
|
38
|
+
export declare function readSecret(name: string, home?: string): StoreResult;
|
|
39
|
+
export declare function removeSecret(name: string, home?: string): StoreResult;
|
|
40
|
+
/**
|
|
41
|
+
* The names this tool has stored, and only the names.
|
|
42
|
+
*
|
|
43
|
+
* Kept in a file of our own rather than asked of the OS: the three stores list their contents
|
|
44
|
+
* three different ways, and one of them (`security dump-keychain`) prompts the user. A names
|
|
45
|
+
* index is the same everywhere and holds nothing worth protecting.
|
|
46
|
+
*/
|
|
47
|
+
export declare function listSecrets(home?: string): string[];
|
|
48
|
+
/** One line for a human deciding whether this machine can do it at all. */
|
|
49
|
+
export declare function storeDescription(kind?: StoreKind): string;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SERVICE = void 0;
|
|
37
|
+
exports.storeKind = storeKind;
|
|
38
|
+
exports.indexPath = indexPath;
|
|
39
|
+
exports.writeSecret = writeSecret;
|
|
40
|
+
exports.readSecret = readSecret;
|
|
41
|
+
exports.removeSecret = removeSecret;
|
|
42
|
+
exports.listSecrets = listSecrets;
|
|
43
|
+
exports.storeDescription = storeDescription;
|
|
44
|
+
const child_process_1 = require("child_process");
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const os = __importStar(require("os"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
/**
|
|
49
|
+
* The OS credential store, as a third `source` for a secret reference.
|
|
50
|
+
*
|
|
51
|
+
* **What this buys, precisely.** `source: "env"` protects three real things — the value never
|
|
52
|
+
* reaches a committed file, a transcript, or a tool argument. It does not protect the
|
|
53
|
+
* *process*: once a human exports the key, everything an agent launches inherits it, which is
|
|
54
|
+
* how `dotenv-cli -- ia-qa-discover-ai plan` works at all. Measured on a real shell: a spawned
|
|
55
|
+
* process read the whole 164-character key out of `process.env`.
|
|
56
|
+
*
|
|
57
|
+
* With `source: "keychain"` the value stops being **ambient**. Only this tool asks the OS for
|
|
58
|
+
* it, at the moment it needs it, and nothing else in the session inherits it.
|
|
59
|
+
*
|
|
60
|
+
* **What it does not buy, and must never be sold as.** It is not a barrier against an agent
|
|
61
|
+
* that decides to read the key: an agent can run the same OS command. What changes is that
|
|
62
|
+
* obtaining the value becomes an explicit, visible act in the transcript rather than an
|
|
63
|
+
* ambient inheritance — something a human reviewing the session can see, and something the
|
|
64
|
+
* skill files forbid by name. Anyone describing this as "the agent cannot read the key" is
|
|
65
|
+
* describing a different feature.
|
|
66
|
+
*
|
|
67
|
+
* **No home-made vault.** Where the OS has no store this refuses and names `env`, rather than
|
|
68
|
+
* inventing an encrypted file and calling it a keychain. A vault this package wrote would be
|
|
69
|
+
* a vault this package would have to defend.
|
|
70
|
+
*/
|
|
71
|
+
/** Everything lands under one service name, so `list` and `rm` need no bookkeeping per key. */
|
|
72
|
+
exports.SERVICE = 'ia-qa';
|
|
73
|
+
function storeKind(platform = process.platform) {
|
|
74
|
+
if (platform === 'darwin')
|
|
75
|
+
return 'macos';
|
|
76
|
+
if (platform === 'win32')
|
|
77
|
+
return 'windows';
|
|
78
|
+
if (platform === 'linux')
|
|
79
|
+
return 'linux';
|
|
80
|
+
return 'unsupported';
|
|
81
|
+
}
|
|
82
|
+
/** Where the names live. Names only — a value is never written here. */
|
|
83
|
+
function indexPath(home = os.homedir()) {
|
|
84
|
+
return path.join(home, '.ia-qa', 'secrets.json');
|
|
85
|
+
}
|
|
86
|
+
/** Windows keeps one DPAPI blob per name: encrypted for this user account, on this machine. */
|
|
87
|
+
function blobPath(name, home = os.homedir()) {
|
|
88
|
+
return path.join(home, '.ia-qa', 'secrets', `${encodeURIComponent(name)}.dpapi`);
|
|
89
|
+
}
|
|
90
|
+
const UNSUPPORTED = (kind) => kind === 'linux'
|
|
91
|
+
? 'No OS credential store here: `secret-tool` (libsecret) is not installed. Install it, or use ' +
|
|
92
|
+
'{ "source": "env", "key": "…" } and export the value in your shell.'
|
|
93
|
+
: `No OS credential store on this platform (${process.platform}). Use ` +
|
|
94
|
+
'{ "source": "env", "key": "…" } and export the value in your shell.';
|
|
95
|
+
function sh(cmd, args, input, env) {
|
|
96
|
+
const r = (0, child_process_1.spawnSync)(cmd, args, {
|
|
97
|
+
encoding: 'utf8',
|
|
98
|
+
input: input ?? '',
|
|
99
|
+
windowsHide: true,
|
|
100
|
+
...(env ? { env: { ...process.env, ...env } } : {}),
|
|
101
|
+
});
|
|
102
|
+
return { code: r.status ?? 1, out: `${r.stdout ?? ''}${r.stderr ?? ''}` };
|
|
103
|
+
}
|
|
104
|
+
const hasBinary = (cmd) => sh(process.platform === 'win32' ? 'where' : 'which', [cmd]).code === 0;
|
|
105
|
+
/**
|
|
106
|
+
* PowerShell reads the value from **stdin**, never from a command line.
|
|
107
|
+
*
|
|
108
|
+
* An argument is visible in the process list and in the shell history for as long as the
|
|
109
|
+
* process lives. This is the one place in the package where a plaintext secret exists in
|
|
110
|
+
* memory at all, so it does not also get a second home.
|
|
111
|
+
*/
|
|
112
|
+
// The path travels in an environment variable, not in `$args`: `powershell -Command "<script>"
|
|
113
|
+
// followed by more words does NOT bind them to `$args`, which produced a null `Split-Path`
|
|
114
|
+
// argument on the first real run. An env var also sidesteps a whole class of quoting bugs on
|
|
115
|
+
// a path with a space in it — and a path is not a secret.
|
|
116
|
+
const PS_WRITE = `$ErrorActionPreference='Stop';$v=[Console]::In.ReadToEnd().TrimEnd("\`r","\`n");` +
|
|
117
|
+
`$p=$env:IAQA_BLOB;$dir=Split-Path -Parent $p;if(-not(Test-Path $dir)){New-Item -ItemType Directory -Path $dir -Force|Out-Null};` +
|
|
118
|
+
`ConvertTo-SecureString -String $v -AsPlainText -Force|ConvertFrom-SecureString|Set-Content -Path $p -Encoding ascii`;
|
|
119
|
+
// `.Trim()` is load-bearing: `Set-Content` writes a trailing newline, `-Raw` keeps it, and
|
|
120
|
+
// `ConvertTo-SecureString` rejects the result with "the input string format is incorrect".
|
|
121
|
+
// Found on the first real write/read round-trip, not by reading the code.
|
|
122
|
+
const PS_READ = `$ErrorActionPreference='Stop';$s=(Get-Content -Path $env:IAQA_BLOB -Raw).Trim()|ConvertTo-SecureString;` +
|
|
123
|
+
`$b=[Runtime.InteropServices.Marshal]::SecureStringToBSTR($s);` +
|
|
124
|
+
`[Console]::Out.Write([Runtime.InteropServices.Marshal]::PtrToStringAuto($b));` +
|
|
125
|
+
`[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($b)`;
|
|
126
|
+
function writeSecret(name, value, home = os.homedir()) {
|
|
127
|
+
const kind = storeKind();
|
|
128
|
+
if (kind === 'macos') {
|
|
129
|
+
// The value goes on the command line here — `security` takes it no other way. It is
|
|
130
|
+
// visible to `ps` for this user for the instant the process lives. Said out loud rather
|
|
131
|
+
// than left for someone to discover.
|
|
132
|
+
const r = sh('security', ['add-generic-password', '-U', '-s', exports.SERVICE, '-a', name, '-w', value]);
|
|
133
|
+
if (r.code !== 0)
|
|
134
|
+
return { ok: false, reason: `macOS keychain refused it: ${r.out.trim() || `exit ${r.code}`}` };
|
|
135
|
+
}
|
|
136
|
+
else if (kind === 'windows') {
|
|
137
|
+
const r = sh('powershell', ['-NoProfile', '-NonInteractive', '-Command', PS_WRITE], value, { IAQA_BLOB: blobPath(name, home) });
|
|
138
|
+
if (r.code !== 0)
|
|
139
|
+
return { ok: false, reason: `Windows DPAPI refused it: ${r.out.trim() || `exit ${r.code}`}` };
|
|
140
|
+
}
|
|
141
|
+
else if (kind === 'linux') {
|
|
142
|
+
if (!hasBinary('secret-tool'))
|
|
143
|
+
return { ok: false, reason: UNSUPPORTED('linux') };
|
|
144
|
+
const r = sh('secret-tool', ['store', '--label', `${exports.SERVICE} ${name}`, 'service', exports.SERVICE, 'account', name], value);
|
|
145
|
+
if (r.code !== 0)
|
|
146
|
+
return { ok: false, reason: `secret-tool refused it: ${r.out.trim() || `exit ${r.code}`}` };
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
return { ok: false, reason: UNSUPPORTED(kind) };
|
|
150
|
+
}
|
|
151
|
+
rememberName(name, home);
|
|
152
|
+
return { ok: true };
|
|
153
|
+
}
|
|
154
|
+
function readSecret(name, home = os.homedir()) {
|
|
155
|
+
const kind = storeKind();
|
|
156
|
+
if (kind === 'macos') {
|
|
157
|
+
const r = sh('security', ['find-generic-password', '-s', exports.SERVICE, '-a', name, '-w']);
|
|
158
|
+
if (r.code !== 0)
|
|
159
|
+
return { ok: false, reason: `not in the macOS keychain under ${exports.SERVICE}/${name}` };
|
|
160
|
+
return { ok: true, value: r.out.replace(/\r?\n$/, '') };
|
|
161
|
+
}
|
|
162
|
+
if (kind === 'windows') {
|
|
163
|
+
const file = blobPath(name, home);
|
|
164
|
+
if (!fs.existsSync(file))
|
|
165
|
+
return { ok: false, reason: `not stored here (${file} does not exist)` };
|
|
166
|
+
const r = sh('powershell', ['-NoProfile', '-NonInteractive', '-Command', PS_READ], '', { IAQA_BLOB: file });
|
|
167
|
+
if (r.code !== 0) {
|
|
168
|
+
// The likely cause is named, and what actually happened is shown. An error message
|
|
169
|
+
// that asserts a cause it did not verify is the thing this package spends its design
|
|
170
|
+
// refusing — the first draft of this branch blamed cross-account DPAPI for what was a
|
|
171
|
+
// trailing newline.
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
reason: `stored, but it could not be decrypted here. A DPAPI blob is bound to the Windows user ` +
|
|
175
|
+
`AND the machine that wrote it, so copying .ia-qa/secrets between machines or accounts ` +
|
|
176
|
+
`does not work — set it again on this one. What the store said: ${r.out.trim().split('\n')[0] || `exit ${r.code}`}`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return { ok: true, value: r.out };
|
|
180
|
+
}
|
|
181
|
+
if (kind === 'linux') {
|
|
182
|
+
if (!hasBinary('secret-tool'))
|
|
183
|
+
return { ok: false, reason: UNSUPPORTED('linux') };
|
|
184
|
+
const r = sh('secret-tool', ['lookup', 'service', exports.SERVICE, 'account', name]);
|
|
185
|
+
if (r.code !== 0 || !r.out)
|
|
186
|
+
return { ok: false, reason: `not in the keyring under ${exports.SERVICE}/${name}` };
|
|
187
|
+
return { ok: true, value: r.out.replace(/\r?\n$/, '') };
|
|
188
|
+
}
|
|
189
|
+
return { ok: false, reason: UNSUPPORTED(kind) };
|
|
190
|
+
}
|
|
191
|
+
function removeSecret(name, home = os.homedir()) {
|
|
192
|
+
const kind = storeKind();
|
|
193
|
+
if (kind === 'macos')
|
|
194
|
+
sh('security', ['delete-generic-password', '-s', exports.SERVICE, '-a', name]);
|
|
195
|
+
else if (kind === 'windows')
|
|
196
|
+
fs.rmSync(blobPath(name, home), { force: true });
|
|
197
|
+
else if (kind === 'linux')
|
|
198
|
+
sh('secret-tool', ['clear', 'service', exports.SERVICE, 'account', name]);
|
|
199
|
+
else
|
|
200
|
+
return { ok: false, reason: UNSUPPORTED(kind) };
|
|
201
|
+
forgetName(name, home);
|
|
202
|
+
return { ok: true };
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The names this tool has stored, and only the names.
|
|
206
|
+
*
|
|
207
|
+
* Kept in a file of our own rather than asked of the OS: the three stores list their contents
|
|
208
|
+
* three different ways, and one of them (`security dump-keychain`) prompts the user. A names
|
|
209
|
+
* index is the same everywhere and holds nothing worth protecting.
|
|
210
|
+
*/
|
|
211
|
+
function listSecrets(home = os.homedir()) {
|
|
212
|
+
try {
|
|
213
|
+
const parsed = JSON.parse(fs.readFileSync(indexPath(home), 'utf8'));
|
|
214
|
+
return Array.isArray(parsed) ? parsed.filter((n) => typeof n === 'string').sort() : [];
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function saveNames(names, home) {
|
|
221
|
+
const file = indexPath(home);
|
|
222
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
223
|
+
fs.writeFileSync(file, `${JSON.stringify([...new Set(names)].sort(), null, 2)}\n`, 'utf8');
|
|
224
|
+
}
|
|
225
|
+
function rememberName(name, home) {
|
|
226
|
+
saveNames([...listSecrets(home), name], home);
|
|
227
|
+
}
|
|
228
|
+
function forgetName(name, home) {
|
|
229
|
+
saveNames(listSecrets(home).filter((n) => n !== name), home);
|
|
230
|
+
}
|
|
231
|
+
/** One line for a human deciding whether this machine can do it at all. */
|
|
232
|
+
function storeDescription(kind = storeKind()) {
|
|
233
|
+
switch (kind) {
|
|
234
|
+
case 'macos':
|
|
235
|
+
return 'the macOS keychain (`security`)';
|
|
236
|
+
case 'windows':
|
|
237
|
+
return 'Windows DPAPI — encrypted for this user account, on this machine';
|
|
238
|
+
case 'linux':
|
|
239
|
+
return hasBinary('secret-tool') ? 'the desktop keyring (`secret-tool`)' : 'nothing: `secret-tool` (libsecret) is not installed';
|
|
240
|
+
default:
|
|
241
|
+
return `nothing: no credential store on ${process.platform}`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=secretStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secretStore.js","sourceRoot":"","sources":["../src/secretStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a session is not opening the app — told apart, not listed.
|
|
3
|
+
*
|
|
4
|
+
* `assertLanded` names three causes: "expired, rotated, or never covering this domain". All
|
|
5
|
+
* three are true statements about the world and the message offers no way to choose between
|
|
6
|
+
* them, so the reader is left to guess — and one of them means "run `login` again" while
|
|
7
|
+
* another means "your `baseUrl` and your session are about different hosts", which no amount
|
|
8
|
+
* of logging in will fix. A field report ended with someone writing their own
|
|
9
|
+
* `check-session.mjs` to answer it.
|
|
10
|
+
*
|
|
11
|
+
* **Two of the three are answerable from the file alone**, which is the finding that makes
|
|
12
|
+
* this verb worth having rather than a second wrapper around a browser:
|
|
13
|
+
*
|
|
14
|
+
* - **expired** — a cookie carries its own `expires`. A file whose session cookies are all
|
|
15
|
+
* in the past cannot authenticate anything, and no request has to be made to know it.
|
|
16
|
+
* - **wrong-host** — a cookie carries its `domain`. If nothing in the file is scoped to the
|
|
17
|
+
* host `baseUrl` points at, the session was taken somewhere else. This is the one a
|
|
18
|
+
* person can stare at for an hour: the session works, the app works, and they are not the
|
|
19
|
+
* same app. `www.` and an apex fold together, as everywhere else in this package.
|
|
20
|
+
* - **rotated** — cookies present, unexpired, correctly scoped, and the app still answers
|
|
21
|
+
* with a login. Only the server knows why (a deploy, a secret rotation, a forced logout),
|
|
22
|
+
* and only a request can observe it.
|
|
23
|
+
*
|
|
24
|
+
* So the offline analysis runs first and the browser is used only for the question it is the
|
|
25
|
+
* only witness to. Everything here is pure; the I/O lives in `cli/session.ts`.
|
|
26
|
+
*/
|
|
27
|
+
/** Playwright's storageState, as much of it as this file needs. */
|
|
28
|
+
export interface StoredSession {
|
|
29
|
+
cookies?: Array<{
|
|
30
|
+
name: string;
|
|
31
|
+
domain?: string;
|
|
32
|
+
path?: string;
|
|
33
|
+
expires?: number;
|
|
34
|
+
}>;
|
|
35
|
+
origins?: Array<{
|
|
36
|
+
origin: string;
|
|
37
|
+
localStorage?: Array<{
|
|
38
|
+
name: string;
|
|
39
|
+
value: string;
|
|
40
|
+
}>;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
export type SessionCause =
|
|
44
|
+
/** Every cookie's `expires` is in the past. */
|
|
45
|
+
'expired'
|
|
46
|
+
/** Nothing in the file is scoped to the host `baseUrl` names. */
|
|
47
|
+
| 'wrong-host'
|
|
48
|
+
/** The file looks usable and the app still shows a login — the server dropped it. */
|
|
49
|
+
| 'rotated'
|
|
50
|
+
/** A storageState with nothing in it: this app keeps its token where the file cannot reach. */
|
|
51
|
+
| 'empty'
|
|
52
|
+
/** It opens the app. */
|
|
53
|
+
| 'holds'
|
|
54
|
+
/** The check could not run — a navigation error is not a verdict about the session. */
|
|
55
|
+
| 'unknown';
|
|
56
|
+
export interface SessionFacts {
|
|
57
|
+
cookies: number;
|
|
58
|
+
origins: number;
|
|
59
|
+
/** Cookies whose `expires` is in the past, and the soonest one still valid. */
|
|
60
|
+
expired: number;
|
|
61
|
+
/** Seconds until the first cookie expires, when any says. Negative: all gone. */
|
|
62
|
+
expiresInSeconds: number | null;
|
|
63
|
+
/** Cookie domains, as written in the file. */
|
|
64
|
+
domains: string[];
|
|
65
|
+
/** True when at least one cookie or origin is scoped to the app's host. */
|
|
66
|
+
coversHost: boolean;
|
|
67
|
+
host: string;
|
|
68
|
+
}
|
|
69
|
+
/** `www.` and an apex are one site, the same fold `discover --sitemap` applies. */
|
|
70
|
+
export declare function foldHost(host: string): string;
|
|
71
|
+
/** A cookie domain (`.example.com`) covers a host when the host ends with it. */
|
|
72
|
+
export declare function domainCovers(domain: string, host: string): boolean;
|
|
73
|
+
export declare function readFacts(state: StoredSession, baseUrl: string, now?: number): SessionFacts;
|
|
74
|
+
export interface SessionVerdict {
|
|
75
|
+
cause: SessionCause;
|
|
76
|
+
/** True only when the app was actually opened and did not show a login. */
|
|
77
|
+
holds: boolean;
|
|
78
|
+
headline: string;
|
|
79
|
+
/** What to do, or nothing when it holds — a remedy printed on a good run is noise. */
|
|
80
|
+
remedy: string[];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The verdict, from the file's own facts plus — when one was possible — what the app
|
|
84
|
+
* answered.
|
|
85
|
+
*
|
|
86
|
+
* `landed` is undefined when no request was made, which is a legitimate state: the offline
|
|
87
|
+
* causes are conclusive on their own, and opening a browser to re-confirm an expiry date is
|
|
88
|
+
* a request nobody needs to make.
|
|
89
|
+
*/
|
|
90
|
+
export declare function judgeSession(facts: SessionFacts, replay?: {
|
|
91
|
+
landed: string;
|
|
92
|
+
wall: boolean;
|
|
93
|
+
error?: string;
|
|
94
|
+
}, loginCommand?: string): SessionVerdict;
|
|
95
|
+
/** "3 days", "4 hours", "12 minutes" — enough precision to act, never a timestamp to parse. */
|
|
96
|
+
export declare function describeAge(seconds: number): string;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Why a session is not opening the app — told apart, not listed.
|
|
4
|
+
*
|
|
5
|
+
* `assertLanded` names three causes: "expired, rotated, or never covering this domain". All
|
|
6
|
+
* three are true statements about the world and the message offers no way to choose between
|
|
7
|
+
* them, so the reader is left to guess — and one of them means "run `login` again" while
|
|
8
|
+
* another means "your `baseUrl` and your session are about different hosts", which no amount
|
|
9
|
+
* of logging in will fix. A field report ended with someone writing their own
|
|
10
|
+
* `check-session.mjs` to answer it.
|
|
11
|
+
*
|
|
12
|
+
* **Two of the three are answerable from the file alone**, which is the finding that makes
|
|
13
|
+
* this verb worth having rather than a second wrapper around a browser:
|
|
14
|
+
*
|
|
15
|
+
* - **expired** — a cookie carries its own `expires`. A file whose session cookies are all
|
|
16
|
+
* in the past cannot authenticate anything, and no request has to be made to know it.
|
|
17
|
+
* - **wrong-host** — a cookie carries its `domain`. If nothing in the file is scoped to the
|
|
18
|
+
* host `baseUrl` points at, the session was taken somewhere else. This is the one a
|
|
19
|
+
* person can stare at for an hour: the session works, the app works, and they are not the
|
|
20
|
+
* same app. `www.` and an apex fold together, as everywhere else in this package.
|
|
21
|
+
* - **rotated** — cookies present, unexpired, correctly scoped, and the app still answers
|
|
22
|
+
* with a login. Only the server knows why (a deploy, a secret rotation, a forced logout),
|
|
23
|
+
* and only a request can observe it.
|
|
24
|
+
*
|
|
25
|
+
* So the offline analysis runs first and the browser is used only for the question it is the
|
|
26
|
+
* only witness to. Everything here is pure; the I/O lives in `cli/session.ts`.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.foldHost = foldHost;
|
|
30
|
+
exports.domainCovers = domainCovers;
|
|
31
|
+
exports.readFacts = readFacts;
|
|
32
|
+
exports.judgeSession = judgeSession;
|
|
33
|
+
exports.describeAge = describeAge;
|
|
34
|
+
/** `www.` and an apex are one site, the same fold `discover --sitemap` applies. */
|
|
35
|
+
function foldHost(host) {
|
|
36
|
+
return host.replace(/^www\./i, '').toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
/** A cookie domain (`.example.com`) covers a host when the host ends with it. */
|
|
39
|
+
function domainCovers(domain, host) {
|
|
40
|
+
const d = foldHost(domain.replace(/^\./, ''));
|
|
41
|
+
const h = foldHost(host);
|
|
42
|
+
return h === d || h.endsWith(`.${d}`);
|
|
43
|
+
}
|
|
44
|
+
function readFacts(state, baseUrl, now = Date.now()) {
|
|
45
|
+
let host = '';
|
|
46
|
+
try {
|
|
47
|
+
// `hostname`, never `host`: a cookie is not scoped by port (RFC 6265 ignores it
|
|
48
|
+
// entirely), so comparing `127.0.0.1` against `127.0.0.1:49331` reports a session on
|
|
49
|
+
// the right machine as belonging to another application. Caught by the live test on the
|
|
50
|
+
// first run, and it would have hit every `localhost:3000` in existence — a confidently
|
|
51
|
+
// wrong diagnosis, in the one verb written to stop those.
|
|
52
|
+
host = new URL(baseUrl).hostname;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* an unusable baseUrl is the caller's problem to report */
|
|
56
|
+
}
|
|
57
|
+
const cookies = state.cookies ?? [];
|
|
58
|
+
const origins = state.origins ?? [];
|
|
59
|
+
// `expires` is seconds since epoch, and -1 means a session cookie: no stated expiry, which
|
|
60
|
+
// is not the same as expired. Counting those as gone would report a live session as dead.
|
|
61
|
+
const dated = cookies.filter((c) => typeof c.expires === 'number' && c.expires > 0);
|
|
62
|
+
const nowSeconds = now / 1000;
|
|
63
|
+
const expired = dated.filter((c) => c.expires < nowSeconds).length;
|
|
64
|
+
const future = dated.filter((c) => c.expires >= nowSeconds).map((c) => c.expires);
|
|
65
|
+
const expiresInSeconds = future.length > 0
|
|
66
|
+
? Math.round(Math.min(...future) - nowSeconds)
|
|
67
|
+
: dated.length > 0
|
|
68
|
+
? Math.round(Math.max(...dated.map((c) => c.expires)) - nowSeconds)
|
|
69
|
+
: null;
|
|
70
|
+
const domains = [...new Set(cookies.map((c) => c.domain ?? '').filter(Boolean))];
|
|
71
|
+
const coversHost = host === '' ||
|
|
72
|
+
domains.some((d) => domainCovers(d, host)) ||
|
|
73
|
+
origins.some((o) => {
|
|
74
|
+
try {
|
|
75
|
+
return foldHost(new URL(o.origin).hostname) === foldHost(host);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return { cookies: cookies.length, origins: origins.length, expired, expiresInSeconds, domains, coversHost, host };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The verdict, from the file's own facts plus — when one was possible — what the app
|
|
85
|
+
* answered.
|
|
86
|
+
*
|
|
87
|
+
* `landed` is undefined when no request was made, which is a legitimate state: the offline
|
|
88
|
+
* causes are conclusive on their own, and opening a browser to re-confirm an expiry date is
|
|
89
|
+
* a request nobody needs to make.
|
|
90
|
+
*/
|
|
91
|
+
function judgeSession(facts, replay, loginCommand = 'ia-qa-heal login') {
|
|
92
|
+
if (facts.cookies === 0 && facts.origins === 0) {
|
|
93
|
+
return {
|
|
94
|
+
cause: 'empty',
|
|
95
|
+
holds: false,
|
|
96
|
+
headline: 'This session file holds nothing — no cookies, no local storage.',
|
|
97
|
+
remedy: [
|
|
98
|
+
'A storageState carries cookies and local storage, and nothing else. An app that keeps',
|
|
99
|
+
'its token in IndexedDB, in memory or in a service worker leaves nothing for it to hold,',
|
|
100
|
+
'however logged in the browser was.',
|
|
101
|
+
`For those, capture during your own suite's run instead: IAQA_CAPTURE=1 with "testCommand".`,
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (facts.cookies > 0 && facts.expired === facts.cookies) {
|
|
106
|
+
const ago = facts.expiresInSeconds === null ? '' : ` (the last one ${describeAge(-facts.expiresInSeconds)} ago)`;
|
|
107
|
+
return {
|
|
108
|
+
cause: 'expired',
|
|
109
|
+
holds: false,
|
|
110
|
+
headline: `Every cookie in this session has expired${ago}.`,
|
|
111
|
+
remedy: [`A person logs in again: ${loginCommand}`, 'Nothing about your app or your config is wrong.'],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (!facts.coversHost) {
|
|
115
|
+
return {
|
|
116
|
+
cause: 'wrong-host',
|
|
117
|
+
holds: false,
|
|
118
|
+
headline: `Nothing in this session is scoped to ${facts.host} — its cookies are for ${facts.domains.length > 0 ? facts.domains.join(', ') : '(no domain stated)'}.`,
|
|
119
|
+
remedy: [
|
|
120
|
+
'The session and the baseUrl are about different applications, so logging in again will',
|
|
121
|
+
'produce the same file. Either point baseUrl at the host this session was taken on, or',
|
|
122
|
+
`take one on this host: ${loginCommand} --url ${facts.host ? `https://${facts.host}` : '<your app>'}`,
|
|
123
|
+
'(www. and the apex are treated as one host, so that difference is not the cause here.)',
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (replay?.error) {
|
|
128
|
+
return {
|
|
129
|
+
cause: 'unknown',
|
|
130
|
+
holds: false,
|
|
131
|
+
headline: `The session looks usable, but the app could not be opened to confirm it: ${replay.error}`,
|
|
132
|
+
remedy: [
|
|
133
|
+
'This is not a verdict about the session — nothing was proven either way.',
|
|
134
|
+
'Check that the app is reachable from this machine, then run this again.',
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (replay && !replay.wall) {
|
|
139
|
+
return {
|
|
140
|
+
cause: 'holds',
|
|
141
|
+
holds: true,
|
|
142
|
+
headline: `This session opens the app — landed on ${replay.landed}, no login form.`,
|
|
143
|
+
remedy: [],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (replay?.wall) {
|
|
147
|
+
return {
|
|
148
|
+
cause: 'rotated',
|
|
149
|
+
holds: false,
|
|
150
|
+
headline: `The cookies are valid and scoped to ${facts.host}, but the app still answered with a login (landed on ${replay.landed}).`,
|
|
151
|
+
remedy: [
|
|
152
|
+
'The server dropped this session: a deploy, a secret rotation, a forced logout, or a',
|
|
153
|
+
'second factor it now asks for. Nothing in the file can say which.',
|
|
154
|
+
`A person logs in again: ${loginCommand}`,
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
// No replay was attempted and no offline cause fired: the file is plausible, and plausible
|
|
159
|
+
// is not the same as working. Saying "holds" here is the exact false green this package
|
|
160
|
+
// exists to refuse.
|
|
161
|
+
return {
|
|
162
|
+
cause: 'unknown',
|
|
163
|
+
holds: false,
|
|
164
|
+
headline: 'This session file looks usable, but nothing opened the app to prove it.',
|
|
165
|
+
remedy: ['Run the check without --offline to find out.'],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** "3 days", "4 hours", "12 minutes" — enough precision to act, never a timestamp to parse. */
|
|
169
|
+
function describeAge(seconds) {
|
|
170
|
+
const s = Math.abs(Math.round(seconds));
|
|
171
|
+
if (s < 90)
|
|
172
|
+
return `${s} second${s === 1 ? '' : 's'}`;
|
|
173
|
+
const m = Math.round(s / 60);
|
|
174
|
+
if (m < 90)
|
|
175
|
+
return `${m} minute${m === 1 ? '' : 's'}`;
|
|
176
|
+
const h = Math.round(m / 60);
|
|
177
|
+
if (h < 48)
|
|
178
|
+
return `${h} hour${h === 1 ? '' : 's'}`;
|
|
179
|
+
const d = Math.round(h / 24);
|
|
180
|
+
return `${d} day${d === 1 ? '' : 's'}`;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=sessionCheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionCheck.js","sourceRoot":"","sources":["../src/sessionCheck.ts"],"names":[],"mappings":"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"}
|