latchkey 3.7.0 → 3.8.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/src/playwrightUtils.d.ts +57 -0
- package/dist/src/playwrightUtils.d.ts.map +1 -1
- package/dist/src/playwrightUtils.js +247 -8
- package/dist/src/playwrightUtils.js.map +1 -1
- package/dist/src/services/core/base.d.ts +55 -16
- package/dist/src/services/core/base.d.ts.map +1 -1
- package/dist/src/services/core/base.js +119 -31
- package/dist/src/services/core/base.js.map +1 -1
- package/dist/src/services/github.d.ts +2 -1
- package/dist/src/services/github.d.ts.map +1 -1
- package/dist/src/services/github.js +13 -0
- package/dist/src/services/github.js.map +1 -1
- package/dist/src/services/huggingface.d.ts +2 -1
- package/dist/src/services/huggingface.d.ts.map +1 -1
- package/dist/src/services/huggingface.js +13 -0
- package/dist/src/services/huggingface.js.map +1 -1
- package/dist/src/services/linear.d.ts +2 -1
- package/dist/src/services/linear.d.ts.map +1 -1
- package/dist/src/services/linear.js +14 -0
- package/dist/src/services/linear.js.map +1 -1
- package/dist/src/services/ngrok.d.ts +2 -1
- package/dist/src/services/ngrok.d.ts.map +1 -1
- package/dist/src/services/ngrok.js +12 -0
- package/dist/src/services/ngrok.js.map +1 -1
- package/dist/src/services/openrouter.d.ts +2 -1
- package/dist/src/services/openrouter.d.ts.map +1 -1
- package/dist/src/services/openrouter.js +12 -0
- package/dist/src/services/openrouter.js.map +1 -1
- package/dist/src/services/tailscale.d.ts +2 -1
- package/dist/src/services/tailscale.d.ts.map +1 -1
- package/dist/src/services/tailscale.js +18 -0
- package/dist/src/services/tailscale.js.map +1 -1
- package/dist/src/services/todoist.d.ts +2 -1
- package/dist/src/services/todoist.d.ts.map +1 -1
- package/dist/src/services/todoist.js +6 -0
- package/dist/src/services/todoist.js.map +1 -1
- package/dist/src/services/zoom.d.ts +2 -1
- package/dist/src/services/zoom.d.ts.map +1 -1
- package/dist/src/services/zoom.js +55 -55
- package/dist/src/services/zoom.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/tests/lint.test.js +1 -1
- package/dist/tests/manualCompletion.test.d.ts +10 -0
- package/dist/tests/manualCompletion.test.d.ts.map +1 -0
- package/dist/tests/manualCompletion.test.js +221 -0
- package/dist/tests/manualCompletion.test.js.map +1 -0
- package/dist/tests/manualCredentialForms.test.d.ts +8 -0
- package/dist/tests/manualCredentialForms.test.d.ts.map +1 -0
- package/dist/tests/manualCredentialForms.test.js +64 -0
- package/dist/tests/manualCredentialForms.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the credential request shown when a browser automation fails: the
|
|
3
|
+
* user pastes what they created by hand into a form, and the page hands it back
|
|
4
|
+
* to latchkey.
|
|
5
|
+
*
|
|
6
|
+
* Browser-free: the context and page are stand-ins exposing only what the
|
|
7
|
+
* request uses, which also lets the exposed binding be called directly.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect } from 'vitest';
|
|
10
|
+
import vm from 'node:vm';
|
|
11
|
+
import { CredentialFormFieldMissingError, CredentialFormValues, parseSubmittedCredentialValues, requestCredentialsFromUser, } from '../src/playwrightUtils.js';
|
|
12
|
+
const FORM_FIELDS = [
|
|
13
|
+
{ name: 'clientId', label: 'Client ID' },
|
|
14
|
+
{ name: 'clientSecret', label: 'Client secret', hint: 'From the app page' },
|
|
15
|
+
];
|
|
16
|
+
function createFakeBrowser() {
|
|
17
|
+
let renderedScript = '';
|
|
18
|
+
let submitBinding;
|
|
19
|
+
const closeListeners = [];
|
|
20
|
+
let markAsShown = () => {
|
|
21
|
+
/* replaced below */
|
|
22
|
+
};
|
|
23
|
+
const shown = new Promise((resolve) => {
|
|
24
|
+
markAsShown = resolve;
|
|
25
|
+
});
|
|
26
|
+
const page = {
|
|
27
|
+
isClosed: () => false,
|
|
28
|
+
bringToFront: () => Promise.resolve(),
|
|
29
|
+
exposeFunction: (_name, binding) => {
|
|
30
|
+
submitBinding = binding;
|
|
31
|
+
return Promise.resolve();
|
|
32
|
+
},
|
|
33
|
+
evaluate: (script) => {
|
|
34
|
+
renderedScript = script;
|
|
35
|
+
markAsShown();
|
|
36
|
+
return Promise.resolve(undefined);
|
|
37
|
+
},
|
|
38
|
+
on: (event, listener) => {
|
|
39
|
+
if (event === 'close') {
|
|
40
|
+
closeListeners.push(listener);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const context = {
|
|
45
|
+
newPage: () => Promise.resolve(page),
|
|
46
|
+
on: () => undefined,
|
|
47
|
+
browser: () => ({ on: () => undefined }),
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
shown,
|
|
51
|
+
getRenderedScript: () => renderedScript,
|
|
52
|
+
submit: (values) => {
|
|
53
|
+
if (submitBinding === undefined) {
|
|
54
|
+
throw new Error('The credential request has not been shown yet.');
|
|
55
|
+
}
|
|
56
|
+
return submitBinding(values);
|
|
57
|
+
},
|
|
58
|
+
closeBrowser: () => {
|
|
59
|
+
for (const listener of closeListeners) {
|
|
60
|
+
listener();
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
context,
|
|
64
|
+
page,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const NEVER_MS = 60_000;
|
|
68
|
+
describe('requestCredentialsFromUser', () => {
|
|
69
|
+
it('returns what the user submitted once it is accepted', async () => {
|
|
70
|
+
const browser = createFakeBrowser();
|
|
71
|
+
const request = requestCredentialsFromUser({
|
|
72
|
+
context: browser.context,
|
|
73
|
+
spinnerPage: browser.page,
|
|
74
|
+
message: 'Automation failed',
|
|
75
|
+
details: 'Do it yourself',
|
|
76
|
+
fields: FORM_FIELDS,
|
|
77
|
+
decide: (values) => Promise.resolve({ accepted: `${values.get('clientId')}:${values.get('clientSecret')}` }),
|
|
78
|
+
timeoutMs: NEVER_MS,
|
|
79
|
+
});
|
|
80
|
+
await browser.shown;
|
|
81
|
+
const decision = await browser.submit({ clientId: 'id-1', clientSecret: 'secret-1' });
|
|
82
|
+
expect(decision).toEqual({ accepted: true });
|
|
83
|
+
await expect(request).resolves.toBe('id-1:secret-1');
|
|
84
|
+
});
|
|
85
|
+
it('reports a refusal back to the form and keeps waiting', async () => {
|
|
86
|
+
const browser = createFakeBrowser();
|
|
87
|
+
const decisions = [
|
|
88
|
+
{ problem: 'GitHub did not accept these credentials.' },
|
|
89
|
+
{ accepted: 'second-try' },
|
|
90
|
+
];
|
|
91
|
+
const request = requestCredentialsFromUser({
|
|
92
|
+
context: browser.context,
|
|
93
|
+
spinnerPage: browser.page,
|
|
94
|
+
message: 'Automation failed',
|
|
95
|
+
details: 'Do it yourself',
|
|
96
|
+
fields: FORM_FIELDS,
|
|
97
|
+
decide: () => Promise.resolve(decisions.shift() ?? { problem: 'no more decisions' }),
|
|
98
|
+
timeoutMs: NEVER_MS,
|
|
99
|
+
});
|
|
100
|
+
await browser.shown;
|
|
101
|
+
const refusal = await browser.submit({ clientId: 'id-1', clientSecret: 'wrong' });
|
|
102
|
+
expect(refusal).toEqual({ problem: 'GitHub did not accept these credentials.' });
|
|
103
|
+
const acceptance = await browser.submit({ clientId: 'id-1', clientSecret: 'right' });
|
|
104
|
+
expect(acceptance).toEqual({ accepted: true });
|
|
105
|
+
await expect(request).resolves.toBe('second-try');
|
|
106
|
+
});
|
|
107
|
+
it('turns a failure to build the credentials into a problem for the user', async () => {
|
|
108
|
+
const browser = createFakeBrowser();
|
|
109
|
+
void requestCredentialsFromUser({
|
|
110
|
+
context: browser.context,
|
|
111
|
+
spinnerPage: browser.page,
|
|
112
|
+
message: 'Automation failed',
|
|
113
|
+
details: 'Do it yourself',
|
|
114
|
+
fields: FORM_FIELDS,
|
|
115
|
+
decide: () => Promise.reject(new SyntaxError('That is not a valid client id.')),
|
|
116
|
+
timeoutMs: 50,
|
|
117
|
+
});
|
|
118
|
+
await browser.shown;
|
|
119
|
+
expect(await browser.submit({ clientId: 'nonsense', clientSecret: 'secret-1' })).toEqual({
|
|
120
|
+
problem: 'That is not a valid client id.',
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
it('asks for the missing fields instead of bothering the caller', async () => {
|
|
124
|
+
const browser = createFakeBrowser();
|
|
125
|
+
let decideCalls = 0;
|
|
126
|
+
void requestCredentialsFromUser({
|
|
127
|
+
context: browser.context,
|
|
128
|
+
spinnerPage: browser.page,
|
|
129
|
+
message: 'Automation failed',
|
|
130
|
+
details: 'Do it yourself',
|
|
131
|
+
fields: FORM_FIELDS,
|
|
132
|
+
decide: () => {
|
|
133
|
+
decideCalls++;
|
|
134
|
+
return Promise.resolve({ accepted: 'never' });
|
|
135
|
+
},
|
|
136
|
+
timeoutMs: 50,
|
|
137
|
+
});
|
|
138
|
+
await browser.shown;
|
|
139
|
+
expect(await browser.submit({ clientId: 'id-1' })).toEqual({
|
|
140
|
+
problem: 'Please fill in every field.',
|
|
141
|
+
});
|
|
142
|
+
expect(decideCalls).toBe(0);
|
|
143
|
+
});
|
|
144
|
+
it('gives up when the user closes the browser', async () => {
|
|
145
|
+
const browser = createFakeBrowser();
|
|
146
|
+
const request = requestCredentialsFromUser({
|
|
147
|
+
context: browser.context,
|
|
148
|
+
spinnerPage: browser.page,
|
|
149
|
+
message: 'Automation failed',
|
|
150
|
+
details: 'Do it yourself',
|
|
151
|
+
fields: FORM_FIELDS,
|
|
152
|
+
decide: () => Promise.resolve({ accepted: 'never' }),
|
|
153
|
+
timeoutMs: NEVER_MS,
|
|
154
|
+
});
|
|
155
|
+
await browser.shown;
|
|
156
|
+
browser.closeBrowser();
|
|
157
|
+
await expect(request).resolves.toBeNull();
|
|
158
|
+
});
|
|
159
|
+
it('gives up when the request times out', async () => {
|
|
160
|
+
const browser = createFakeBrowser();
|
|
161
|
+
const request = requestCredentialsFromUser({
|
|
162
|
+
context: browser.context,
|
|
163
|
+
spinnerPage: browser.page,
|
|
164
|
+
message: 'Automation failed',
|
|
165
|
+
details: 'Do it yourself',
|
|
166
|
+
fields: FORM_FIELDS,
|
|
167
|
+
decide: () => Promise.resolve({ accepted: 'never' }),
|
|
168
|
+
timeoutMs: 10,
|
|
169
|
+
});
|
|
170
|
+
await expect(request).resolves.toBeNull();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
/**
|
|
174
|
+
* The overlay is built as a source string, so a mistake in it only shows up in a
|
|
175
|
+
* real browser. Parsing what we would send catches that here instead — with
|
|
176
|
+
* texts full of the characters that would break a naive template.
|
|
177
|
+
*/
|
|
178
|
+
describe('credential request page script', () => {
|
|
179
|
+
const AWKWARD_MESSAGE = "Backtick ` and <b>markup</b> and ${braces} and 'quotes'";
|
|
180
|
+
const AWKWARD_DETAILS = 'Line one\nLine `two` with "quotes" and ${more}';
|
|
181
|
+
it('parses as JavaScript and carries the field texts', async () => {
|
|
182
|
+
const browser = createFakeBrowser();
|
|
183
|
+
void requestCredentialsFromUser({
|
|
184
|
+
context: browser.context,
|
|
185
|
+
spinnerPage: browser.page,
|
|
186
|
+
message: AWKWARD_MESSAGE,
|
|
187
|
+
details: AWKWARD_DETAILS,
|
|
188
|
+
fields: [{ name: 'clientId', label: 'Client `ID`', hint: 'Looks like ${this}' }],
|
|
189
|
+
decide: () => Promise.resolve({ accepted: 'never' }),
|
|
190
|
+
timeoutMs: 10,
|
|
191
|
+
});
|
|
192
|
+
await browser.shown;
|
|
193
|
+
const script = browser.getRenderedScript();
|
|
194
|
+
expect(() => new vm.Script(script)).not.toThrow();
|
|
195
|
+
expect(script).toContain('Client `ID`');
|
|
196
|
+
expect(script).toContain('clientId');
|
|
197
|
+
expect(script).toContain('__latchkeySubmitCredentials');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe('parseSubmittedCredentialValues', () => {
|
|
201
|
+
it('accepts a complete set of values', () => {
|
|
202
|
+
const values = parseSubmittedCredentialValues({ clientId: 'id-1', clientSecret: 'secret-1' }, FORM_FIELDS);
|
|
203
|
+
expect(values?.get('clientId')).toBe('id-1');
|
|
204
|
+
expect(values?.get('clientSecret')).toBe('secret-1');
|
|
205
|
+
});
|
|
206
|
+
it('rejects submissions with a missing or empty field', () => {
|
|
207
|
+
expect(parseSubmittedCredentialValues({ clientId: 'id-1' }, FORM_FIELDS)).toBeNull();
|
|
208
|
+
expect(parseSubmittedCredentialValues({ clientId: 'id-1', clientSecret: '' }, FORM_FIELDS)).toBeNull();
|
|
209
|
+
});
|
|
210
|
+
it('rejects anything that is not a set of values', () => {
|
|
211
|
+
expect(parseSubmittedCredentialValues('token', FORM_FIELDS)).toBeNull();
|
|
212
|
+
expect(parseSubmittedCredentialValues(null, FORM_FIELDS)).toBeNull();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
describe('CredentialFormValues', () => {
|
|
216
|
+
it('rejects reading a field the form does not have', () => {
|
|
217
|
+
const values = new CredentialFormValues(new Map([['token', 'ghp_1']]));
|
|
218
|
+
expect(() => values.get('somethingElse')).toThrow(CredentialFormFieldMissingError);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
//# sourceMappingURL=manualCompletion.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manualCompletion.test.js","sourceRoot":"","sources":["../../tests/manualCompletion.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every browser-automated service should be able to fall back on asking the
|
|
3
|
+
* user for the credentials, so these tests go over the whole registry rather
|
|
4
|
+
* than a single service: the forms are declarative, so they can be checked
|
|
5
|
+
* without a browser.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=manualCredentialForms.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manualCredentialForms.test.d.ts","sourceRoot":"","sources":["../../tests/manualCredentialForms.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every browser-automated service should be able to fall back on asking the
|
|
3
|
+
* user for the credentials, so these tests go over the whole registry rather
|
|
4
|
+
* than a single service: the forms are declarative, so they can be checked
|
|
5
|
+
* without a browser.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { SERVICE_REGISTRY } from '../src/serviceRegistry.js';
|
|
9
|
+
import { BrowserFollowupServiceSession, } from '../src/services/core/base.js';
|
|
10
|
+
import { CredentialFormValues } from '../src/playwrightUtils.js';
|
|
11
|
+
/**
|
|
12
|
+
* Services whose login ends in an OAuth exchange (an authorization code
|
|
13
|
+
* redeemed for access and refresh tokens against an app latchkey created), so
|
|
14
|
+
* there is no value the user could produce by hand and paste back. Asking them
|
|
15
|
+
* for one would be asking for something that does not exist.
|
|
16
|
+
*/
|
|
17
|
+
const SERVICES_WITHOUT_A_PASTEABLE_CREDENTIAL = ['dropbox'];
|
|
18
|
+
function isOAuthOnlyService(service) {
|
|
19
|
+
return (SERVICES_WITHOUT_A_PASTEABLE_CREDENTIAL.includes(service.name) ||
|
|
20
|
+
// Google's services all share one flow: latchkey prepares an OAuth client,
|
|
21
|
+
// then trades a consent for tokens.
|
|
22
|
+
service.name === 'google' ||
|
|
23
|
+
service.name.startsWith('google-'));
|
|
24
|
+
}
|
|
25
|
+
const BROWSER_FOLLOWUP_SESSIONS = SERVICE_REGISTRY.services
|
|
26
|
+
.map((service) => ({ service, session: service.getSession?.('latchkey') }))
|
|
27
|
+
.filter((candidate) => candidate.session instanceof BrowserFollowupServiceSession);
|
|
28
|
+
function formsWithService() {
|
|
29
|
+
return BROWSER_FOLLOWUP_SESSIONS.flatMap(({ service, session }) => session.manualCredentialForm === undefined
|
|
30
|
+
? []
|
|
31
|
+
: [{ service, form: session.manualCredentialForm }]);
|
|
32
|
+
}
|
|
33
|
+
describe('manual credential forms', () => {
|
|
34
|
+
it('finds the browser-automated services to check', () => {
|
|
35
|
+
expect(BROWSER_FOLLOWUP_SESSIONS.length).toBeGreaterThan(5);
|
|
36
|
+
});
|
|
37
|
+
it.each(BROWSER_FOLLOWUP_SESSIONS.filter(({ service }) => !isOAuthOnlyService(service)).map(({ service, session }) => ({ name: service.name, session })))('$name offers a form for finishing the login by hand', ({ session }) => {
|
|
38
|
+
expect(session.manualCredentialForm).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
it.each(formsWithService().map(({ service, form }) => ({
|
|
41
|
+
name: service.name,
|
|
42
|
+
displayName: service.displayName,
|
|
43
|
+
form,
|
|
44
|
+
})))('$name asks for its credentials in a usable way', ({ displayName, form }) => {
|
|
45
|
+
expect(form.instructions.length).toBeGreaterThan(30);
|
|
46
|
+
expect(form.fields.length).toBeGreaterThan(0);
|
|
47
|
+
const fieldNames = form.fields.map((field) => field.name);
|
|
48
|
+
expect(new Set(fieldNames).size).toBe(fieldNames.length);
|
|
49
|
+
for (const field of form.fields) {
|
|
50
|
+
expect(field.name).not.toBe('');
|
|
51
|
+
expect(field.label).not.toBe('');
|
|
52
|
+
}
|
|
53
|
+
// The builder has to agree with the field names, which is the mistake this
|
|
54
|
+
// catches: reading a field the form never asked for throws.
|
|
55
|
+
const values = new CredentialFormValues(new Map(fieldNames.map((fieldName) => [fieldName, `pasted-${fieldName}`])));
|
|
56
|
+
const credentials = form.buildCredentials(values);
|
|
57
|
+
expect(credentials.objectType).not.toBe('');
|
|
58
|
+
// The instructions name the service the user is being sent to, so the page
|
|
59
|
+
// does not read as if it came from nowhere.
|
|
60
|
+
const firstWordOfDisplayName = displayName.split(' ')[0] ?? displayName;
|
|
61
|
+
expect(form.instructions.toLowerCase()).toContain(firstWordOfDisplayName.toLowerCase());
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
//# sourceMappingURL=manualCredentialForms.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manualCredentialForms.test.js","sourceRoot":"","sources":["../../tests/manualCredentialForms.test.ts"],"names":[],"mappings":"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"}
|