@lamalibre/create-lamaste 4.4.0 → 4.5.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/package.json +2 -2
- package/scripts/lamaste-priv +200 -16
- package/src/index.js +1 -0
- package/src/lib/backend-cookie.js +65 -0
- package/src/lib/ip-panel-vhost.js +208 -0
- package/src/lib/service-config.js +5 -3
- package/src/tasks/nginx.js +18 -135
- package/src/tasks/redeploy.js +39 -1
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/authentication.md +1 -1
- package/vendor/docs/01-concepts/authorization.md +66 -48
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +70 -33
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +5 -5
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
- package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/managing-users.md +113 -25
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +3 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +165 -18
- package/vendor/docs/03-architecture/overview.md +13 -17
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +3 -2
- package/vendor/docs/03-architecture/state-management.md +4 -8
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +292 -23
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/gatekeeper.md +60 -5
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +15 -13
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/registry-and-oidc.md +6 -4
- package/vendor/docs/04-api-reference/tunnels.md +67 -29
- package/vendor/docs/04-api-reference/users.md +241 -45
- package/vendor/docs/05-operations/backup-and-restore.md +35 -26
- package/vendor/docs/05-operations/monitoring.md +39 -0
- package/vendor/docs/05-operations/uninstalling.md +2 -1
- package/vendor/docs/05-operations/upgrades.md +15 -6
- package/vendor/docs/06-reference/config-files.md +83 -189
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +25 -26
- package/vendor/docs/06-reference/troubleshooting.md +8 -8
- package/vendor/docs/gatekeeper-design.md +10 -8
- package/vendor/docs/identity-directory-design.md +48 -14
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/index.d.ts +2 -1
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/index.js +2 -0
- package/vendor/gatekeeper/dist/index.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.d.ts +6 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.js +6 -1
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/groups.d.ts +5 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/groups.js +5 -1
- package/vendor/gatekeeper/dist/lib/groups.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/principals.d.ts +29 -0
- package/vendor/gatekeeper/dist/lib/principals.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/principals.js +67 -0
- package/vendor/gatekeeper/dist/lib/principals.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts +13 -0
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/tunnels.js +68 -0
- package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts +19 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +22 -48
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts +15 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +250 -103
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +61 -4
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/principals.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/principals.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/principals.js +27 -0
- package/vendor/gatekeeper/dist/server/routes/principals.js.map +1 -0
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/bin/lamaste-server.js +15 -1
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/plugin-identity.js +335 -0
- package/vendor/server/src/commands/plugins.js +99 -11
- package/vendor/server/src/commands/registry-credential.js +2 -1
- package/vendor/server/src/commands/tunnels.js +102 -2
- package/vendor/server/src/commands/uninstall.js +3 -1
- package/vendor/server/src/commands/users.js +358 -0
- package/vendor/server/src/plugin-approval.js +107 -5
- package/vendor/server/src/plugin-identity.js +382 -0
- package/vendor/server-ui/dist/assets/index-D-fer5ef.css +1 -0
- package/vendor/server-ui/dist/assets/index-DSaDPJaw.js +84 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +25 -1
- package/vendor/serverd/src/lib/audience.js +576 -0
- package/vendor/serverd/src/lib/authelia.js +78 -7
- package/vendor/serverd/src/lib/config.js +68 -43
- package/vendor/serverd/src/lib/directory.js +1113 -0
- package/vendor/serverd/src/lib/entitlements.js +517 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +215 -10
- package/vendor/serverd/src/lib/hook-pipelines.js +13 -0
- package/vendor/serverd/src/lib/identity-sync.js +905 -0
- package/vendor/serverd/src/lib/migrations/state/0013_identity_directory.sql +158 -0
- package/vendor/serverd/src/lib/migrations/state/0014_identity_ledger.sql +20 -0
- package/vendor/serverd/src/lib/mtls.js +6 -2
- package/vendor/serverd/src/lib/nginx.js +279 -48
- package/vendor/serverd/src/lib/plugin-hooks.js +98 -45
- package/vendor/serverd/src/lib/plugin-hosting.js +33 -5
- package/vendor/serverd/src/lib/plugin-lifecycle.js +100 -9
- package/vendor/serverd/src/lib/plugin-packages.js +30 -22
- package/vendor/serverd/src/lib/plugins.js +94 -15
- package/vendor/serverd/src/lib/registry-credential.js +46 -6
- package/vendor/serverd/src/lib/request-logging.js +19 -0
- package/vendor/serverd/src/lib/retained-data.js +50 -4
- package/vendor/serverd/src/lib/state.js +18 -0
- package/vendor/serverd/src/lib/tunnel-hooks.js +4 -1
- package/vendor/serverd/src/lib/user-access-session.js +21 -11
- package/vendor/serverd/src/lib/user-caller.js +67 -0
- package/vendor/serverd/src/lib/user-surface.js +733 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +136 -34
- package/vendor/serverd/src/middleware/user-access-session.js +9 -0
- package/vendor/serverd/src/routes/agent-forwarder.js +4 -2
- package/vendor/serverd/src/routes/invite.js +53 -12
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +107 -16
- package/vendor/serverd/src/routes/management/identity-directory.js +79 -0
- package/vendor/serverd/src/routes/management/invitations.js +78 -68
- package/vendor/serverd/src/routes/management/plugin-identity.js +221 -0
- package/vendor/serverd/src/routes/management/registry-credential.js +1 -1
- package/vendor/serverd/src/routes/management/tunnels.js +98 -2
- package/vendor/serverd/src/routes/management/users.js +290 -208
- package/vendor/serverd/src/routes/management.js +4 -0
- package/vendor/serverd/src/routes/onboarding/provision.js +14 -1
- package/vendor/serverd/src/routes/user-access.js +17 -4
- package/vendor/serverd/src/routes/user-surface.js +69 -0
- package/vendor/server-ui/dist/assets/index-25wo_8av.css +0 -1
- package/vendor/server-ui/dist/assets/index-CESyyK8V.js +0 -84
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from '@lamalibre/lamaste';
|
|
30
30
|
import { readTarballFiles } from '@lamalibre/lamaste/server';
|
|
31
31
|
import { httpError } from './app-error.js';
|
|
32
|
+
import { withRegistryUserconfig } from './registry-credential.js';
|
|
32
33
|
|
|
33
34
|
const STATE_DIR = process.env.LAMALIBRE_LAMASTE_STATE_DIR || '/etc/lamalibre/lamaste';
|
|
34
35
|
|
|
@@ -88,9 +89,11 @@ export async function installedHookEntrySha256(packageName, manifest) {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
/**
|
|
91
|
-
* Fetch `<package>@<version>` from the registry
|
|
92
|
-
*
|
|
93
|
-
*
|
|
92
|
+
* Fetch `<package>@<version>` from the registry the panel installs from
|
|
93
|
+
* (`npm pack` through {@link withRegistryUserconfig}: the configured
|
|
94
|
+
* registry credential, else the machine's npm configuration) and read its
|
|
95
|
+
* manifest and, when the manifest names one, its relay hook module — in
|
|
96
|
+
* memory only.
|
|
94
97
|
*
|
|
95
98
|
* @param {string} packageName bare `@lamalibre/<name>`
|
|
96
99
|
* @param {string} version an exact version or a dist-tag (`latest`)
|
|
@@ -103,22 +106,24 @@ export async function readRegistryPackage(packageName, version) {
|
|
|
103
106
|
try {
|
|
104
107
|
let packed;
|
|
105
108
|
try {
|
|
106
|
-
const { stdout } = await
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
const { stdout } = await withRegistryUserconfig((env) =>
|
|
110
|
+
execa(
|
|
111
|
+
'npm',
|
|
112
|
+
[
|
|
113
|
+
'pack',
|
|
114
|
+
`${packageName}@${version}`,
|
|
115
|
+
'--json',
|
|
116
|
+
'--ignore-scripts',
|
|
117
|
+
'--pack-destination',
|
|
118
|
+
dir,
|
|
119
|
+
],
|
|
120
|
+
{ cwd: STATE_DIR, timeout: 120000, env },
|
|
121
|
+
),
|
|
117
122
|
);
|
|
118
123
|
packed = JSON.parse(stdout);
|
|
119
124
|
} catch (err) {
|
|
120
125
|
throw httpError(
|
|
121
|
-
`Could not fetch ${packageName}@${version} from the registry: ${(err
|
|
126
|
+
`Could not fetch ${packageName}@${version} from the registry: ${npmFailureLine(err)}`,
|
|
122
127
|
502,
|
|
123
128
|
);
|
|
124
129
|
}
|
|
@@ -171,9 +176,9 @@ function npmFailureLine(err) {
|
|
|
171
176
|
|
|
172
177
|
/**
|
|
173
178
|
* The npm Subresource Integrity of `<package>@<version>`'s tarball, as the
|
|
174
|
-
* registry
|
|
175
|
-
*
|
|
176
|
-
* installs use. Only a sha512 SRI is accepted: it is what the approval
|
|
179
|
+
* registry the panel installs from publishes it — `npm view … dist.integrity`
|
|
180
|
+
* through {@link withRegistryUserconfig}, so the registry and credential
|
|
181
|
+
* are the ones plugin installs use. Only a sha512 SRI is accepted: it is what the approval
|
|
177
182
|
* binds and what the agent checks the tarball it fetches against.
|
|
178
183
|
*
|
|
179
184
|
* @param {string} packageName bare `@lamalibre/<name>`
|
|
@@ -186,10 +191,13 @@ export async function readRegistryIntegrity(packageName, version) {
|
|
|
186
191
|
const spec = `${packageName}@${version}`;
|
|
187
192
|
let stdout;
|
|
188
193
|
try {
|
|
189
|
-
({ stdout } = await
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
194
|
+
({ stdout } = await withRegistryUserconfig((env) =>
|
|
195
|
+
execa('npm', ['view', spec, 'dist.integrity', '--json'], {
|
|
196
|
+
cwd: STATE_DIR,
|
|
197
|
+
timeout: 60000,
|
|
198
|
+
env,
|
|
199
|
+
}),
|
|
200
|
+
));
|
|
193
201
|
} catch (err) {
|
|
194
202
|
if (/\bE404\b/.test(`${err.stderr ?? ''}\n${err.stdout ?? ''}`)) {
|
|
195
203
|
throw httpError(
|
|
@@ -19,12 +19,15 @@ import {
|
|
|
19
19
|
RESERVED_NAV_LABELS,
|
|
20
20
|
derivePluginRoute,
|
|
21
21
|
hasAgentHalf,
|
|
22
|
+
normalizeIdentityBlock,
|
|
22
23
|
pluginHostCapability,
|
|
23
24
|
validateManifest as coreValidateManifest,
|
|
24
25
|
} from '@lamalibre/lamaste';
|
|
25
26
|
import { listRegisteredSubScopeNames } from './tickets.js';
|
|
26
27
|
import { getStateDb } from './state-db.js';
|
|
27
28
|
import { revokePluginCapabilitiesFromAgents } from './mtls.js';
|
|
29
|
+
import { withRegistryUserconfig } from './registry-credential.js';
|
|
30
|
+
import { RegistryCredentialError } from '@lamalibre/lamaste/server';
|
|
28
31
|
import {
|
|
29
32
|
bareLamalibrePackageName,
|
|
30
33
|
installedHookEntrySha256,
|
|
@@ -69,10 +72,12 @@ async function getStmts() {
|
|
|
69
72
|
INSERT INTO plugins
|
|
70
73
|
(name, route, display_name, package_name, version, description, capabilities,
|
|
71
74
|
packages, panel, config, modes, hosting, agent, hooks, hooks_entry_sha256,
|
|
72
|
-
agent_half_pin, settings, status, installed_at, enabled_at)
|
|
73
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
75
|
+
agent_half_pin, settings, identity, status, installed_at, enabled_at)
|
|
76
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
74
77
|
`),
|
|
75
78
|
deleteByName: db.prepare('DELETE FROM plugins WHERE name = ?'),
|
|
79
|
+
// The plugin's audience and the state of its identity feed
|
|
80
|
+
// (lib/audience.js, lib/identity-sync.js) go with its row.
|
|
76
81
|
// The relay's cache of the plugin's settings (lib/plugin-settings.js)
|
|
77
82
|
// goes with its row, and follows its settings schema.
|
|
78
83
|
deleteSettingsByName: db.prepare('DELETE FROM plugin_settings WHERE plugin = ?'),
|
|
@@ -135,6 +140,7 @@ function rowToPlugin(row) {
|
|
|
135
140
|
if (row.hooks_entry_sha256 != null) entry.hooksEntrySha256 = row.hooks_entry_sha256;
|
|
136
141
|
if (row.agent_half_pin != null) entry.agentHalfPin = JSON.parse(row.agent_half_pin);
|
|
137
142
|
if (row.settings != null) entry.settings = JSON.parse(row.settings);
|
|
143
|
+
if (row.identity != null) entry.identity = JSON.parse(row.identity);
|
|
138
144
|
if (row.enabled_at != null) entry.enabledAt = row.enabled_at;
|
|
139
145
|
return entry;
|
|
140
146
|
}
|
|
@@ -147,6 +153,29 @@ function nullableJson(value) {
|
|
|
147
153
|
return value === undefined || value === null ? null : JSON.stringify(value);
|
|
148
154
|
}
|
|
149
155
|
|
|
156
|
+
/**
|
|
157
|
+
* A manifest's `identity` block as the registry stores it — normalized
|
|
158
|
+
* (core `normalizeIdentityBlock`: roles by id, attributes sorted, the
|
|
159
|
+
* default applied) — or undefined when it declares none. The stored block
|
|
160
|
+
* enters the approval digest.
|
|
161
|
+
*
|
|
162
|
+
* @param {{ identity?: import('@lamalibre/lamaste').IdentityBlockInput }} manifest
|
|
163
|
+
* @returns {import('@lamalibre/lamaste').NormalizedIdentityBlock | undefined}
|
|
164
|
+
*/
|
|
165
|
+
export function identityOf(manifest) {
|
|
166
|
+
return manifest.identity ? normalizeIdentityBlock(manifest.identity) : undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Recompute the plugin audiences (lib/audience.js) beside other work — never throws. */
|
|
170
|
+
async function recomputeAudiencesInBackground(logger) {
|
|
171
|
+
try {
|
|
172
|
+
const { recomputeAudiences } = await import('./audience.js');
|
|
173
|
+
void recomputeAudiences({ logger });
|
|
174
|
+
} catch (err) {
|
|
175
|
+
logger?.warn({ err: err.message }, 'Could not recompute the plugin audiences');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
150
179
|
// --- Plugin registry persistence ---
|
|
151
180
|
|
|
152
181
|
/**
|
|
@@ -194,6 +223,7 @@ export async function writePlugins(data) {
|
|
|
194
223
|
nullableString(p.hooksEntrySha256),
|
|
195
224
|
nullableJson(p.agentHalfPin),
|
|
196
225
|
nullableJson(p.settings),
|
|
226
|
+
nullableJson(p.identity),
|
|
197
227
|
p.status,
|
|
198
228
|
p.installedAt,
|
|
199
229
|
nullableString(p.enabledAt),
|
|
@@ -326,6 +356,17 @@ export async function manifestPolicyError(manifest) {
|
|
|
326
356
|
return null;
|
|
327
357
|
}
|
|
328
358
|
|
|
359
|
+
/**
|
|
360
|
+
* What a failed install of `spec` is reported as. A registry that refused
|
|
361
|
+
* the panel's credential says so (the registry host and the status, from
|
|
362
|
+
* {@link withRegistryUserconfig}); anything else is npm's failure, whose
|
|
363
|
+
* output is in the log.
|
|
364
|
+
*/
|
|
365
|
+
function installFailure(spec, err) {
|
|
366
|
+
const detail = err instanceof RegistryCredentialError ? `: ${err.message}` : '';
|
|
367
|
+
return `Failed to install "${spec}"${detail}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
329
370
|
export function installPlugin(packageName, logger) {
|
|
330
371
|
return withPluginLock(async () => {
|
|
331
372
|
// Validate package scope
|
|
@@ -358,19 +399,25 @@ export function installPlugin(packageName, logger) {
|
|
|
358
399
|
}
|
|
359
400
|
}
|
|
360
401
|
|
|
361
|
-
// Install the npm package
|
|
402
|
+
// Install the npm package — from the configured registry, as the
|
|
403
|
+
// panel's own identity, when a registry credential is set.
|
|
362
404
|
logger.info({ packageName: installSpec }, 'Installing plugin package');
|
|
363
405
|
try {
|
|
364
|
-
await
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
406
|
+
await withRegistryUserconfig(
|
|
407
|
+
(env) =>
|
|
408
|
+
execa('npm', ['install', '--ignore-scripts', installSpec], {
|
|
409
|
+
cwd: STATE_DIR,
|
|
410
|
+
timeout: 120000,
|
|
411
|
+
env,
|
|
412
|
+
}),
|
|
413
|
+
{ log: logger },
|
|
414
|
+
);
|
|
368
415
|
} catch (err) {
|
|
369
416
|
logger.error(
|
|
370
417
|
{ packageName: installSpec, stderr: err.stderr, err: err.message },
|
|
371
418
|
'npm install failed',
|
|
372
419
|
);
|
|
373
|
-
throw Object.assign(new Error(
|
|
420
|
+
throw Object.assign(new Error(installFailure(installSpec, err)), { statusCode: 500 });
|
|
374
421
|
}
|
|
375
422
|
|
|
376
423
|
// Read the plugin manifest from the installed package
|
|
@@ -460,6 +507,7 @@ export function installPlugin(packageName, logger) {
|
|
|
460
507
|
hooksEntrySha256,
|
|
461
508
|
agentHalfPin,
|
|
462
509
|
settings: manifest.settings,
|
|
510
|
+
identity: identityOf(manifest),
|
|
463
511
|
status: 'disabled',
|
|
464
512
|
installedAt: new Date().toISOString(),
|
|
465
513
|
};
|
|
@@ -487,6 +535,7 @@ export function installPlugin(packageName, logger) {
|
|
|
487
535
|
nullableString(entry.hooksEntrySha256),
|
|
488
536
|
nullableJson(entry.agentHalfPin),
|
|
489
537
|
nullableJson(entry.settings),
|
|
538
|
+
nullableJson(entry.identity),
|
|
490
539
|
entry.status,
|
|
491
540
|
entry.installedAt,
|
|
492
541
|
null,
|
|
@@ -499,6 +548,13 @@ export function installPlugin(packageName, logger) {
|
|
|
499
548
|
|
|
500
549
|
logger.info({ name: manifest.name, packageName }, 'Plugin installed');
|
|
501
550
|
const oidcClients = await syncOidcClientsBestEffort(logger);
|
|
551
|
+
// Users already entitled to its route (grants that outlived an earlier
|
|
552
|
+
// installation) make up its audience from the start.
|
|
553
|
+
if (entry.identity) await recomputeAudiencesInBackground(logger);
|
|
554
|
+
// A new installation is served on no host (its version is not approved
|
|
555
|
+
// yet): a vhost still carrying the route from before goes in line.
|
|
556
|
+
const { refreshUserSurfaceVhosts } = await import('./user-surface.js');
|
|
557
|
+
await refreshUserSurfaceVhosts(entry.route, logger);
|
|
502
558
|
return { ...entry, oidcClients };
|
|
503
559
|
});
|
|
504
560
|
}
|
|
@@ -572,6 +628,17 @@ export function uninstallPlugin(name, logger) {
|
|
|
572
628
|
throw err;
|
|
573
629
|
}
|
|
574
630
|
|
|
631
|
+
// Its identity ledger — everyone its agent halves were sent, and the
|
|
632
|
+
// erasures it owes — stays while an agent keeps its data (an uninstall
|
|
633
|
+
// without purge); with no data left anywhere it goes. A later install
|
|
634
|
+
// replays whatever is left of it at its first hosting.
|
|
635
|
+
try {
|
|
636
|
+
const { dropIdentityLedgerIfNoData } = await import('./audience.js');
|
|
637
|
+
await dropIdentityLedgerIfNoData(name, logger);
|
|
638
|
+
} catch (err) {
|
|
639
|
+
logger.warn({ name, err: err.message }, 'Could not settle the identity ledger on uninstall');
|
|
640
|
+
}
|
|
641
|
+
|
|
575
642
|
// Sweep this plugin's capabilities off every agent so re-installing the
|
|
576
643
|
// plugin later does NOT silently re-grant. Failure here is non-fatal
|
|
577
644
|
// (the plugin is already gone), but logged loudly. A plugin without a
|
|
@@ -600,6 +667,11 @@ export function uninstallPlugin(name, logger) {
|
|
|
600
667
|
);
|
|
601
668
|
}
|
|
602
669
|
|
|
670
|
+
// Its user surface is unbound from every host, so a later install
|
|
671
|
+
// starts unbound (a plugin sharing another's route never held one).
|
|
672
|
+
const { releasePluginUserSurface } = await import('./user-surface.js');
|
|
673
|
+
await releasePluginUserSurface(plugin.route, { plugin: name, logger });
|
|
674
|
+
|
|
603
675
|
logger.info({ name }, 'Plugin uninstalled');
|
|
604
676
|
await syncOidcClientsBestEffort(logger);
|
|
605
677
|
return { ok: true, name };
|
|
@@ -612,7 +684,7 @@ const updateRowSql = `
|
|
|
612
684
|
UPDATE plugins SET
|
|
613
685
|
display_name = ?, version = ?, description = ?, capabilities = ?, packages = ?, panel = ?,
|
|
614
686
|
config = ?, modes = ?, hosting = ?, agent = ?, hooks = ?, hooks_entry_sha256 = ?,
|
|
615
|
-
agent_half_pin = ?, settings = ?
|
|
687
|
+
agent_half_pin = ?, settings = ?, identity = ?
|
|
616
688
|
WHERE name = ?`;
|
|
617
689
|
|
|
618
690
|
/**
|
|
@@ -624,7 +696,8 @@ const updateRowSql = `
|
|
|
624
696
|
* (`prunedSettingsCache`) — so no read pairs the new schema with values
|
|
625
697
|
* stored for the old one: a value whose field became a secret never shows
|
|
626
698
|
* beside it, and a secret whose field became a plain value is no longer
|
|
627
|
-
* reported set.
|
|
699
|
+
* reported set. The identity block is stored normalized; the update's
|
|
700
|
+
* caller recomputes the audiences once the version is in place.
|
|
628
701
|
*/
|
|
629
702
|
export async function writePluginRow(name, manifest, hooksEntrySha256, agentHalfPin) {
|
|
630
703
|
const s = await getStmts();
|
|
@@ -649,6 +722,7 @@ export async function writePluginRow(name, manifest, hooksEntrySha256, agentHalf
|
|
|
649
722
|
nullableString(hooksEntrySha256),
|
|
650
723
|
nullableJson(agentHalfPin),
|
|
651
724
|
nullableJson(manifest.settings),
|
|
725
|
+
nullableJson(identityOf(manifest)),
|
|
652
726
|
name,
|
|
653
727
|
);
|
|
654
728
|
const pruned = prunedSettingsCache(
|
|
@@ -688,13 +762,18 @@ export function replacePluginPackage(name, packageName, version, logger) {
|
|
|
688
762
|
assertSafeLamalibrePackage(spec);
|
|
689
763
|
logger?.info({ name, spec }, 'Installing plugin package version');
|
|
690
764
|
try {
|
|
691
|
-
await
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
765
|
+
await withRegistryUserconfig(
|
|
766
|
+
(env) =>
|
|
767
|
+
execa('npm', ['install', '--ignore-scripts', spec], {
|
|
768
|
+
cwd: STATE_DIR,
|
|
769
|
+
timeout: 120000,
|
|
770
|
+
env,
|
|
771
|
+
}),
|
|
772
|
+
{ log: logger },
|
|
773
|
+
);
|
|
695
774
|
} catch (err) {
|
|
696
775
|
logger?.error({ spec, stderr: err.stderr, err: err.message }, 'npm install failed');
|
|
697
|
-
throw Object.assign(new Error(
|
|
776
|
+
throw Object.assign(new Error(installFailure(spec, err)), { statusCode: 502 });
|
|
698
777
|
}
|
|
699
778
|
const require = createRequire(`${STATE_DIR}/`);
|
|
700
779
|
const manifestPath = require.resolve(`${bare}/lamaste-plugin.json`);
|
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
* install within the hour and limited to ten new tokens per agent per
|
|
13
13
|
* hour.
|
|
14
14
|
*
|
|
15
|
+
* The panel's own npm calls — installing a plugin, staging an update with
|
|
16
|
+
* `npm pack`, pinning an agent half with `npm view` — go through
|
|
17
|
+
* {@link withRegistryUserconfig}: with a credential configured, npm is
|
|
18
|
+
* routed to its registry for its scope and authenticated with the service
|
|
19
|
+
* token through a throw-away userconfig; without one, npm runs as it is
|
|
20
|
+
* configured on the machine (the public registry by default).
|
|
21
|
+
*
|
|
15
22
|
* Also the one piece of policy that belongs here: when a user loses a
|
|
16
23
|
* `feria-*` group or is deleted, their registry tokens are revoked (best
|
|
17
24
|
* effort, never blocks the user operation).
|
|
@@ -26,6 +33,7 @@ import {
|
|
|
26
33
|
revokeRegistryTokensForOwner,
|
|
27
34
|
lostFeriaGroups,
|
|
28
35
|
readOidcClientSecret,
|
|
36
|
+
withRegistryUserconfig as withRegistryUserconfigCore,
|
|
29
37
|
RegistryServiceTokenCache,
|
|
30
38
|
AgentTokenBroker,
|
|
31
39
|
RegistryCredentialError,
|
|
@@ -88,7 +96,7 @@ async function oidcDeps(config) {
|
|
|
88
96
|
|
|
89
97
|
/**
|
|
90
98
|
* Verify a candidate credential end to end (for `oidc`: Authelia token
|
|
91
|
-
* endpoint → Feria login → whoami with the admin
|
|
99
|
+
* endpoint → Feria login → whoami with the read and admin scopes). The
|
|
92
100
|
* `lamaste-server` client must already be in Authelia — the route syncs it
|
|
93
101
|
* first.
|
|
94
102
|
* @param {{ registryUrl: string, auth: { kind: 'token', token: string } | { kind: 'oidc', clientId: string } }} candidate
|
|
@@ -102,6 +110,35 @@ async function serviceToken(config) {
|
|
|
102
110
|
return (await serviceTokens.resolve(config, await oidcDeps(config))).token;
|
|
103
111
|
}
|
|
104
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Run `fn(env)` — one npm call of the panel's own — against the configured
|
|
115
|
+
* registry: with a credential, `env` names an isolated userconfig (0600,
|
|
116
|
+
* removed afterwards) routing the credential's scope to its registry with
|
|
117
|
+
* the panel's service token; a `401` refreshes the token once and retries.
|
|
118
|
+
* Without a credential `fn({})` runs npm as the machine configures it.
|
|
119
|
+
* A credential file that cannot be read fails the call rather than
|
|
120
|
+
* silently installing from the public registry.
|
|
121
|
+
*
|
|
122
|
+
* @template T
|
|
123
|
+
* @param {(env: Record<string, string>) => Promise<T>} fn
|
|
124
|
+
* @param {{ log?: import('pino').Logger | undefined }} [options]
|
|
125
|
+
* @returns {Promise<T>}
|
|
126
|
+
*/
|
|
127
|
+
export async function withRegistryUserconfig(fn, options = {}) {
|
|
128
|
+
const config = await readRegistryCredentialConfig();
|
|
129
|
+
if (!config) return fn({});
|
|
130
|
+
const deps = await oidcDeps(config);
|
|
131
|
+
return withRegistryUserconfigCore(
|
|
132
|
+
config,
|
|
133
|
+
async ({ refresh }) => {
|
|
134
|
+
if (refresh) serviceTokens.invalidate();
|
|
135
|
+
return (await serviceTokens.resolve(config, deps)).token;
|
|
136
|
+
},
|
|
137
|
+
fn,
|
|
138
|
+
{ log: options.log },
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
105
142
|
/**
|
|
106
143
|
* The calling agent's registry token: the cached one when it is still good
|
|
107
144
|
* for an install, otherwise a freshly brokered one-hour token — unless the
|
|
@@ -121,14 +158,17 @@ export async function acquireAgentToken(agentLabel) {
|
|
|
121
158
|
|
|
122
159
|
/**
|
|
123
160
|
* Revoke a user's registry tokens after their `feria-*` groups changed or
|
|
124
|
-
* they were deleted
|
|
125
|
-
*
|
|
161
|
+
* they were offboarded (`{ deleted: true }`: disabled or removed). Returns
|
|
162
|
+
* the number revoked, or `null` when nothing needed doing (no credential
|
|
163
|
+
* configured, or no feria group lost). The registry knows the user by
|
|
164
|
+
* username (`user:<username>`); the log line names them by `userId` alone.
|
|
126
165
|
*
|
|
127
166
|
* @param {string} username
|
|
128
167
|
* @param {{ before: string[], after: string[] } | { deleted: true }} change
|
|
129
|
-
* @param {import('pino').Logger} log
|
|
168
|
+
* @param {import('pino').Logger | undefined} log
|
|
169
|
+
* @param {{ userId?: string | null }} [who]
|
|
130
170
|
*/
|
|
131
|
-
export async function revokeUserRegistryTokens(username, change, log) {
|
|
171
|
+
export async function revokeUserRegistryTokens(username, change, log, { userId = null } = {}) {
|
|
132
172
|
const config = await readRegistryCredentialConfig();
|
|
133
173
|
if (!config) return null;
|
|
134
174
|
|
|
@@ -140,6 +180,6 @@ export async function revokeUserRegistryTokens(username, change, log) {
|
|
|
140
180
|
const revoked = await revokeRegistryTokensForOwner(config, owner, {
|
|
141
181
|
serviceToken: await serviceToken(config),
|
|
142
182
|
});
|
|
143
|
-
log
|
|
183
|
+
log?.info({ userId, lost, revoked }, 'Revoked a user’s registry tokens');
|
|
144
184
|
return revoked;
|
|
145
185
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the panel logs its requests: one metadata line per request (core
|
|
3
|
+
* `logRequestMetadata`) instead of Fastify's own lines, which carry every
|
|
4
|
+
* URL with its query and the client address. The line has the method, the
|
|
5
|
+
* route pattern (never the URL a username or a query may be in), the
|
|
6
|
+
* status, the duration and the caller's certificate role; the client
|
|
7
|
+
* address only for certificate holders — administrators and agents, not
|
|
8
|
+
* people: never for a user of a plugin's user surface, the desktop's
|
|
9
|
+
* user-access calls or the public routes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Callers whose address a request line may carry: certificate holders, not people. */
|
|
13
|
+
const CERT_ROLES_LOGGED_WITH_IP = new Set(['admin', 'agent', 'plugin-agent']);
|
|
14
|
+
|
|
15
|
+
/** The options the panel passes to `logRequestMetadata`. */
|
|
16
|
+
export const panelRequestLogOptions = Object.freeze({
|
|
17
|
+
fields: (request) => ({ certRole: request.certRole ?? null }),
|
|
18
|
+
logIp: (request) => CERT_ROLES_LOGGED_WITH_IP.has(request.certRole),
|
|
19
|
+
});
|
|
@@ -38,18 +38,64 @@ function rowToRecord(row) {
|
|
|
38
38
|
* agent package `pkg` of plugin `plugin`. A record that exists already keeps
|
|
39
39
|
* its `since` — the data has been there since then.
|
|
40
40
|
*
|
|
41
|
+
* With it goes how far the agent half's identity projection — part of that
|
|
42
|
+
* data — had applied the plugin's feed, as the half last answered (the
|
|
43
|
+
* plugin's feed state, read now, before the hosting's is forgotten): the
|
|
44
|
+
* erasures of the plugin's ledger after that cursor are pending on this
|
|
45
|
+
* agent (`pendingErasures` in `lib/identity-sync.js`). When the relay knows
|
|
46
|
+
* no position, a record that exists keeps the one it had — the projection
|
|
47
|
+
* did not move without a feed.
|
|
48
|
+
*
|
|
41
49
|
* @param {{ agentLabel: string, route: string, plugin: string, package: string, operation?: string | null }} record
|
|
42
50
|
*/
|
|
43
51
|
export async function recordRetainedData({ agentLabel, route, plugin, package: pkg, operation }) {
|
|
44
52
|
const db = await getStateDb();
|
|
53
|
+
const feed = db
|
|
54
|
+
.prepare('SELECT directory_id, cursor FROM plugin_identity_sync WHERE plugin = ?')
|
|
55
|
+
.get(plugin);
|
|
56
|
+
const known = feed && feed.directory_id !== null && feed.cursor !== null;
|
|
45
57
|
db.prepare(
|
|
46
|
-
`INSERT INTO retained_agent_data
|
|
47
|
-
|
|
58
|
+
`INSERT INTO retained_agent_data
|
|
59
|
+
(agent_label, route, plugin, package, since, operation, identity_directory_id, identity_cursor)
|
|
60
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
48
61
|
ON CONFLICT(agent_label, route) DO UPDATE SET
|
|
49
62
|
plugin = excluded.plugin,
|
|
50
63
|
package = excluded.package,
|
|
51
|
-
operation = excluded.operation
|
|
52
|
-
|
|
64
|
+
operation = excluded.operation,
|
|
65
|
+
identity_directory_id = CASE WHEN excluded.identity_cursor IS NULL
|
|
66
|
+
THEN identity_directory_id ELSE excluded.identity_directory_id END,
|
|
67
|
+
identity_cursor = COALESCE(excluded.identity_cursor, identity_cursor)`,
|
|
68
|
+
).run(
|
|
69
|
+
agentLabel,
|
|
70
|
+
route,
|
|
71
|
+
plugin,
|
|
72
|
+
pkg,
|
|
73
|
+
new Date().toISOString(),
|
|
74
|
+
operation ?? null,
|
|
75
|
+
known ? feed.directory_id : null,
|
|
76
|
+
known ? Number(feed.cursor) : null,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Where the identity projection in each record's data stands, for plugin
|
|
82
|
+
* `plugin`: `[{ agentLabel, directoryId, cursor }]` (both null when unknown).
|
|
83
|
+
*
|
|
84
|
+
* @param {string} plugin
|
|
85
|
+
*/
|
|
86
|
+
export async function retainedIdentityPositions(plugin) {
|
|
87
|
+
const db = await getStateDb();
|
|
88
|
+
return db
|
|
89
|
+
.prepare(
|
|
90
|
+
`SELECT agent_label, identity_directory_id, identity_cursor FROM retained_agent_data
|
|
91
|
+
WHERE plugin = ? ORDER BY since, agent_label`,
|
|
92
|
+
)
|
|
93
|
+
.all(plugin)
|
|
94
|
+
.map((row) => ({
|
|
95
|
+
agentLabel: row.agent_label,
|
|
96
|
+
directoryId: row.identity_directory_id,
|
|
97
|
+
cursor: row.identity_cursor === null ? null : Number(row.identity_cursor),
|
|
98
|
+
}));
|
|
53
99
|
}
|
|
54
100
|
|
|
55
101
|
/** Drop the record of the data agent `agentLabel` keeps for route `route` (purged, or taken back). */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, writeFile, rename, open } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { PromiseChainMutex } from '@lamalibre/lamaste';
|
|
3
4
|
|
|
4
5
|
const STATE_DIR = process.env.LAMALIBRE_LAMASTE_STATE_DIR || '/etc/lamalibre/lamaste';
|
|
5
6
|
|
|
@@ -138,3 +139,20 @@ export async function writeInvitations(invitations) {
|
|
|
138
139
|
|
|
139
140
|
await rename(tmpPath, filePath);
|
|
140
141
|
}
|
|
142
|
+
|
|
143
|
+
/** Serializes every read-modify-write of invitations.json in this process. */
|
|
144
|
+
const invitationsMutex = new PromiseChainMutex();
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Run `fn` holding the invitations lock: a read of invitations.json, the
|
|
148
|
+
* decisions made on it and the write that follows must not interleave with
|
|
149
|
+
* another — creating, revoking, accepting an invitation, and purging a
|
|
150
|
+
* removed user's.
|
|
151
|
+
*
|
|
152
|
+
* @template T
|
|
153
|
+
* @param {() => Promise<T>} fn
|
|
154
|
+
* @returns {Promise<T>}
|
|
155
|
+
*/
|
|
156
|
+
export function withInvitationsLock(fn) {
|
|
157
|
+
return invitationsMutex.run(fn);
|
|
158
|
+
}
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* Whenever the plugin comes up — after a `down`, or first seen up — the
|
|
13
13
|
* relay also re-reads its settings from the agent half: they may have
|
|
14
14
|
* changed while the relay could not see it (a restore, a retyped field
|
|
15
|
-
* dropped by a new version)
|
|
15
|
+
* dropped by a new version) — and, for a plugin that declares identity,
|
|
16
|
+
* sends its agent half the audience changes it missed while it was down.
|
|
16
17
|
*
|
|
17
18
|
* Calls for one plugin are serialised and run beside the monitor, which
|
|
18
19
|
* does not wait for them.
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
import { runRelayHook } from './relay-hooks.js';
|
|
28
29
|
import { rememberTunnelFallback } from './request-hooks.js';
|
|
29
30
|
import { refreshPluginSettingsBestEffort } from './plugin-settings.js';
|
|
31
|
+
import { syncPluginIdentity } from './identity-sync.js';
|
|
30
32
|
|
|
31
33
|
const chains = new Map();
|
|
32
34
|
|
|
@@ -44,6 +46,7 @@ export function onHealthTransition(plugin, before, after, logger) {
|
|
|
44
46
|
.then(async () => {
|
|
45
47
|
if (after.state === 'up' && before?.state !== 'up') {
|
|
46
48
|
await refreshPluginSettingsBestEffort(plugin, logger);
|
|
49
|
+
await syncPluginIdentity(plugin, { logger });
|
|
47
50
|
}
|
|
48
51
|
});
|
|
49
52
|
chains.set(plugin, next);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
+
import { isIdentityUuid } from '@lamalibre/lamaste';
|
|
2
3
|
|
|
3
4
|
const SESSION_MAX_AGE = 12 * 60 * 60; // 12 hours in seconds
|
|
4
5
|
const INACTIVITY_TIMEOUT = 2 * 60 * 60; // 2 hours in seconds
|
|
@@ -21,19 +22,23 @@ function signPayload(payloadB64, secretHex) {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
|
-
* Create a signed user-access session token
|
|
25
|
+
* Create a signed user-access session token, bound to the user's directory
|
|
26
|
+
* id as well as their username: a later user given the same username never
|
|
27
|
+
* holds this session (the middleware checks the username's current holder).
|
|
25
28
|
*
|
|
26
29
|
* @param {string} sessionSecret - Hex-encoded session secret
|
|
27
|
-
* @param {string}
|
|
30
|
+
* @param {{ username: string, userId: string }} user - the Authelia username and its directory id
|
|
28
31
|
* @returns {{ value: string, maxAge: number, expiresAt: string }}
|
|
29
32
|
*/
|
|
30
|
-
export function createUserSession(sessionSecret, username) {
|
|
33
|
+
export function createUserSession(sessionSecret, { username, userId }) {
|
|
34
|
+
if (!isIdentityUuid(userId)) throw new Error('A user session names a directory user id');
|
|
31
35
|
const now = Math.floor(Date.now() / 1000);
|
|
32
36
|
const payload = {
|
|
33
37
|
iat: now,
|
|
34
38
|
exp: now + SESSION_MAX_AGE,
|
|
35
39
|
lastActivity: now,
|
|
36
40
|
username,
|
|
41
|
+
userId,
|
|
37
42
|
type: 'user-access',
|
|
38
43
|
};
|
|
39
44
|
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
@@ -52,10 +57,12 @@ export function createUserSession(sessionSecret, username) {
|
|
|
52
57
|
* @param {string} tokenValue - The token (payload.sig)
|
|
53
58
|
* @param {string} sessionSecret - Hex-encoded session secret
|
|
54
59
|
* @param {{ userEpochs?: Record<string, number> }} [options]
|
|
55
|
-
* userEpochs maps
|
|
56
|
-
* is older than
|
|
57
|
-
* This is how
|
|
58
|
-
* HMAC signature is otherwise still valid.
|
|
60
|
+
* userEpochs maps a directory user id → Unix-second epoch. If the
|
|
61
|
+
* session's `iat` is older than its user's epoch, the session is rejected
|
|
62
|
+
* as revoked. This is how offboarding (disable, delete) invalidates live
|
|
63
|
+
* sessions whose HMAC signature is otherwise still valid. A session
|
|
64
|
+
* without a user id (issued before 4.5) is rejected: it could not be
|
|
65
|
+
* told from a later user's.
|
|
59
66
|
* @returns {{ valid: boolean, payload?: object, reason?: string }}
|
|
60
67
|
*/
|
|
61
68
|
export function validateUserSession(tokenValue, sessionSecret, options = {}) {
|
|
@@ -96,6 +103,10 @@ export function validateUserSession(tokenValue, sessionSecret, options = {}) {
|
|
|
96
103
|
return { valid: false, reason: 'missing_username' };
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
if (!isIdentityUuid(payload.userId)) {
|
|
107
|
+
return { valid: false, reason: 'missing_user_id' };
|
|
108
|
+
}
|
|
109
|
+
|
|
99
110
|
const now = Math.floor(Date.now() / 1000);
|
|
100
111
|
|
|
101
112
|
if (payload.exp && now > payload.exp) {
|
|
@@ -106,12 +117,11 @@ export function validateUserSession(tokenValue, sessionSecret, options = {}) {
|
|
|
106
117
|
return { valid: false, reason: 'inactive' };
|
|
107
118
|
}
|
|
108
119
|
|
|
109
|
-
// Per-user invalidation epoch (set when a user is
|
|
110
|
-
//
|
|
111
|
-
// longer exists, even if a same-named user is later re-created.
|
|
120
|
+
// Per-user invalidation epoch (set when a user is offboarded). Reject
|
|
121
|
+
// sessions issued before it.
|
|
112
122
|
const userEpochs = options.userEpochs;
|
|
113
123
|
if (userEpochs && typeof userEpochs === 'object') {
|
|
114
|
-
const epoch = userEpochs[payload.
|
|
124
|
+
const epoch = userEpochs[payload.userId];
|
|
115
125
|
if (typeof epoch === 'number' && typeof payload.iat === 'number' && payload.iat < epoch) {
|
|
116
126
|
return { valid: false, reason: 'revoked' };
|
|
117
127
|
}
|