@nacre.work/api 0.23.9 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +56 -5
- package/dist/adapters.js.map +1 -1
- package/dist/login.d.ts +9 -2
- package/dist/login.d.ts.map +1 -1
- package/dist/login.js +5 -1
- package/dist/login.js.map +1 -1
- package/dist/pagination.d.ts +16 -1
- package/dist/pagination.d.ts.map +1 -1
- package/dist/pagination.js +15 -18
- package/dist/pagination.js.map +1 -1
- package/dist/second-factor.d.ts +16 -2
- package/dist/second-factor.d.ts.map +1 -1
- package/dist/second-factor.js +30 -5
- package/dist/second-factor.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +114 -42
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -13,7 +13,7 @@ import { MIN_PASSWORD_LENGTH } from './recovery.js';
|
|
|
13
13
|
import { looksLikeEmail } from './principals.js';
|
|
14
14
|
import { clientSource } from './source.js';
|
|
15
15
|
import { auditFormat, auditJson, readAuditQuery, toCsv, toNdjson, } from './audit-export.js';
|
|
16
|
-
import { readPage } from './pagination.js';
|
|
16
|
+
import { decodeCursor, readPage } from './pagination.js';
|
|
17
17
|
/**
|
|
18
18
|
* The default body cap, in bytes.
|
|
19
19
|
*
|
|
@@ -868,6 +868,37 @@ function sendSessionOutcome(res, outcome, instance, requestId) {
|
|
|
868
868
|
});
|
|
869
869
|
send(res, problem.status, problem.toJSON(), requestId);
|
|
870
870
|
}
|
|
871
|
+
/**
|
|
872
|
+
* Match a route whose captured segments are percent-decoded, or `null`.
|
|
873
|
+
*
|
|
874
|
+
* WHATWG `URL` leaves an invalid escape in `pathname` verbatim, so a bare
|
|
875
|
+
* `decodeURIComponent` over a captured segment throws `URIError` on
|
|
876
|
+
* `/v1/documents/%ZZ` — which the error boundary turned into a `500` and an
|
|
877
|
+
* audit row saying `error`, where the right answer is the `404` every other
|
|
878
|
+
* unknown id gets. A malformed escape names nothing, so here it is a path
|
|
879
|
+
* that does not match: the request falls through to the same `notFound` an
|
|
880
|
+
* unrouted path gets, and any authenticated caller stops being able to fill
|
|
881
|
+
* the journal with `error` rows on demand. Twelve routes decode a segment;
|
|
882
|
+
* this is the one place they all do it, and eslint refuses a bare
|
|
883
|
+
* `decodeURIComponent` in this package so the thirteenth cannot be written
|
|
884
|
+
* the old way.
|
|
885
|
+
*/
|
|
886
|
+
function pathMatch(pattern, instance) {
|
|
887
|
+
const matched = pattern.exec(instance);
|
|
888
|
+
if (matched === null)
|
|
889
|
+
return null;
|
|
890
|
+
try {
|
|
891
|
+
for (let i = 1; i < matched.length; i += 1) {
|
|
892
|
+
const raw = matched[i];
|
|
893
|
+
if (raw !== undefined)
|
|
894
|
+
matched[i] = decodeURIComponent(raw);
|
|
895
|
+
}
|
|
896
|
+
return matched;
|
|
897
|
+
}
|
|
898
|
+
catch {
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
871
902
|
async function handleAuth(req, res, instance, requestId, options) {
|
|
872
903
|
// Every route here produces a credential and therefore takes a body, with
|
|
873
904
|
// exactly one exception: `/v1/auth/methods` is a **read**, and the contract
|
|
@@ -1064,10 +1095,15 @@ async function handleAuth(req, res, instance, requestId, options) {
|
|
|
1064
1095
|
return;
|
|
1065
1096
|
}
|
|
1066
1097
|
}
|
|
1067
|
-
//
|
|
1068
|
-
//
|
|
1069
|
-
//
|
|
1070
|
-
|
|
1098
|
+
// Deliberately not awaited. The body is the same `204` either way, but the
|
|
1099
|
+
// *time* to it was not: a miss returns after one SELECT and a hit does a
|
|
1100
|
+
// full SMTP round trip — hundreds of milliseconds against a couple — so an
|
|
1101
|
+
// awaited request() made the one endpoint reachable without a credential a
|
|
1102
|
+
// timing oracle for "does this address have an account", which is exactly
|
|
1103
|
+
// what the module's own header says it must not be. The failure is still
|
|
1104
|
+
// swallowed into a log line: an unhandled rejection would take the process
|
|
1105
|
+
// down over a relay being briefly unreachable.
|
|
1106
|
+
void options.recovery.request(email.email).catch((error) => {
|
|
1071
1107
|
logger.warn('could not start a password recovery', {
|
|
1072
1108
|
request_id: requestId,
|
|
1073
1109
|
error: String(error).slice(0, 200),
|
|
@@ -2348,9 +2384,9 @@ async function handle(req, res, options) {
|
|
|
2348
2384
|
send(res, outcome.unchanged ? 200 : 202, { document_id: outcome.documentId, job_id: outcome.jobId, status: outcome.unchanged ? 'indexed' : 'queued' }, requestId);
|
|
2349
2385
|
return;
|
|
2350
2386
|
}
|
|
2351
|
-
const documentMatch = /^\/v1\/documents\/([^/]+)
|
|
2387
|
+
const documentMatch = pathMatch(/^\/v1\/documents\/([^/]+)$/, instance);
|
|
2352
2388
|
if (req.method === 'DELETE' && documentMatch) {
|
|
2353
|
-
const id =
|
|
2389
|
+
const id = documentMatch[1];
|
|
2354
2390
|
const removed = await options.ingest.remove(auth, id);
|
|
2355
2391
|
await options.audit.write({
|
|
2356
2392
|
orgId: auth.orgId,
|
|
@@ -2370,7 +2406,7 @@ async function handle(req, res, options) {
|
|
|
2370
2406
|
return;
|
|
2371
2407
|
}
|
|
2372
2408
|
if (req.method === 'PATCH' && documentMatch) {
|
|
2373
|
-
const id =
|
|
2409
|
+
const id = documentMatch[1];
|
|
2374
2410
|
if (options.documents.updateMetadata === undefined) {
|
|
2375
2411
|
const problem = notFound(instance, requestId);
|
|
2376
2412
|
send(res, problem.status, problem.toJSON(), requestId);
|
|
@@ -2417,7 +2453,7 @@ async function handle(req, res, options) {
|
|
|
2417
2453
|
return;
|
|
2418
2454
|
}
|
|
2419
2455
|
if (req.method === 'GET' && documentMatch) {
|
|
2420
|
-
const id =
|
|
2456
|
+
const id = documentMatch[1];
|
|
2421
2457
|
const document = await options.documents.read(auth, id);
|
|
2422
2458
|
if (document === undefined) {
|
|
2423
2459
|
// T8. The same response for "no such document" and "another
|
|
@@ -2448,9 +2484,9 @@ async function handle(req, res, options) {
|
|
|
2448
2484
|
send(res, 200, document, requestId);
|
|
2449
2485
|
return;
|
|
2450
2486
|
}
|
|
2451
|
-
const jobMatch = /^\/v1\/jobs\/([^/]+)
|
|
2487
|
+
const jobMatch = pathMatch(/^\/v1\/jobs\/([^/]+)$/, instance);
|
|
2452
2488
|
if (req.method === 'GET' && jobMatch && options.jobs !== undefined) {
|
|
2453
|
-
const id =
|
|
2489
|
+
const id = jobMatch[1];
|
|
2454
2490
|
const job = await options.jobs.read(auth, id);
|
|
2455
2491
|
if (job === undefined) {
|
|
2456
2492
|
// A job names a document, so it is as much of an oracle as the document
|
|
@@ -2816,7 +2852,7 @@ async function handle(req, res, options) {
|
|
|
2816
2852
|
}
|
|
2817
2853
|
if (instance === '/v1/workspaces' && options.workspaces !== undefined) {
|
|
2818
2854
|
if (req.method === 'GET') {
|
|
2819
|
-
const page = readPage(url.searchParams, instance, requestId);
|
|
2855
|
+
const page = readPage(url.searchParams, instance, requestId, 'uuid');
|
|
2820
2856
|
if (page instanceof Problem) {
|
|
2821
2857
|
send(res, page.status, page.toJSON(), requestId);
|
|
2822
2858
|
return;
|
|
@@ -2889,7 +2925,7 @@ async function handle(req, res, options) {
|
|
|
2889
2925
|
}
|
|
2890
2926
|
}
|
|
2891
2927
|
if (req.method === 'GET' && instance === '/v1/layers' && options.layers !== undefined) {
|
|
2892
|
-
const page = readPage(url.searchParams, instance, requestId);
|
|
2928
|
+
const page = readPage(url.searchParams, instance, requestId, 'uuid');
|
|
2893
2929
|
if (page instanceof Problem) {
|
|
2894
2930
|
send(res, page.status, page.toJSON(), requestId);
|
|
2895
2931
|
return;
|
|
@@ -2979,7 +3015,7 @@ async function handle(req, res, options) {
|
|
|
2979
3015
|
return;
|
|
2980
3016
|
}
|
|
2981
3017
|
if (req.method === 'GET' && instance === '/v1/grants' && options.grants !== undefined) {
|
|
2982
|
-
const page = readPage(url.searchParams, instance, requestId);
|
|
3018
|
+
const page = readPage(url.searchParams, instance, requestId, 'uuid');
|
|
2983
3019
|
if (page instanceof Problem) {
|
|
2984
3020
|
send(res, page.status, page.toJSON(), requestId);
|
|
2985
3021
|
return;
|
|
@@ -3018,9 +3054,9 @@ async function handle(req, res, options) {
|
|
|
3018
3054
|
send(res, 201, grantJson(issued), requestId);
|
|
3019
3055
|
return;
|
|
3020
3056
|
}
|
|
3021
|
-
const grantMatch = /^\/v1\/grants\/([^/]+)
|
|
3057
|
+
const grantMatch = pathMatch(/^\/v1\/grants\/([^/]+)$/, instance);
|
|
3022
3058
|
if (req.method === 'DELETE' && grantMatch && options.grants !== undefined) {
|
|
3023
|
-
const id =
|
|
3059
|
+
const id = grantMatch[1];
|
|
3024
3060
|
const revoked = await options.grants.revoke(auth, id);
|
|
3025
3061
|
// Written before the response either way. A revocation nobody can prove
|
|
3026
3062
|
// happened is not a revocation an auditor will accept, and a *refused*
|
|
@@ -3339,7 +3375,7 @@ async function handle(req, res, options) {
|
|
|
3339
3375
|
send(res, problem.status, problem.toJSON(), requestId);
|
|
3340
3376
|
return;
|
|
3341
3377
|
}
|
|
3342
|
-
const page = readPage(url.searchParams, instance, requestId);
|
|
3378
|
+
const page = readPage(url.searchParams, instance, requestId, 'sequence');
|
|
3343
3379
|
if (page instanceof Problem) {
|
|
3344
3380
|
send(res, page.status, page.toJSON(), requestId);
|
|
3345
3381
|
return;
|
|
@@ -3609,11 +3645,31 @@ async function handle(req, res, options) {
|
|
|
3609
3645
|
// consent theirs to give. `404` for anything else, because an agent they
|
|
3610
3646
|
// cannot see and one that does not exist are the same answer here as
|
|
3611
3647
|
// everywhere.
|
|
3648
|
+
//
|
|
3649
|
+
// The whole listing, not one page of it. A `find` over the first 200
|
|
3650
|
+
// used to stand here, so in an organization with more accounts than
|
|
3651
|
+
// that, approving one ordered past the cut answered `404`
|
|
3652
|
+
// indistinguishably from "no such agent" — a valid consent refused with
|
|
3653
|
+
// nothing on the screen to say why. The loop terminates because the
|
|
3654
|
+
// listing seeks on `(created_at, id)`, and it stops early on a hit.
|
|
3612
3655
|
if (!delegating) {
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3656
|
+
let visible = false;
|
|
3657
|
+
if (options.serviceAccounts !== undefined) {
|
|
3658
|
+
let after;
|
|
3659
|
+
for (;;) {
|
|
3660
|
+
const found = await options.serviceAccounts.list(auth, { limit: 200, after });
|
|
3661
|
+
if (found.items.some((a) => a.id === serviceAccountId)) {
|
|
3662
|
+
visible = true;
|
|
3663
|
+
break;
|
|
3664
|
+
}
|
|
3665
|
+
if (found.nextCursor === null)
|
|
3666
|
+
break;
|
|
3667
|
+
after = decodeCursor(found.nextCursor, 'uuid');
|
|
3668
|
+
if (after === undefined)
|
|
3669
|
+
break;
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
if (!visible) {
|
|
3617
3673
|
const problem = notFound(instance, requestId);
|
|
3618
3674
|
send(res, problem.status, problem.toJSON(), requestId);
|
|
3619
3675
|
return;
|
|
@@ -3628,10 +3684,26 @@ async function handle(req, res, options) {
|
|
|
3628
3684
|
// restriction they wrote is not the one they meant. `404` for an unknown
|
|
3629
3685
|
// id and for an unreadable one alike, on invariant I6.
|
|
3630
3686
|
if (delegating && narrowing !== undefined && narrowing.length > 0) {
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3687
|
+
// The whole listing, for the reason the agent lookup above walks its
|
|
3688
|
+
// pages: one fixed page refused any layer ordered past the cut. The
|
|
3689
|
+
// loop stops early once every named layer has been seen.
|
|
3690
|
+
const readable = new Set();
|
|
3691
|
+
if (options.layers !== undefined) {
|
|
3692
|
+
let after;
|
|
3693
|
+
for (;;) {
|
|
3694
|
+
const found = await options.layers.list(auth, { limit: 500, after });
|
|
3695
|
+
for (const layer of found.items)
|
|
3696
|
+
readable.add(layer.id);
|
|
3697
|
+
if (narrowing.every((l) => readable.has(l.id)))
|
|
3698
|
+
break;
|
|
3699
|
+
if (found.nextCursor === null)
|
|
3700
|
+
break;
|
|
3701
|
+
after = decodeCursor(found.nextCursor, 'uuid');
|
|
3702
|
+
if (after === undefined)
|
|
3703
|
+
break;
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
if (narrowing.some((l) => !readable.has(l.id))) {
|
|
3635
3707
|
const problem = notFound(instance, requestId);
|
|
3636
3708
|
send(res, problem.status, problem.toJSON(), requestId);
|
|
3637
3709
|
return;
|
|
@@ -3723,7 +3795,7 @@ async function handle(req, res, options) {
|
|
|
3723
3795
|
return;
|
|
3724
3796
|
}
|
|
3725
3797
|
if (req.method === 'GET') {
|
|
3726
|
-
const page = readPage(url.searchParams, instance, requestId);
|
|
3798
|
+
const page = readPage(url.searchParams, instance, requestId, 'uuid');
|
|
3727
3799
|
if (page instanceof Problem) {
|
|
3728
3800
|
send(res, page.status, page.toJSON(), requestId);
|
|
3729
3801
|
return;
|
|
@@ -3782,14 +3854,14 @@ async function handle(req, res, options) {
|
|
|
3782
3854
|
return;
|
|
3783
3855
|
}
|
|
3784
3856
|
}
|
|
3785
|
-
const accountMatch = /^\/v1\/service-accounts\/([^/]+)
|
|
3857
|
+
const accountMatch = pathMatch(/^\/v1\/service-accounts\/([^/]+)$/, instance);
|
|
3786
3858
|
if (req.method === 'DELETE' && accountMatch && options.serviceAccounts !== undefined) {
|
|
3787
3859
|
if (!administers(auth)) {
|
|
3788
3860
|
const problem = notFound(instance, requestId);
|
|
3789
3861
|
send(res, problem.status, problem.toJSON(), requestId);
|
|
3790
3862
|
return;
|
|
3791
3863
|
}
|
|
3792
|
-
const id =
|
|
3864
|
+
const id = accountMatch[1];
|
|
3793
3865
|
const revoked = await options.serviceAccounts.revoke(auth, id);
|
|
3794
3866
|
await options.audit.write({
|
|
3795
3867
|
orgId: auth.orgId,
|
|
@@ -3838,7 +3910,7 @@ async function handle(req, res, options) {
|
|
|
3838
3910
|
}
|
|
3839
3911
|
if (instance === '/v1/users' && options.users !== undefined) {
|
|
3840
3912
|
if (req.method === 'GET') {
|
|
3841
|
-
const page = readPage(url.searchParams, instance, requestId);
|
|
3913
|
+
const page = readPage(url.searchParams, instance, requestId, 'uuid');
|
|
3842
3914
|
if (page instanceof Problem) {
|
|
3843
3915
|
send(res, page.status, page.toJSON(), requestId);
|
|
3844
3916
|
return;
|
|
@@ -3924,9 +3996,9 @@ async function handle(req, res, options) {
|
|
|
3924
3996
|
return;
|
|
3925
3997
|
}
|
|
3926
3998
|
}
|
|
3927
|
-
const userMatch = /^\/v1\/users\/([^/]+)
|
|
3999
|
+
const userMatch = pathMatch(/^\/v1\/users\/([^/]+)$/, instance);
|
|
3928
4000
|
if (userMatch && options.users !== undefined) {
|
|
3929
|
-
const id =
|
|
4001
|
+
const id = userMatch[1];
|
|
3930
4002
|
if (req.method === 'DELETE') {
|
|
3931
4003
|
// Disabled, never deleted, which is what `DELETE` means on every
|
|
3932
4004
|
// removable thing here: a document is tombstoned, a key is revoked, and
|
|
@@ -4047,9 +4119,9 @@ async function handle(req, res, options) {
|
|
|
4047
4119
|
return;
|
|
4048
4120
|
}
|
|
4049
4121
|
}
|
|
4050
|
-
const passwordMatch = /^\/v1\/users\/([^/]+)\/password
|
|
4122
|
+
const passwordMatch = pathMatch(/^\/v1\/users\/([^/]+)\/password$/, instance);
|
|
4051
4123
|
if (req.method === 'POST' && passwordMatch && options.users !== undefined) {
|
|
4052
|
-
const id =
|
|
4124
|
+
const id = passwordMatch[1];
|
|
4053
4125
|
const reset = await options.users.resetPassword(auth, id);
|
|
4054
4126
|
const refused = typeof reset === 'string' ? reset : undefined;
|
|
4055
4127
|
await options.audit.write({
|
|
@@ -4085,7 +4157,7 @@ async function handle(req, res, options) {
|
|
|
4085
4157
|
}
|
|
4086
4158
|
if (instance === '/v1/groups' && options.groups !== undefined) {
|
|
4087
4159
|
if (req.method === 'GET') {
|
|
4088
|
-
const page = readPage(url.searchParams, instance, requestId);
|
|
4160
|
+
const page = readPage(url.searchParams, instance, requestId, 'uuid');
|
|
4089
4161
|
if (page instanceof Problem) {
|
|
4090
4162
|
send(res, page.status, page.toJSON(), requestId);
|
|
4091
4163
|
return;
|
|
@@ -4136,9 +4208,9 @@ async function handle(req, res, options) {
|
|
|
4136
4208
|
return;
|
|
4137
4209
|
}
|
|
4138
4210
|
}
|
|
4139
|
-
const groupMatch = /^\/v1\/groups\/([^/]+)
|
|
4211
|
+
const groupMatch = pathMatch(/^\/v1\/groups\/([^/]+)$/, instance);
|
|
4140
4212
|
if (req.method === 'DELETE' && groupMatch && options.groups !== undefined) {
|
|
4141
|
-
const id =
|
|
4213
|
+
const id = groupMatch[1];
|
|
4142
4214
|
const removed = await options.groups.remove(auth, id);
|
|
4143
4215
|
await options.audit.write({
|
|
4144
4216
|
orgId: auth.orgId,
|
|
@@ -4157,11 +4229,11 @@ async function handle(req, res, options) {
|
|
|
4157
4229
|
send(res, 204, null, requestId);
|
|
4158
4230
|
return;
|
|
4159
4231
|
}
|
|
4160
|
-
const membersMatch = /^\/v1\/groups\/([^/]+)\/members
|
|
4232
|
+
const membersMatch = pathMatch(/^\/v1\/groups\/([^/]+)\/members$/, instance);
|
|
4161
4233
|
if (membersMatch && options.groups !== undefined) {
|
|
4162
|
-
const groupId =
|
|
4234
|
+
const groupId = membersMatch[1];
|
|
4163
4235
|
if (req.method === 'GET') {
|
|
4164
|
-
const page = readPage(url.searchParams, instance, requestId);
|
|
4236
|
+
const page = readPage(url.searchParams, instance, requestId, 'uuid');
|
|
4165
4237
|
if (page instanceof Problem) {
|
|
4166
4238
|
send(res, page.status, page.toJSON(), requestId);
|
|
4167
4239
|
return;
|
|
@@ -4213,11 +4285,11 @@ async function handle(req, res, options) {
|
|
|
4213
4285
|
// `{type}/{id}` rather than `{id}` alone: the edge is keyed by which member
|
|
4214
4286
|
// column it uses, so a bare uuid does not identify one. Same shape `grants`
|
|
4215
4287
|
// uses for the other end of the same relationship.
|
|
4216
|
-
const memberMatch = /^\/v1\/groups\/([^/]+)\/members\/(user|group)\/([^/]+)
|
|
4288
|
+
const memberMatch = pathMatch(/^\/v1\/groups\/([^/]+)\/members\/(user|group)\/([^/]+)$/, instance);
|
|
4217
4289
|
if (req.method === 'DELETE' && memberMatch && options.groups !== undefined) {
|
|
4218
|
-
const groupId =
|
|
4290
|
+
const groupId = memberMatch[1];
|
|
4219
4291
|
const type = memberMatch[2];
|
|
4220
|
-
const memberId =
|
|
4292
|
+
const memberId = memberMatch[3];
|
|
4221
4293
|
const removed = await options.groups.removeMember(auth, groupId, { type, id: memberId });
|
|
4222
4294
|
await options.audit.write({
|
|
4223
4295
|
orgId: auth.orgId,
|