@adonis-agora/authkit-server 0.82.0 → 0.82.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.
@@ -79,9 +79,17 @@ export function buildOrganizations(ctx) {
79
79
  },
80
80
  async listOrgsForAccount(accountId) {
81
81
  const memberships = await MemberModel.query().where('account_id', accountId);
82
+ if (memberships.length === 0)
83
+ return [];
84
+ // One query for every org (by primary key, like `find`), instead of one per
85
+ // membership. The result keeps the memberships' order.
86
+ const ids = [...new Set(memberships.map((m) => m.organizationId))];
87
+ const orgs = new Map();
88
+ for (const org of await OrgModel.findMany(ids))
89
+ orgs.set(String(org.id), org);
82
90
  const result = [];
83
91
  for (const m of memberships) {
84
- const org = await OrgModel.find(m.organizationId);
92
+ const org = orgs.get(String(m.organizationId));
85
93
  if (org)
86
94
  result.push({ ...toOrgSummary(org), role: m.role });
87
95
  }
@@ -119,7 +119,8 @@ export class DatabaseAdapter {
119
119
  const accountId = payload.accountId;
120
120
  row.account_id = typeof accountId === 'string' && accountId ? accountId : null;
121
121
  }
122
- const existing = await this.#query().where('id', id).first();
122
+ // Only whether the row exists matters: don't read the (possibly large) payload back.
123
+ const existing = await this.#query().where('id', id).select('id').first();
123
124
  if (existing) {
124
125
  await this.#query().where('id', id).update(row);
125
126
  }
@@ -101,15 +101,23 @@ export class RedisAdapter {
101
101
  return this.find(id);
102
102
  }
103
103
  async consume(id) {
104
- const found = await this.find(id);
105
- if (!found)
104
+ const key = this.#key(id);
105
+ // GET + TTL in one round trip (a pipeline, read-only: nothing changes before the SET).
106
+ const results = await this.redis.pipeline().get(key).ttl(key).exec();
107
+ const [getError, data] = results?.[0] ?? [null, null];
108
+ if (getError)
109
+ throw getError;
110
+ if (!data)
106
111
  return;
112
+ const [ttlError, ttl] = results?.[1] ?? [null, -1];
113
+ if (ttlError)
114
+ throw ttlError;
115
+ const found = JSON.parse(data);
107
116
  found.consumed = Math.floor(Date.now() / 1000);
108
- const ttl = await this.redis.ttl(this.#key(id));
109
117
  if (ttl > 0)
110
- await this.redis.set(this.#key(id), JSON.stringify(found), 'EX', ttl);
118
+ await this.redis.set(key, JSON.stringify(found), 'EX', ttl);
111
119
  else
112
- await this.redis.set(this.#key(id), JSON.stringify(found));
120
+ await this.redis.set(key, JSON.stringify(found));
113
121
  }
114
122
  async destroy(id) {
115
123
  await this.redis.del(this.#key(id));
@@ -1,6 +1,25 @@
1
1
  import { ACCOUNT_IDP_SESSION_KEY, readIdpSession } from './idp_session_bridge.js';
2
2
  import { impersonationState } from './impersonation_session.js';
3
3
  import { assertAccountEnabled } from './login_attempt.js';
4
+ /**
5
+ * How long (seconds) a renewal stays good enough: a linked RP whose IdP session was
6
+ * renewed less than this ago, with the current TTL, is not renewed again. Without
7
+ * it every authenticated request wrote the IdP session row and re-sent its cookie.
8
+ */
9
+ const RENEW_RESOLUTION_SEC = 60;
10
+ /**
11
+ * The linked RP's IdP session already expires between `ttl - RENEW_RESOLUTION_SEC`
12
+ * and `ttl` from now, i.e. it was renewed with this TTL moments ago. Renewing again
13
+ * would only move the expiry by those few seconds. A session that expires LATER
14
+ * than `now + ttl` (the TTL was lowered) is still renewed, which shortens it.
15
+ * Restoration (no local login yet) always renews.
16
+ */
17
+ function renewedRecently(idp, ttl, current, linkedUid) {
18
+ if (!current || linkedUid !== idp.uid || typeof idp.exp !== 'number')
19
+ return false;
20
+ const remaining = idp.exp - Date.now() / 1000;
21
+ return remaining <= ttl && remaining > ttl - RENEW_RESOLUTION_SEC;
22
+ }
4
23
  /** Same-host RP restoration: the signed IdP cookie is the persistent credential. */
5
24
  export async function ensureRpSession(ctx, sessionKey) {
6
25
  const service = await ctx.containerResolver.make('authkit.server');
@@ -39,7 +58,7 @@ export async function ensureRpSession(ctx, sessionKey) {
39
58
  }
40
59
  const adapter = service.provider.Session.adapter;
41
60
  const ttl = service.sessionTtlHolder.rememberSec;
42
- if (adapter.renewSession) {
61
+ if (adapter.renewSession && !renewedRecently(idp, ttl, current, linkedUid)) {
43
62
  if (!(await adapter.renewSession(idp.id, ttl))) {
44
63
  if (linkedUid)
45
64
  ctx.session.clear();