@teleporthq/teleport-plugin-next-workflows 0.45.2 → 0.45.4

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 (54) hide show
  1. package/__tests__/analytics-track-outcome.test.ts +71 -3
  2. package/__tests__/auth-password-security.test.ts +101 -0
  3. package/__tests__/automations-bus.test.ts +85 -2
  4. package/__tests__/security-lists-parity.test.ts +2 -2
  5. package/__tests__/security-runtime.test.ts +37 -20
  6. package/dist/cjs/auth-generator.d.ts.map +1 -1
  7. package/dist/cjs/auth-generator.js +48 -19
  8. package/dist/cjs/auth-generator.js.map +1 -1
  9. package/dist/cjs/automations/event-bus-source.d.ts.map +1 -1
  10. package/dist/cjs/automations/event-bus-source.js +37 -23
  11. package/dist/cjs/automations/event-bus-source.js.map +1 -1
  12. package/dist/cjs/nodes/account/account-request-password-reset.d.ts.map +1 -1
  13. package/dist/cjs/nodes/account/account-request-password-reset.js +3 -2
  14. package/dist/cjs/nodes/account/account-request-password-reset.js.map +1 -1
  15. package/dist/cjs/nodes/analytics/analytics-track-outcome.d.ts.map +1 -1
  16. package/dist/cjs/nodes/analytics/analytics-track-outcome.js +17 -4
  17. package/dist/cjs/nodes/analytics/analytics-track-outcome.js.map +1 -1
  18. package/dist/cjs/password-reset-route-generator.d.ts +2 -0
  19. package/dist/cjs/password-reset-route-generator.d.ts.map +1 -1
  20. package/dist/cjs/password-reset-route-generator.js +17 -10
  21. package/dist/cjs/password-reset-route-generator.js.map +1 -1
  22. package/dist/cjs/security-runtime/security-runtime-code.d.ts +3 -2
  23. package/dist/cjs/security-runtime/security-runtime-code.d.ts.map +1 -1
  24. package/dist/cjs/security-runtime/security-runtime-code.js +17 -13
  25. package/dist/cjs/security-runtime/security-runtime-code.js.map +1 -1
  26. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  27. package/dist/esm/auth-generator.d.ts.map +1 -1
  28. package/dist/esm/auth-generator.js +48 -19
  29. package/dist/esm/auth-generator.js.map +1 -1
  30. package/dist/esm/automations/event-bus-source.d.ts.map +1 -1
  31. package/dist/esm/automations/event-bus-source.js +37 -23
  32. package/dist/esm/automations/event-bus-source.js.map +1 -1
  33. package/dist/esm/nodes/account/account-request-password-reset.d.ts.map +1 -1
  34. package/dist/esm/nodes/account/account-request-password-reset.js +3 -2
  35. package/dist/esm/nodes/account/account-request-password-reset.js.map +1 -1
  36. package/dist/esm/nodes/analytics/analytics-track-outcome.d.ts.map +1 -1
  37. package/dist/esm/nodes/analytics/analytics-track-outcome.js +17 -4
  38. package/dist/esm/nodes/analytics/analytics-track-outcome.js.map +1 -1
  39. package/dist/esm/password-reset-route-generator.d.ts +2 -0
  40. package/dist/esm/password-reset-route-generator.d.ts.map +1 -1
  41. package/dist/esm/password-reset-route-generator.js +16 -9
  42. package/dist/esm/password-reset-route-generator.js.map +1 -1
  43. package/dist/esm/security-runtime/security-runtime-code.d.ts +3 -2
  44. package/dist/esm/security-runtime/security-runtime-code.d.ts.map +1 -1
  45. package/dist/esm/security-runtime/security-runtime-code.js +17 -13
  46. package/dist/esm/security-runtime/security-runtime-code.js.map +1 -1
  47. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  48. package/package.json +2 -2
  49. package/src/auth-generator.ts +48 -19
  50. package/src/automations/event-bus-source.ts +37 -23
  51. package/src/nodes/account/account-request-password-reset.ts +3 -2
  52. package/src/nodes/analytics/analytics-track-outcome.ts +17 -4
  53. package/src/password-reset-route-generator.ts +18 -9
  54. package/src/security-runtime/security-runtime-code.ts +17 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teleporthq/teleport-plugin-next-workflows",
3
- "version": "0.45.2",
3
+ "version": "0.45.4",
4
4
  "description": "A plugin for workflow code generation in Next.js projects.",
5
5
  "author": "teleportHQ",
6
6
  "license": "MIT",
@@ -69,5 +69,5 @@
69
69
  "@teleporthq/teleport-shared": "^0.45.0",
70
70
  "@teleporthq/teleport-types": "^0.45.0"
71
71
  },
72
- "gitHead": "3be925a09c56ec0afe44940a623cbfcc54e5170f"
72
+ "gitHead": "4fc86526c794a1304f0298809bf3530b4d380486"
73
73
  }
@@ -730,13 +730,15 @@ export const PASSWORD_RESETS_TABLE = 'teleport_password_resets'
730
730
  const PG_PASSWORD_RESET_FUNCTIONS = `
731
731
  // Password-reset links (pages/api/auth/forgot-password + reset-password). Only
732
732
  // the SHA-256 of a link's token is stored, bound to the account id, with its
733
- // expiry; the table is created on first use. The earlier password_reset_tokens
734
- // table and its rows are never read.
733
+ // expiry. The editor makes the table with the other account tables, and a site's
734
+ // database role may read and write it without being allowed to create one
735
+ // (CREATE ... IF NOT EXISTS asks for that permission even when the table is
736
+ // there), so a create is attempted only for a table that really is missing. The
737
+ // earlier password_reset_tokens table and its rows are never read.
735
738
  var PASSWORD_RESETS_TABLE = '${PASSWORD_RESETS_TABLE}';
736
- var __passwordResetsTableReady = false;
739
+ var UNDEFINED_TABLE_CODE = '42P01';
737
740
 
738
- async function ensurePasswordResetsTable(client) {
739
- if (__passwordResetsTableReady) return;
741
+ async function createPasswordResetsTable(client) {
740
742
  await client.query('BEGIN');
741
743
  try {
742
744
  await client.query('SELECT pg_advisory_xact_lock(hashtext($1))', [PASSWORD_RESETS_TABLE]);
@@ -748,28 +750,53 @@ async function ensurePasswordResetsTable(client) {
748
750
  await client.query('COMMIT');
749
751
  } catch (err) {
750
752
  try { await client.query('ROLLBACK'); } catch (_e) {}
751
- throw err;
753
+ throw new Error('[password-reset] the table ' + PASSWORD_RESETS_TABLE + ' is missing and could not be created: ' + (err && err.message ? err.message : String(err)));
754
+ }
755
+ }
756
+
757
+ // Throws when a link could not be kept. Asked for every address, with an account
758
+ // or without, so a table that cannot be used refuses them all alike.
759
+ async function ensurePasswordResetsUsable() {
760
+ const client = getClient();
761
+ try {
762
+ await client.connect();
763
+ try {
764
+ await client.query('SELECT 1 FROM ' + PASSWORD_RESETS_TABLE + ' LIMIT 1');
765
+ } catch (err) {
766
+ if (!err || err.code !== UNDEFINED_TABLE_CODE) throw err;
767
+ await createPasswordResetsTable(client);
768
+ }
769
+ } finally {
770
+ try { await client.end(); } catch (_e) {}
752
771
  }
753
- __passwordResetsTableReady = true;
754
772
  }
755
773
 
756
774
  // One live link per account: a new one ends the ones sent before it.
775
+ async function writePasswordReset(client, userId, tokenHash, ttlSeconds) {
776
+ await client.query(
777
+ 'UPDATE ' + PASSWORD_RESETS_TABLE + ' SET used_at = NOW() WHERE user_id = $1 AND used_at IS NULL',
778
+ [String(userId)]
779
+ );
780
+ await client.query(
781
+ 'INSERT INTO ' + PASSWORD_RESETS_TABLE + " (token_hash, user_id, expires_at) VALUES ($1, $2, NOW() + ($3::int * INTERVAL '1 second'))",
782
+ [tokenHash, String(userId), ttlSeconds]
783
+ );
784
+ await client.query(
785
+ 'DELETE FROM ' + PASSWORD_RESETS_TABLE + " WHERE expires_at < NOW() - INTERVAL '1 day'"
786
+ );
787
+ }
788
+
757
789
  async function storePasswordReset(userId, tokenHash, ttlSeconds) {
758
790
  const client = getClient();
759
791
  try {
760
792
  await client.connect();
761
- await ensurePasswordResetsTable(client);
762
- await client.query(
763
- 'UPDATE ' + PASSWORD_RESETS_TABLE + ' SET used_at = NOW() WHERE user_id = $1 AND used_at IS NULL',
764
- [String(userId)]
765
- );
766
- await client.query(
767
- 'INSERT INTO ' + PASSWORD_RESETS_TABLE + " (token_hash, user_id, expires_at) VALUES ($1, $2, NOW() + ($3::int * INTERVAL '1 second'))",
768
- [tokenHash, String(userId), ttlSeconds]
769
- );
770
- await client.query(
771
- 'DELETE FROM ' + PASSWORD_RESETS_TABLE + " WHERE expires_at < NOW() - INTERVAL '1 day'"
772
- );
793
+ try {
794
+ await writePasswordReset(client, userId, tokenHash, ttlSeconds);
795
+ } catch (err) {
796
+ if (!err || err.code !== UNDEFINED_TABLE_CODE) throw err;
797
+ await createPasswordResetsTable(client);
798
+ await writePasswordReset(client, userId, tokenHash, ttlSeconds);
799
+ }
773
800
  } finally {
774
801
  try { await client.end(); } catch (_e) {}
775
802
  }
@@ -825,6 +852,7 @@ const PASSWORD_RESET_SUPPORTED = true;`
825
852
  const UNSUPPORTED_PASSWORD_RESET_FUNCTIONS = `
826
853
  // Password-reset links need a Postgres database; this project's user store is
827
854
  // not one, so the reset routes answer without ever sending a link.
855
+ async function ensurePasswordResetsUsable() { throw new Error('Password reset needs a Postgres database'); }
828
856
  async function storePasswordReset() { throw new Error('Password reset needs a Postgres database'); }
829
857
  async function consumePasswordReset() { return null; }
830
858
  async function passwordResetIsLive() { return false; }
@@ -1349,6 +1377,7 @@ ${
1349
1377
  module.exports.checkSignInAllowed = checkSignInAllowed;
1350
1378
  module.exports.countSignInFailure = countSignInFailure;
1351
1379
  module.exports.SIGN_IN_VISITOR_HEADER = SIGN_IN_VISITOR_HEADER;
1380
+ module.exports.ensurePasswordResetsUsable = ensurePasswordResetsUsable;
1352
1381
  module.exports.storePasswordReset = storePasswordReset;
1353
1382
  module.exports.consumePasswordReset = consumePasswordReset;
1354
1383
  module.exports.passwordResetIsLive = passwordResetIsLive;
@@ -89,10 +89,15 @@ async function saveWait(wait) {
89
89
 
90
90
  // The signed requests the dispatch route has taken, each kept until its
91
91
  // signature can no longer verify: the same request again is refused. The
92
- // table (and its expiry index) is made on first use, two instances taking
93
- // turns under the lock. The rows that are over are deleted at most once per
94
- // instance every SIGNED_REQUESTS_CLEANUP_MS, a bounded batch at a time.
92
+ // editor makes the table with the other automation tables, and a site's
93
+ // database role may read and write it without being allowed to create one
94
+ // (CREATE ... IF NOT EXISTS asks for that permission even when the table is
95
+ // there), so a create is attempted only for a table that really is missing,
96
+ // two instances taking turns under the lock. The rows that are over are
97
+ // deleted at most once per instance every SIGNED_REQUESTS_CLEANUP_MS, a
98
+ // bounded batch at a time.
95
99
  var SIGNED_REQUESTS_TABLE = '${SIGNED_REQUESTS_TABLE}';
100
+ var UNDEFINED_TABLE_CODE = '42P01';
96
101
  var LOCK_SIGNED_REQUESTS_SQL = 'SELECT pg_advisory_xact_lock(hashtext($1))';
97
102
  var CREATE_SIGNED_REQUESTS_SQL = 'CREATE TABLE IF NOT EXISTS ' + SIGNED_REQUESTS_TABLE +
98
103
  ' (request_key TEXT PRIMARY KEY, expires_at BIGINT NOT NULL)';
@@ -103,34 +108,43 @@ var FORGET_SIGNED_REQUESTS_SQL = 'DELETE FROM ' + SIGNED_REQUESTS_TABLE + ' WHER
103
108
  var TAKE_SIGNED_REQUEST_SQL = 'INSERT INTO ' + SIGNED_REQUESTS_TABLE + ' (request_key, expires_at) VALUES ($1, $2) ' +
104
109
  'ON CONFLICT (request_key) DO NOTHING RETURNING request_key';
105
110
  var SIGNED_REQUESTS_CLEANUP_MS = 600000;
106
- var __signedRequestsReady = false;
107
111
  var __signedRequestsCleanedAt = 0;
108
112
 
113
+ async function createSignedRequestsTable(client) {
114
+ await client.query('BEGIN');
115
+ try {
116
+ await client.query(LOCK_SIGNED_REQUESTS_SQL, [SIGNED_REQUESTS_TABLE]);
117
+ await client.query(CREATE_SIGNED_REQUESTS_SQL);
118
+ await client.query(INDEX_SIGNED_REQUESTS_SQL);
119
+ await client.query('COMMIT');
120
+ } catch (error) {
121
+ try { await client.query('ROLLBACK'); } catch (_e) {}
122
+ throw new Error('[automations] the table ' + SIGNED_REQUESTS_TABLE + ' is missing and could not be created: ' + errorMessage(error));
123
+ }
124
+ }
125
+
126
+ async function recordSignedRequest(client, key, expiresAt) {
127
+ var nowMs = Date.now();
128
+ if (nowMs - __signedRequestsCleanedAt >= SIGNED_REQUESTS_CLEANUP_MS) {
129
+ __signedRequestsCleanedAt = nowMs;
130
+ await client.query(FORGET_SIGNED_REQUESTS_SQL, [Math.floor(nowMs / 1000)]);
131
+ }
132
+ var taken = await client.query(TAKE_SIGNED_REQUEST_SQL, [key, expiresAt]);
133
+ return !!(taken && taken.rows && taken.rows[0]);
134
+ }
135
+
109
136
  // true the first time a signed request (its key) is taken, false every time
110
137
  // after while it is kept (until expiresAt, unix seconds). Throws when the
111
138
  // database cannot say: the route then refuses.
112
139
  async function takeSignedRequest(key, expiresAt) {
113
140
  return withClient(async function (client) {
114
- if (!__signedRequestsReady) {
115
- await client.query('BEGIN');
116
- try {
117
- await client.query(LOCK_SIGNED_REQUESTS_SQL, [SIGNED_REQUESTS_TABLE]);
118
- await client.query(CREATE_SIGNED_REQUESTS_SQL);
119
- await client.query(INDEX_SIGNED_REQUESTS_SQL);
120
- await client.query('COMMIT');
121
- } catch (error) {
122
- try { await client.query('ROLLBACK'); } catch (_e) {}
123
- throw error;
124
- }
125
- __signedRequestsReady = true;
126
- }
127
- var nowMs = Date.now();
128
- if (nowMs - __signedRequestsCleanedAt >= SIGNED_REQUESTS_CLEANUP_MS) {
129
- __signedRequestsCleanedAt = nowMs;
130
- await client.query(FORGET_SIGNED_REQUESTS_SQL, [Math.floor(nowMs / 1000)]);
141
+ try {
142
+ return await recordSignedRequest(client, key, expiresAt);
143
+ } catch (error) {
144
+ if (!error || error.code !== UNDEFINED_TABLE_CODE) throw error;
145
+ await createSignedRequestsTable(client);
146
+ return recordSignedRequest(client, key, expiresAt);
131
147
  }
132
- var taken = await client.query(TAKE_SIGNED_REQUEST_SQL, [key, expiresAt]);
133
- return !!(taken && taken.rows && taken.rows[0]);
134
148
  });
135
149
  }
136
150
 
@@ -5,8 +5,9 @@ import { NodeHandlerGenerator, handlerToString } from '../types'
5
5
  // server: whether the address has an account, the link's token (minted there,
6
6
  // stored hashed), the page it opens and the email itself (provider, sender and
7
7
  // template baked from this node's config at generation). The route answers the
8
- // same whether the address has an account or not, so this node always reports
9
- // success for a well-formed request.
8
+ // same whether the address has an account or not, so this node reports success
9
+ // for a well-formed request unless the site could not keep a link, for any
10
+ // address.
10
11
  async function account_request_password_reset(config: any, context: Record<string, unknown>) {
11
12
  const email = config && typeof config.email === 'string' ? config.email.trim() : ''
12
13
  if (!email) {
@@ -14,6 +14,11 @@ import { NodeHandlerGenerator } from '../types'
14
14
  * carries the ids the trigger stamped on the context (`__tpa`, set by every
15
15
  * generated trigger) plus whatever first-touch snapshot the caller binds in
16
16
  * `attribution` (the checkout stamps `utm_*`, `referrer_host`, `landing_path`).
17
+ * A snapshot naming a `session_id` is the visit that placed an order, read back
18
+ * by a run outside that visit (a payment webhook, a cron, an admin page): its
19
+ * `session_id`, `visitor_id` and `exp` stand in for the trigger's ids. Otherwise
20
+ * the visitor id is the tracker's alone: a browser may still hold a persistent
21
+ * id from before its consent was withdrawn.
17
22
  *
18
23
  * Never throws: an outcome that cannot be reported answers `{ sent: false }`
19
24
  * and the workflow continues — analytics must never cost a sale.
@@ -60,22 +65,30 @@ const SERVER_BODY = `async function analytics_track_outcome(config, context) {
60
65
  if (baseUrl.indexOf('http') !== 0 || !publicKey || !serverKey) {
61
66
  return { sent: false, reason: 'not-configured' };
62
67
  }
68
+ // The service dedupes on this id and drops an outcome without one.
69
+ if (config.outcomeId == null || config.outcomeId === '') {
70
+ return { sent: false, reason: 'no-outcome-id' };
71
+ }
63
72
  var ids = context && context.__tpa && typeof context.__tpa === 'object' ? context.__tpa : {};
64
73
  var attribution = config.attribution && typeof config.attribution === 'object' ? config.attribution : {};
74
+ var visit = attribution.session_id
75
+ ? { sessionId: attribution.session_id, visitorId: attribution.visitor_id, exp: attribution.exp }
76
+ : ids;
65
77
  var outcome = {
66
78
  name: name,
67
79
  value: config.value == null || config.value === '' ? null : Number(config.value),
68
80
  currency: config.currency == null ? null : String(config.currency),
69
81
  outcomeId: config.outcomeId == null ? null : String(config.outcomeId),
70
82
  recordId: config.recordId == null ? null : String(config.recordId),
71
- sessionId: ids.sessionId || null,
72
- visitorId: ids.visitorId || attribution.visitor_id || null,
73
- path: ids.path || null,
74
- exp: ids.exp || null,
83
+ sessionId: visit.sessionId || null,
84
+ visitorId: visit.visitorId || null,
85
+ path: visit.path || null,
86
+ exp: visit.exp || null,
75
87
  attribution: {
76
88
  utmSource: attribution.utm_source || attribution.utmSource || null,
77
89
  utmMedium: attribution.utm_medium || attribution.utmMedium || null,
78
90
  utmCampaign: attribution.utm_campaign || attribution.utmCampaign || null,
91
+ utmContent: attribution.utm_content || attribution.utmContent || null,
79
92
  referrerHost: attribution.referrer_host || attribution.referrerHost || null,
80
93
  landingPath: attribution.landing_path || attribution.landingPath || null
81
94
  }
@@ -55,6 +55,10 @@ export const DEFAULT_RESET_PAGE_PATH = '/reset-password'
55
55
  export const FORGOT_PASSWORD_ANSWER =
56
56
  'If an account exists for that email, we sent it a link to reset the password.'
57
57
 
58
+ /** What the route answers, to every address, when a link could not be kept. */
59
+ export const FORGOT_PASSWORD_NOT_SENT =
60
+ 'We could not send the reset link. Please try again in a moment.'
61
+
58
62
  /**
59
63
  * The sessionStorage key the reset page reads the link's token from (mirrored
60
64
  * in teleport-gui `RESET_TOKEN_STORAGE_KEY`). The published site's first head
@@ -333,7 +337,9 @@ module.exports = async function handler(req, res) {
333
337
  return __security.sendClientError(res, 503, 'We could not reach the account service. Please try again in a moment.', err);
334
338
  }
335
339
 
340
+ var link = null;
336
341
  try {
342
+ await authUtils.ensurePasswordResetsUsable();
337
343
  // Counted for every address, with an account or without, so neither the
338
344
  // answer nor its timing differs.
339
345
  var recipient = await __security.rateLimit({
@@ -345,17 +351,20 @@ module.exports = async function handler(req, res) {
345
351
  if (user && userId != null && user.email && !recipient.allowed) {
346
352
  console.warn('[forgot-password] reset link not sent: this address was sent ${FORGOT_PASSWORD_RECIPIENT_LIMIT} in the last hour');
347
353
  } else if (user && userId != null && user.email) {
348
- if (!authUtils.PASSWORD_RESET_SUPPORTED) {
349
- console.warn('[forgot-password] reset link not sent: password reset needs a Postgres user store');
350
- } else {
351
- var token = crypto.randomBytes(32).toString('hex');
352
- await authUtils.storePasswordReset(userId, sha256(token), TOKEN_TTL_SECONDS);
353
- var resetUrl = trustedOrigin(req) + RESET_PAGE_PATH + TOKEN_LINK_PREFIX + token;
354
- await sendResetEmail(user, resetUrl, token, __emailLocale.resolveRequestLocale(req));
355
- }
354
+ var token = crypto.randomBytes(32).toString('hex');
355
+ await authUtils.storePasswordReset(userId, sha256(token), TOKEN_TTL_SECONDS);
356
+ link = { token: token, url: trustedOrigin(req) + RESET_PAGE_PATH + TOKEN_LINK_PREFIX + token };
356
357
  }
357
358
  } catch (err) {
358
- console.error('[forgot-password] could not send a reset link:', err && err.message ? err.message : err);
359
+ await waitUntilElapsed(startedAt, MIN_RESPONSE_MS);
360
+ return __security.sendClientError(res, 503, ${JSON.stringify(FORGOT_PASSWORD_NOT_SENT)}, err);
361
+ }
362
+ if (link) {
363
+ try {
364
+ await sendResetEmail(user, link.url, link.token, __emailLocale.resolveRequestLocale(req));
365
+ } catch (err) {
366
+ console.error('[forgot-password] could not send a reset link:', err && err.message ? err.message : err);
367
+ }
359
368
  }
360
369
 
361
370
  await waitUntilElapsed(startedAt, MIN_RESPONSE_MS);
@@ -38,8 +38,9 @@ export const securityRuntimeSpecifier = (relativePrefix: string): string =>
38
38
  * itself, an IPv6 one its /64 (an IPv4 tail counted as the two groups it
39
39
  * fills). Every rate limit keyed by visitor uses it.
40
40
  * - `rateLimit` counts fixed windows in the app's Postgres database (table
41
- * `teleport_rate_limits`, created on first use under an advisory lock), so
42
- * every instance of a serverless app counts together. Only an app with no
41
+ * `teleport_rate_limits`, which the editor makes, created here under an
42
+ * advisory lock only when it is really missing), so every instance of a
43
+ * serverless app counts together. Only an app with no
43
44
  * database (or no `pg` driver) counts in a bounded per-instance map; a
44
45
  * database error counts in that map for a short while and is logged once.
45
46
  * A request waits on the database at most 1.5 s; after a failure one
@@ -102,6 +103,9 @@ var CLEANUP_MAX_BATCHES = 3;
102
103
  var CLEANUP_QUERY_TIMEOUT_MS = 5000;
103
104
  var MEMORY_MAX_WINDOWS = 10000;
104
105
 
106
+ // A site's database role may read and write its tables without being allowed
107
+ // to create one, and CREATE ... IF NOT EXISTS asks for that permission even when
108
+ // the table is there: these run only for a table that really is missing.
105
109
  var CREATE_SQL = 'CREATE TABLE IF NOT EXISTS "' + RATE_LIMITS_TABLE + '" (' +
106
110
  '"key" TEXT NOT NULL, "window_start" BIGINT NOT NULL, "count" INTEGER NOT NULL DEFAULT 0, ' +
107
111
  '"expires_at" BIGINT NOT NULL, PRIMARY KEY ("key", "window_start"))';
@@ -298,7 +302,6 @@ function __memoryHit(storageKey, windowStart, windowSeconds, nowSec) {
298
302
  return entry.count;
299
303
  }
300
304
 
301
- var __tableReady = false;
302
305
  // Healthy, every request asks the database. After a failure, requests count in
303
306
  // memory until __databaseRetryAt; then ONE request probes (it pushes the retry
304
307
  // time back before it asks), so the requests beside it never wait on a
@@ -377,12 +380,15 @@ async function __databaseHit(storageKey, windowStart, expiresAt, nowSec) {
377
380
  return within(client.query(sql, values || []), QUERY_TIMEOUT_MS);
378
381
  };
379
382
  var createTable = async function () {
380
- await run('BEGIN');
381
- await run(LOCK_SQL, [RATE_LIMITS_TABLE]);
382
- await run(CREATE_SQL);
383
- await run(INDEX_SQL);
384
- await run('COMMIT');
385
- __tableReady = true;
383
+ try {
384
+ await run('BEGIN');
385
+ await run(LOCK_SQL, [RATE_LIMITS_TABLE]);
386
+ await run(CREATE_SQL);
387
+ await run(INDEX_SQL);
388
+ await run('COMMIT');
389
+ } catch (err) {
390
+ throw new Error('the table ' + RATE_LIMITS_TABLE + ' is missing and could not be created: ' + __errorText(err));
391
+ }
386
392
  };
387
393
  var hit = function () {
388
394
  return run(HIT_SQL, [storageKey, windowStart, expiresAt]);
@@ -390,15 +396,13 @@ async function __databaseHit(storageKey, windowStart, expiresAt, nowSec) {
390
396
  var handedToCleanup = false;
391
397
  try {
392
398
  await within(client.connect(), CONNECT_TIMEOUT_MS);
393
- if (!__tableReady) await createTable();
394
399
  var result;
395
400
  try {
396
401
  result = await hit();
397
402
  } catch (err) {
398
- // The table went away (dropped, or the database restored from an
399
- // older backup): create it again and count this request in it.
403
+ // The table is not there (dropped, or the database restored from an
404
+ // older backup): create it and count this request in it.
400
405
  if (!err || err.code !== '42P01') throw err;
401
- __tableReady = false;
402
406
  await createTable();
403
407
  result = await hit();
404
408
  }