latchkey 3.10.1 → 3.11.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.
Files changed (35) hide show
  1. package/dist/src/curlInjection.d.ts +10 -2
  2. package/dist/src/curlInjection.d.ts.map +1 -1
  3. package/dist/src/curlInjection.js +42 -27
  4. package/dist/src/curlInjection.js.map +1 -1
  5. package/dist/src/errorMessages.d.ts +1 -0
  6. package/dist/src/errorMessages.d.ts.map +1 -1
  7. package/dist/src/errorMessages.js +2 -0
  8. package/dist/src/errorMessages.js.map +1 -1
  9. package/dist/src/gateway/gatewayEndpoint.d.ts +11 -0
  10. package/dist/src/gateway/gatewayEndpoint.d.ts.map +1 -1
  11. package/dist/src/gateway/gatewayEndpoint.js +61 -16
  12. package/dist/src/gateway/gatewayEndpoint.js.map +1 -1
  13. package/dist/src/services/core/base.d.ts +17 -0
  14. package/dist/src/services/core/base.d.ts.map +1 -1
  15. package/dist/src/services/core/base.js +20 -0
  16. package/dist/src/services/core/base.js.map +1 -1
  17. package/dist/src/services/core/loginFlows/tokenCapture.d.ts +9 -2
  18. package/dist/src/services/core/loginFlows/tokenCapture.d.ts.map +1 -1
  19. package/dist/src/services/core/loginFlows/tokenCapture.js +103 -15
  20. package/dist/src/services/core/loginFlows/tokenCapture.js.map +1 -1
  21. package/dist/src/services/slack.d.ts +35 -3
  22. package/dist/src/services/slack.d.ts.map +1 -1
  23. package/dist/src/services/slack.js +112 -21
  24. package/dist/src/services/slack.js.map +1 -1
  25. package/dist/src/version.d.ts +1 -1
  26. package/dist/src/version.js +1 -1
  27. package/dist/tests/gateway.test.js +95 -0
  28. package/dist/tests/gateway.test.js.map +1 -1
  29. package/dist/tests/slack.test.d.ts +6 -0
  30. package/dist/tests/slack.test.d.ts.map +1 -0
  31. package/dist/tests/slack.test.js +159 -0
  32. package/dist/tests/slack.test.js.map +1 -0
  33. package/dist/tests/tokenCapture.test.js +103 -2
  34. package/dist/tests/tokenCapture.test.js.map +1 -1
  35. package/package.json +4 -1
@@ -1,7 +1,14 @@
1
1
  /**
2
2
  * A generic browser login for services whose credential is a token the web app
3
- * mints for itself: open a URL, watch the responses that arrive while the user
4
- * signs in, and lift a token out of the JSON body of a named endpoint.
3
+ * mints for itself: open a URL, and lift a token out of the JSON body of a
4
+ * named endpoint, either as it goes by or by asking for it.
5
+ *
6
+ * Watching alone is not enough, because a web app calls its own mint endpoint
7
+ * when it needs a token and not otherwise. ChatGPT is the case that showed
8
+ * this: it signs in, renders the app, and never calls `/api/auth/session` —
9
+ * the endpoint is there and answers with a token, but a login that only
10
+ * listens waits for a request that is never made. So once the browser reaches
11
+ * the endpoint's origin, this flow also asks it directly.
5
12
  *
6
13
  * This is the counterpart to `cookie-capture`, for the other common shape. In a
7
14
  * cookie-authenticated service the cookie *is* the API credential, so capturing
@@ -36,6 +43,12 @@ const TOKEN_PLACEHOLDER = '{token}';
36
43
  * Bearer is what the great majority of token-minting web APIs expect.
37
44
  */
38
45
  const DEFAULT_HEADER_TEMPLATE = `Authorization: Bearer ${TOKEN_PLACEHOLDER}`;
46
+ /**
47
+ * How often the endpoint is asked, once the browser is somewhere it can be
48
+ * asked from. Slow enough not to hammer a service through a login that takes a
49
+ * while, quick enough that the login ends promptly once a token exists.
50
+ */
51
+ const TOKEN_REQUEST_INTERVAL_MS = 2_000;
39
52
  /**
40
53
  * Identity of an endpoint for matching purposes: origin and path, with the
41
54
  * query string and fragment dropped.
@@ -101,24 +114,53 @@ const TokenCaptureParamsSchema = z
101
114
  .optional(),
102
115
  })
103
116
  .strict();
117
+ /**
118
+ * Ask the endpoint from inside the page, so the request is the browser's own:
119
+ * same cookies, same bot-detection clearance the app itself enjoys. Returns
120
+ * null when the endpoint refuses the request.
121
+ */
122
+ function requestTokenBody(page, tokenUrl) {
123
+ return page.evaluate(async (url) => {
124
+ const response = await fetch(url, { credentials: 'include' });
125
+ return response.ok ? await response.text() : null;
126
+ }, tokenUrl);
127
+ }
104
128
  /** Store the captured token the way `latchkey auth set -H` would. */
105
129
  function buildTokenCredentials(headerTemplate, token) {
106
130
  return new RawCurlCredentials(['-H', headerTemplate.replaceAll(TOKEN_PLACEHOLDER, token)]);
107
131
  }
108
132
  /**
109
- * One run of the flow: responses go by, and the first one from the mint
110
- * endpoint that carries a token ends the login.
133
+ * One run of the flow. The endpoint is both watched and asked, and whichever
134
+ * produces a token first ends the login.
111
135
  */
112
136
  class TokenCaptureSession extends SimpleServiceSession {
137
+ tokenUrl;
113
138
  tokenEndpointIdentity;
114
139
  tokenPath;
115
140
  headerTemplate;
141
+ lastRequestedAt = 0;
116
142
  constructor(service, appNamePrefix, tokenUrl, tokenPath, headerTemplate) {
117
143
  super(service, appNamePrefix);
144
+ this.tokenUrl = tokenUrl;
118
145
  this.tokenEndpointIdentity = endpointIdentity(tokenUrl);
119
146
  this.tokenPath = tokenPath;
120
147
  this.headerTemplate = headerTemplate;
121
148
  }
149
+ /**
150
+ * Credentials from one body of the endpoint, or null when it carries no
151
+ * token — including when it is not JSON at all, which is how a bot-detection
152
+ * interstitial or an error page arrives.
153
+ */
154
+ credentialsFromBody(rawBody) {
155
+ let token;
156
+ try {
157
+ token = readTokenAtPath(JSON.parse(rawBody), this.tokenPath);
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ return token === null ? null : buildTokenCredentials(this.headerTemplate, token);
163
+ }
122
164
  async getApiCredentialsFromResponse(response) {
123
165
  let responseUrl;
124
166
  try {
@@ -131,17 +173,61 @@ class TokenCaptureSession extends SimpleServiceSession {
131
173
  if (endpointIdentity(responseUrl) !== this.tokenEndpointIdentity) {
132
174
  return null;
133
175
  }
134
- let token;
176
+ let rawBody;
135
177
  try {
136
- token = readTokenAtPath(JSON.parse(await response.text()), this.tokenPath);
178
+ rawBody = await response.text();
137
179
  }
138
180
  catch {
139
- // Unreadable or not JSON: the endpoint answered with something this
140
- // registration does not describe. Keep waiting rather than fail the
141
- // login — the response that carries the token may still be coming.
181
+ // The body is no longer retrievable — a redirect, or a response the page
182
+ // navigated away from. Keep waiting rather than fail the login; the next
183
+ // request to the endpoint, ours or the app's, gets another chance.
142
184
  return null;
143
185
  }
144
- return token === null ? null : buildTokenCredentials(this.headerTemplate, token);
186
+ return this.credentialsFromBody(rawBody);
187
+ }
188
+ /**
189
+ * Ask the endpoint ourselves, because an app only calls it when it wants a
190
+ * token and may never want one while we are watching.
191
+ *
192
+ * This waits for the browser to arrive on the endpoint's own origin. The
193
+ * request is made from inside the page so that it carries the session, and a
194
+ * cross-origin read of the response would be refused — mid-login the browser
195
+ * is commonly parked on an identity provider, where there is nothing to ask.
196
+ */
197
+ async whileWaitingForLogin(page) {
198
+ if (Date.now() - this.lastRequestedAt < TOKEN_REQUEST_INTERVAL_MS) {
199
+ return;
200
+ }
201
+ let pageUrl;
202
+ try {
203
+ pageUrl = new URL(page.url());
204
+ }
205
+ catch {
206
+ return;
207
+ }
208
+ if (pageUrl.origin !== this.tokenUrl.origin) {
209
+ return;
210
+ }
211
+ this.lastRequestedAt = Date.now();
212
+ let rawBody;
213
+ try {
214
+ rawBody = await requestTokenBody(page, this.tokenUrl.href);
215
+ }
216
+ catch {
217
+ // The page navigated out from under the request, or the endpoint could
218
+ // not be reached. The next poll asks again; a browser the user closed is
219
+ // caught by the wait loop itself.
220
+ return;
221
+ }
222
+ if (rawBody === null) {
223
+ return;
224
+ }
225
+ const credentials = this.credentialsFromBody(rawBody);
226
+ // A response may have produced credentials while this request was in
227
+ // flight, and the first token captured is the one the login keeps.
228
+ if (credentials !== null && this.apiCredentials === null) {
229
+ this.apiCredentials = credentials;
230
+ }
145
231
  }
146
232
  }
147
233
  /**
@@ -168,11 +254,13 @@ export class TokenCaptureLoginFlow {
168
254
  'and the page calls an endpoint of its own to mint a short-lived token. Use',
169
255
  'cookie-capture instead when the cookie is itself the API credential.',
170
256
  '',
171
- 'The login finishes when that endpoint answers with a non-empty token, not',
172
- 'when it is merely requested: a signed-out visitor commonly gets the same',
173
- 'endpoint answering `{}`. A token minted this way expires and cannot be',
174
- 'refreshed — the refresh material stays in the browser — so expect to sign',
175
- 'in again periodically.',
257
+ 'The endpoint is both watched and asked: once the browser reaches its',
258
+ 'origin, it is requested from inside the page every couple of seconds, so a',
259
+ 'web app that never calls it unprompted still works. Either way the login',
260
+ 'finishes when it answers with a non-empty token, not when it is merely',
261
+ 'requested: a signed-out visitor commonly gets the same endpoint answering',
262
+ '`{}`. A token minted this way expires and cannot be refreshed — the refresh',
263
+ 'material stays in the browser — so expect to sign in again periodically.',
176
264
  '',
177
265
  'Example:',
178
266
  ' $ latchkey services register my-app \\',
@@ -1 +1 @@
1
- {"version":3,"file":"tokenCapture.js","sourceRoot":"","sources":["../../../../../src/services/core/loginFlows/tokenCapture.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tokenCapture.js","sourceRoot":"","sources":["../../../../../src/services/core/loginFlows/tokenCapture.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Slack service implementation.
3
3
  */
4
- import type { Response } from 'playwright';
4
+ import type { Page, Response } from 'playwright';
5
5
  import { z } from 'zod';
6
6
  import type { ApiCredentials } from '../apiCredentials/base.js';
7
- import { Service, SimpleServiceSession } from './core/base.js';
7
+ import { LoginFailedError, Service, SimpleServiceSession } from './core/base.js';
8
8
  /**
9
9
  * Slack-specific credentials (token + d cookie).
10
10
  */
@@ -32,9 +32,41 @@ export declare class SlackApiCredentials implements ApiCredentials {
32
32
  toJSON(): SlackApiCredentialsData;
33
33
  static fromJSON(data: SlackApiCredentialsData): SlackApiCredentials;
34
34
  }
35
+ /**
36
+ * Slack signed the user in but the browser has no `d` session cookie, which the
37
+ * API needs alongside the token. Observed after "Reject cookies" in Slack's
38
+ * cookie dialog.
39
+ */
40
+ export declare class SlackSessionCookieMissingError extends LoginFailedError {
41
+ constructor();
42
+ }
43
+ /**
44
+ * The Slack client loaded, so the user is signed in, but none of its responses
45
+ * carried a session token.
46
+ */
47
+ export declare class SlackTokenMissingError extends LoginFailedError {
48
+ constructor();
49
+ }
35
50
  declare class SlackServiceSession extends SimpleServiceSession {
36
- private pendingDCookie;
51
+ /** Token seen in a response that did not carry the `d` cookie. */
52
+ private pendingToken;
53
+ /** When the Slack client was first seen fully loaded, or null until then. */
54
+ private clientLoadedAt;
37
55
  protected getApiCredentialsFromResponse(response: Response): Promise<ApiCredentials | null>;
56
+ /**
57
+ * Notice a login that is stuck: Slack has signed the user in, yet the two
58
+ * things the credentials are made of have not both turned up.
59
+ *
60
+ * A token whose request did not carry the `d` cookie is paired with the
61
+ * cookie from the browser's jar. If the jar has none either, the login is
62
+ * given up on at once: the cookie is set at sign-in, before Slack serves
63
+ * anything containing a token, so it is not still coming.
64
+ *
65
+ * Without a token, the login is given up on once the client has finished
66
+ * loading and has then had ample time to make its own requests without any
67
+ * of them carrying one.
68
+ */
69
+ whileWaitingForLogin(page: Page): Promise<void>;
38
70
  }
39
71
  export declare class Slack extends Service {
40
72
  readonly name = "slack";
@@ -1 +1 @@
1
- {"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../../src/services/slack.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAG/D;;GAEG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;EAIpC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAEhF,qBAAa,mBAAoB,YAAW,cAAc;IACxD,QAAQ,CAAC,UAAU,EAAG,OAAO,CAAU;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEb,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAK1C,kBAAkB,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC;IAUhF,SAAS,IAAI,OAAO,GAAG,SAAS;IAIhC,MAAM,IAAI,uBAAuB;IAQjC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,uBAAuB,GAAG,mBAAmB;CAGpE;AAED,cAAM,mBAAoB,SAAQ,oBAAoB;IACpD,OAAO,CAAC,cAAc,CAAuB;cAE7B,6BAA6B,CAC3C,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;CAmClC;AAED,qBAAa,KAAM,SAAQ,OAAO;IAChC,QAAQ,CAAC,IAAI,WAAW;IACxB,QAAQ,CAAC,WAAW,WAAW;IAC/B,QAAQ,CAAC,WAAW,kEAAmE;IACvF,QAAQ,CAAC,QAAQ,8BAA8B;IAC/C,QAAQ,CAAC,IAAI,SAEyD;IAEtE,QAAQ,CAAC,4BAA4B,+CAAgD;IAErF,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;IAIzC,UAAU,CAAC,aAAa,EAAE,MAAM,GAAG,mBAAmB;cAM5C,8BAA8B,CAC/C,eAAe,EAAE,MAAM,EACvB,YAAY,EAAE,MAAM,GACnB,OAAO;IAKD,UAAU,CAAC,cAAc,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAuB5E;AAED,eAAO,MAAM,KAAK,OAAc,CAAC"}
1
+ {"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../../src/services/slack.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAGjF;;GAEG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;EAIpC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAEhF,qBAAa,mBAAoB,YAAW,cAAc;IACxD,QAAQ,CAAC,UAAU,EAAG,OAAO,CAAU;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEb,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAK1C,kBAAkB,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC;IAUhF,SAAS,IAAI,OAAO,GAAG,SAAS;IAIhC,MAAM,IAAI,uBAAuB;IAQjC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,uBAAuB,GAAG,mBAAmB;CAGpE;AAwBD;;;;GAIG;AACH,qBAAa,8BAA+B,SAAQ,gBAAgB;;CAQnE;AAED;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,gBAAgB;;CAK3D;AA2BD,cAAM,mBAAoB,SAAQ,oBAAoB;IACpD,kEAAkE;IAClE,OAAO,CAAC,YAAY,CAAuB;IAE3C,6EAA6E;IAC7E,OAAO,CAAC,cAAc,CAAuB;cAE7B,6BAA6B,CAC3C,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IA2BjC;;;;;;;;;;;;OAYG;IACY,oBAAoB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAwB/D;AAED,qBAAa,KAAM,SAAQ,OAAO;IAChC,QAAQ,CAAC,IAAI,WAAW;IACxB,QAAQ,CAAC,WAAW,WAAW;IAC/B,QAAQ,CAAC,WAAW,kEAAmE;IACvF,QAAQ,CAAC,QAAQ,8BAA8B;IAC/C,QAAQ,CAAC,IAAI,SAEyD;IAEtE,QAAQ,CAAC,4BAA4B,+CAAgD;IAErF,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;IAIzC,UAAU,CAAC,aAAa,EAAE,MAAM,GAAG,mBAAmB;cAM5C,8BAA8B,CAC/C,eAAe,EAAE,MAAM,EACvB,YAAY,EAAE,MAAM,GACnB,OAAO;IAKD,UAAU,CAAC,cAAc,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAuB5E;AAED,eAAO,MAAM,KAAK,OAAc,CAAC"}
@@ -2,7 +2,7 @@
2
2
  * Slack service implementation.
3
3
  */
4
4
  import { z } from 'zod';
5
- import { Service, SimpleServiceSession } from './core/base.js';
5
+ import { LoginFailedError, Service, SimpleServiceSession } from './core/base.js';
6
6
  import { fetchAccountFromEndpoint, tryParseJson } from '../apiCredentials/account.js';
7
7
  /**
8
8
  * Slack-specific credentials (token + d cookie).
@@ -43,38 +43,129 @@ export class SlackApiCredentials {
43
43
  return new SlackApiCredentials(data.token, data.dCookie);
44
44
  }
45
45
  }
46
+ /**
47
+ * Any host under slack.com. Enterprise Grid workspaces live two labels deep
48
+ * (`<workspace>.enterprise.slack.com`), so a single optional label is not enough.
49
+ */
50
+ const SLACK_URL_PATTERN = /^https:\/\/([a-z0-9-]+\.)*slack\.com\//;
51
+ /** Where the browser ends up once Slack considers the user signed in. */
52
+ const SLACK_CLIENT_URL_PATTERN = /^https:\/\/app\.slack\.com\/client(\/|$)/;
53
+ /** The session token, as embedded in the client's HTML or in raw JSON. */
54
+ const API_TOKEN_PATTERN = /"api_token":"(xoxc-[a-zA-Z0-9-]+)"/;
55
+ const D_COOKIE_HEADER_PATTERN = /\bd=([^;]+)/;
56
+ /**
57
+ * How long after the Slack client has finished loading a token is still waited
58
+ * for. The token may arrive in the client's HTML or in a request the client
59
+ * makes afterwards, so this is deliberately generous: it only delays the
60
+ * failure message, never a login that is going to succeed.
61
+ */
62
+ const TOKEN_SETTLE_PERIOD_MS = 5_000;
63
+ /**
64
+ * Slack signed the user in but the browser has no `d` session cookie, which the
65
+ * API needs alongside the token. Observed after "Reject cookies" in Slack's
66
+ * cookie dialog.
67
+ */
68
+ export class SlackSessionCookieMissingError extends LoginFailedError {
69
+ constructor() {
70
+ super('Login failed: The browser holds no "d" session cookie. ' +
71
+ "Maybe you rejected cookies in Slack's cookie dialog?");
72
+ this.name = 'SlackSessionCookieMissingError';
73
+ }
74
+ }
75
+ /**
76
+ * The Slack client loaded, so the user is signed in, but none of its responses
77
+ * carried a session token.
78
+ */
79
+ export class SlackTokenMissingError extends LoginFailedError {
80
+ constructor() {
81
+ super('Login failed: No session token was found.');
82
+ this.name = 'SlackTokenMissingError';
83
+ }
84
+ }
85
+ async function readDCookieFromJar(page) {
86
+ const cookies = await page.context().cookies();
87
+ const dCookie = cookies.find((cookie) => cookie.name === 'd' && /(^|\.)slack\.com$/.test(cookie.domain));
88
+ return dCookie?.value ?? null;
89
+ }
90
+ /**
91
+ * Whether the browser sits on a fully loaded Slack client. Loading counts
92
+ * subresources too, so on a slow connection this simply takes longer to become
93
+ * true. A page mid-navigation cannot be asked and counts as not loaded.
94
+ */
95
+ async function isSlackClientLoaded(page) {
96
+ if (!SLACK_CLIENT_URL_PATTERN.test(page.url())) {
97
+ return false;
98
+ }
99
+ try {
100
+ const readyState = await page.evaluate('document.readyState');
101
+ return readyState === 'complete';
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
46
107
  class SlackServiceSession extends SimpleServiceSession {
47
- pendingDCookie = null;
108
+ /** Token seen in a response that did not carry the `d` cookie. */
109
+ pendingToken = null;
110
+ /** When the Slack client was first seen fully loaded, or null until then. */
111
+ clientLoadedAt = null;
48
112
  async getApiCredentialsFromResponse(response) {
49
113
  const request = response.request();
50
- const url = request.url();
51
- // Check if the domain is under slack.com
52
- if (!/^https:\/\/([a-z0-9-]+\.)?slack\.com\//.test(url)) {
114
+ if (!SLACK_URL_PATTERN.test(request.url())) {
53
115
  return null;
54
116
  }
55
- const headers = await request.allHeaders();
56
- const cookieHeader = headers.cookie;
57
- if (cookieHeader === undefined) {
117
+ let responseBody;
118
+ try {
119
+ responseBody = await response.text();
120
+ }
121
+ catch {
122
+ // The body is no longer retrievable (a redirect, or a page that has
123
+ // navigated on); other responses get their chance.
58
124
  return null;
59
125
  }
60
- const cookieMatch = /\bd=([^;]+)/.exec(cookieHeader);
61
- if (!cookieMatch?.[1]) {
126
+ const token = API_TOKEN_PATTERN.exec(responseBody)?.[1];
127
+ if (token === undefined) {
62
128
  return null;
63
129
  }
64
- const dCookie = cookieMatch[1];
65
- this.pendingDCookie = dCookie;
66
- // Extract token from response body (JSON embedded in HTML or raw JSON)
67
- try {
68
- const responseBody = await response.text();
69
- const tokenMatch = /"api_token":"(xoxc-[a-zA-Z0-9-]+)"/.exec(responseBody);
70
- if (tokenMatch?.[1]) {
71
- return new SlackApiCredentials(tokenMatch[1], dCookie);
130
+ this.pendingToken = token;
131
+ const cookieHeader = (await request.allHeaders()).cookie;
132
+ const dCookie = cookieHeader === undefined ? undefined : D_COOKIE_HEADER_PATTERN.exec(cookieHeader)?.[1];
133
+ return dCookie === undefined ? null : new SlackApiCredentials(token, dCookie);
134
+ }
135
+ /**
136
+ * Notice a login that is stuck: Slack has signed the user in, yet the two
137
+ * things the credentials are made of have not both turned up.
138
+ *
139
+ * A token whose request did not carry the `d` cookie is paired with the
140
+ * cookie from the browser's jar. If the jar has none either, the login is
141
+ * given up on at once: the cookie is set at sign-in, before Slack serves
142
+ * anything containing a token, so it is not still coming.
143
+ *
144
+ * Without a token, the login is given up on once the client has finished
145
+ * loading and has then had ample time to make its own requests without any
146
+ * of them carrying one.
147
+ */
148
+ async whileWaitingForLogin(page) {
149
+ if (this.apiCredentials !== null) {
150
+ return;
151
+ }
152
+ if (this.pendingToken !== null) {
153
+ const dCookie = await readDCookieFromJar(page);
154
+ if (dCookie === null) {
155
+ throw new SlackSessionCookieMissingError();
72
156
  }
157
+ this.apiCredentials = new SlackApiCredentials(this.pendingToken, dCookie);
158
+ return;
73
159
  }
74
- catch {
75
- // Ignore errors reading response body
160
+ if (this.clientLoadedAt === null) {
161
+ if (await isSlackClientLoaded(page)) {
162
+ this.clientLoadedAt = Date.now();
163
+ }
164
+ return;
165
+ }
166
+ if (Date.now() - this.clientLoadedAt >= TOKEN_SETTLE_PERIOD_MS) {
167
+ throw new SlackTokenMissingError();
76
168
  }
77
- return null;
78
169
  }
79
170
  }
80
171
  export class Slack extends Service {
@@ -1 +1 @@
1
- {"version":3,"file":"slack.js","sourceRoot":"","sources":["../../../src/services/slack.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"slack.js","sourceRoot":"","sources":["../../../src/services/slack.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "3.10.1";
1
+ export declare const VERSION = "3.11.1";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,4 +1,4 @@
1
1
  // Auto-generated from package.json by scripts/generateVersion.js.
2
2
  // Do not edit by hand; run `node scripts/generateVersion.js` to refresh.
3
- export const VERSION = "3.10.1";
3
+ export const VERSION = "3.11.1";
4
4
  //# sourceMappingURL=version.js.map
@@ -83,6 +83,7 @@ describe('buildCurlArguments', () => {
83
83
  ['X-Latchkey-Gateway-Password', 'sekret'],
84
84
  ['X-Latchkey-Gateway-Permissions-Override', 'jwt-payload'],
85
85
  ['X-Latchkey-Gateway-Account', 'jou'],
86
+ ['X-Latchkey-Gateway-No-Credentials', '1'],
86
87
  ]);
87
88
  const args = buildCurlArguments('GET', headers, 'https://api.example.com/test', false);
88
89
  expect(args).toContain('Content-Type: application/json');
@@ -90,6 +91,7 @@ describe('buildCurlArguments', () => {
90
91
  expect(joined).not.toContain('x-latchkey-gateway-password');
91
92
  expect(joined).not.toContain('x-latchkey-gateway-permissions-override');
92
93
  expect(joined).not.toContain('x-latchkey-gateway-account');
94
+ expect(joined).not.toContain('x-latchkey-gateway-no-credentials');
93
95
  expect(joined).not.toContain('sekret');
94
96
  expect(joined).not.toContain('jwt-payload');
95
97
  });
@@ -760,6 +762,99 @@ describe('gateway server', () => {
760
762
  expect(body.error).toContain('missing-permissions.json');
761
763
  });
762
764
  });
765
+ describe('no-credentials header', () => {
766
+ const noCredentialsHeaders = { 'X-Latchkey-Gateway-No-Credentials': '1' };
767
+ const DEFAULT_CREDENTIALS = {
768
+ slack: { objectType: 'rawCurl', curlArguments: ['-H', 'Authorization: Bearer test-token'] },
769
+ };
770
+ // The header is only honored on a gateway that allows forwarding requests
771
+ // without credential injection.
772
+ function createPassthroughGateway(optionOverrides = {}) {
773
+ return createTestGateway(DEFAULT_CREDENTIALS, {}, optionOverrides, {
774
+ passthroughUnknown: true,
775
+ });
776
+ }
777
+ function headerArgumentsOf(args) {
778
+ const headerArguments = [];
779
+ for (let index = 0; index < args.length; index++) {
780
+ if (args[index] === '-H' && index + 1 < args.length) {
781
+ headerArguments.push(args[index + 1]);
782
+ }
783
+ }
784
+ return headerArguments;
785
+ }
786
+ it('is refused unless the gateway allows uninjected requests', async () => {
787
+ gateway = await createTestGateway();
788
+ const response = await fetch('/gateway/https://slack.com/api/auth.test', {
789
+ headers: { ...noCredentialsHeaders, Authorization: 'Bearer already-injected' },
790
+ });
791
+ expect(response.status).toBe(403);
792
+ expect(await response.text()).toContain('LATCHKEY_PASSTHROUGH_UNKNOWN');
793
+ expect(capturedCurlArgs).toEqual([]);
794
+ expect(capturedPermissionCheckBody).toBeUndefined();
795
+ });
796
+ it('forwards the request as received, without injecting stored credentials', async () => {
797
+ gateway = await createPassthroughGateway();
798
+ const response = await fetch('/gateway/https://slack.com/api/auth.test', {
799
+ headers: { ...noCredentialsHeaders, Authorization: 'Bearer already-injected' },
800
+ });
801
+ expect(response.status).toBe(200);
802
+ const headerArguments = headerArgumentsOf(capturedCurlArgs);
803
+ expect(headerArguments).toContain('Authorization: Bearer already-injected');
804
+ expect(headerArguments).not.toContain('Authorization: Bearer test-token');
805
+ expect(capturedCurlArgs).toContain('https://slack.com/api/auth.test');
806
+ });
807
+ it('never forwards the header itself upstream', async () => {
808
+ gateway = await createPassthroughGateway();
809
+ await fetch('/gateway/https://slack.com/api/auth.test', { headers: noCredentialsHeaders });
810
+ expect(capturedCurlArgs.join('\n').toLowerCase()).not.toContain('x-latchkey-gateway-no-credentials');
811
+ });
812
+ it('still enforces the permission check', async () => {
813
+ mockPermissionResult = false;
814
+ gateway = await createPassthroughGateway();
815
+ const response = await fetch('/gateway/https://slack.com/api/auth.test', {
816
+ headers: noCredentialsHeaders,
817
+ });
818
+ expect(response.status).toBe(403);
819
+ expect(capturedCurlArgs).toEqual([]);
820
+ });
821
+ it('checks permissions against the real request body', async () => {
822
+ gateway = await createPassthroughGateway();
823
+ const requestBody = JSON.stringify({ channel: 'general', text: 'hello' });
824
+ const response = await fetch('/gateway/https://slack.com/api/chat.postMessage', {
825
+ method: 'POST',
826
+ headers: { ...noCredentialsHeaders, 'Content-Type': 'application/json' },
827
+ body: requestBody,
828
+ });
829
+ expect(response.status).toBe(200);
830
+ expect(capturedPermissionCheckBody).toBe(requestBody);
831
+ });
832
+ it('does not require the target to match a known service', async () => {
833
+ gateway = await createPassthroughGateway();
834
+ const response = await fetch('/gateway/https://unknown-api.example.com/test', {
835
+ headers: noCredentialsHeaders,
836
+ });
837
+ expect(response.status).toBe(200);
838
+ expect(capturedCurlArgs).toContain('https://unknown-api.example.com/test');
839
+ });
840
+ it('still requires the gateway password', async () => {
841
+ gateway = await createPassthroughGateway({ password: 'sekret' });
842
+ const unauthenticated = await fetch('/gateway/https://slack.com/api/auth.test', {
843
+ headers: noCredentialsHeaders,
844
+ });
845
+ expect(unauthenticated.status).toBe(401);
846
+ const authenticated = await fetch('/gateway/https://slack.com/api/auth.test', {
847
+ headers: { ...noCredentialsHeaders, 'X-Latchkey-Gateway-Password': 'sekret' },
848
+ });
849
+ expect(authenticated.status).toBe(200);
850
+ });
851
+ it('is ignored when absent, so ordinary requests still get credentials', async () => {
852
+ gateway = await createPassthroughGateway();
853
+ const response = await fetch('/gateway/https://slack.com/api/auth.test');
854
+ expect(response.status).toBe(200);
855
+ expect(capturedCurlArgs).toContain('Authorization: Bearer test-token');
856
+ });
857
+ });
763
858
  describe('services registered while the gateway runs', () => {
764
859
  const REGISTERED_BASE_URL = 'https://selfhosted.example.com/api/';
765
860
  const CREDENTIALS = {