@nacre.work/api 0.16.0 → 0.17.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/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +23 -0
- package/dist/errors.js.map +1 -1
- package/dist/principals.d.ts +50 -5
- package/dist/principals.d.ts.map +1 -1
- package/dist/principals.js +50 -9
- package/dist/principals.js.map +1 -1
- package/dist/server.d.ts +11 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +72 -32
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
|
5
5
|
import { URL } from 'node:url';
|
|
6
6
|
import { logger, MetadataError, MultipartError, multipartBoundary, parseMultipart, queryAudit, parseFilters, parseMetadata, PROTECTED_RESOURCE_PATH, JWKS_PATH, AUTHORIZATION_SERVER_PATH, AUTHORIZE_PATH, CODE_TTL_MS, REGISTER_PATH, TOKEN_PATH, authorizationServerMetadata, consentRedirect, generateClientId, generateCode, redirectAllowed, verifierMatches, ADMIN_PREFIX, adminRoutes, withAuditSinks, TooBusy, } from '@nacre.work/core';
|
|
7
7
|
import { administers, administersTenants, authenticate, rejectTenantOverride, } from './auth.js';
|
|
8
|
-
import { badRequest, internal, notFound, Problem } from './errors.js';
|
|
8
|
+
import { badRequest, internal, notAdministeredHere, notFound, Problem } from './errors.js';
|
|
9
9
|
import { isConflict, isReplay } from './idempotency.js';
|
|
10
10
|
import { limitHeaders } from './limits.js';
|
|
11
11
|
import { looksLikeEmail } from './principals.js';
|
|
@@ -614,6 +614,7 @@ async function handleAuth(req, res, instance, requestId, options) {
|
|
|
614
614
|
actor: `user:${tokens.userId}`,
|
|
615
615
|
action: 'login',
|
|
616
616
|
result: 'allow',
|
|
617
|
+
target: { user_id: tokens.userId },
|
|
617
618
|
detail: {},
|
|
618
619
|
requestId,
|
|
619
620
|
});
|
|
@@ -1175,7 +1176,8 @@ async function handle(req, res, options) {
|
|
|
1175
1176
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
1176
1177
|
action: 'tenant_override_attempt',
|
|
1177
1178
|
result: 'deny',
|
|
1178
|
-
|
|
1179
|
+
target: { path: instance },
|
|
1180
|
+
detail: {},
|
|
1179
1181
|
requestId,
|
|
1180
1182
|
});
|
|
1181
1183
|
send(res, override.status, override.toJSON(), requestId);
|
|
@@ -1201,7 +1203,8 @@ async function handle(req, res, options) {
|
|
|
1201
1203
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
1202
1204
|
action: 'rate_limited',
|
|
1203
1205
|
result: 'deny',
|
|
1204
|
-
|
|
1206
|
+
target: { resource },
|
|
1207
|
+
detail: { limit: decision.limit },
|
|
1205
1208
|
requestId,
|
|
1206
1209
|
});
|
|
1207
1210
|
const problem = new Problem({
|
|
@@ -1791,7 +1794,8 @@ async function handle(req, res, options) {
|
|
|
1791
1794
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
1792
1795
|
action: 'embedding_provider.create',
|
|
1793
1796
|
result: 'deny',
|
|
1794
|
-
|
|
1797
|
+
target: { name: name.trim() },
|
|
1798
|
+
detail: {},
|
|
1795
1799
|
requestId,
|
|
1796
1800
|
});
|
|
1797
1801
|
const problem = notFound(instance, requestId);
|
|
@@ -1815,7 +1819,8 @@ async function handle(req, res, options) {
|
|
|
1815
1819
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
1816
1820
|
action: 'embedding_provider.create',
|
|
1817
1821
|
result: 'allow',
|
|
1818
|
-
|
|
1822
|
+
target: { provider_id: outcome.provider.id, name: outcome.provider.name },
|
|
1823
|
+
detail: { model: outcome.provider.model },
|
|
1819
1824
|
requestId,
|
|
1820
1825
|
});
|
|
1821
1826
|
send(res, 201, {
|
|
@@ -1867,7 +1872,8 @@ async function handle(req, res, options) {
|
|
|
1867
1872
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
1868
1873
|
action: 'create_workspace',
|
|
1869
1874
|
result: outcome.kind === 'created' ? 'allow' : 'deny',
|
|
1870
|
-
|
|
1875
|
+
target: { slug },
|
|
1876
|
+
detail: { outcome: outcome.kind },
|
|
1871
1877
|
requestId,
|
|
1872
1878
|
});
|
|
1873
1879
|
if (outcome.kind === 'denied') {
|
|
@@ -1951,7 +1957,8 @@ async function handle(req, res, options) {
|
|
|
1951
1957
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
1952
1958
|
action: 'create_layer',
|
|
1953
1959
|
result: outcome.kind === 'created' ? 'allow' : 'deny',
|
|
1954
|
-
|
|
1960
|
+
target: { workspace_id: workspaceId, slug },
|
|
1961
|
+
detail: { outcome: outcome.kind },
|
|
1955
1962
|
requestId,
|
|
1956
1963
|
});
|
|
1957
1964
|
if (outcome.kind === 'denied') {
|
|
@@ -2014,11 +2021,12 @@ async function handle(req, res, options) {
|
|
|
2014
2021
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
2015
2022
|
action: 'issue_grant',
|
|
2016
2023
|
result: issued === undefined ? 'deny' : 'allow',
|
|
2017
|
-
|
|
2024
|
+
target: {
|
|
2018
2025
|
principal: `${parsed.principalType}:${parsed.principalId}`,
|
|
2019
2026
|
scope: `${parsed.scopeType}:${parsed.scopeId}`,
|
|
2020
2027
|
permission: parsed.permission,
|
|
2021
2028
|
},
|
|
2029
|
+
detail: {},
|
|
2022
2030
|
requestId,
|
|
2023
2031
|
});
|
|
2024
2032
|
if (issued === undefined) {
|
|
@@ -2041,7 +2049,8 @@ async function handle(req, res, options) {
|
|
|
2041
2049
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
2042
2050
|
action: 'revoke_grant',
|
|
2043
2051
|
result: revoked ? 'allow' : 'deny',
|
|
2044
|
-
|
|
2052
|
+
target: { grant_id: id },
|
|
2053
|
+
detail: {},
|
|
2045
2054
|
requestId,
|
|
2046
2055
|
});
|
|
2047
2056
|
if (!revoked) {
|
|
@@ -2759,7 +2768,8 @@ async function handle(req, res, options) {
|
|
|
2759
2768
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
2760
2769
|
action: 'create_service_account',
|
|
2761
2770
|
result: 'deny',
|
|
2762
|
-
|
|
2771
|
+
target: { name: name.trim() },
|
|
2772
|
+
detail: { reason: 'name taken' },
|
|
2763
2773
|
requestId,
|
|
2764
2774
|
});
|
|
2765
2775
|
const problem = new Problem({
|
|
@@ -2779,10 +2789,11 @@ async function handle(req, res, options) {
|
|
|
2779
2789
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
2780
2790
|
action: 'create_service_account',
|
|
2781
2791
|
result: 'allow',
|
|
2792
|
+
target: { service_account_id: account.id, name: account.name },
|
|
2782
2793
|
// The prefix, never the key. This row is readable by anyone with the
|
|
2783
2794
|
// audit log, and the key is not recoverable from anywhere else by
|
|
2784
2795
|
// design — putting it here would undo that.
|
|
2785
|
-
detail: {
|
|
2796
|
+
detail: { key_prefix: account.keyPrefix },
|
|
2786
2797
|
requestId,
|
|
2787
2798
|
});
|
|
2788
2799
|
// The only time the key exists outside the caller's process.
|
|
@@ -2804,7 +2815,8 @@ async function handle(req, res, options) {
|
|
|
2804
2815
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
2805
2816
|
action: 'revoke_service_account',
|
|
2806
2817
|
result: revoked ? 'allow' : 'deny',
|
|
2807
|
-
|
|
2818
|
+
target: { service_account_id: id },
|
|
2819
|
+
detail: {},
|
|
2808
2820
|
requestId,
|
|
2809
2821
|
});
|
|
2810
2822
|
if (!revoked) {
|
|
@@ -2834,7 +2846,8 @@ async function handle(req, res, options) {
|
|
|
2834
2846
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
2835
2847
|
action: 'administer_principals',
|
|
2836
2848
|
result: 'deny',
|
|
2837
|
-
|
|
2849
|
+
target: { path: instance, method: req.method ?? 'GET' },
|
|
2850
|
+
detail: { reason: 'not an org_admin' },
|
|
2838
2851
|
requestId,
|
|
2839
2852
|
});
|
|
2840
2853
|
const problem = notFound(instance, requestId);
|
|
@@ -2881,7 +2894,8 @@ async function handle(req, res, options) {
|
|
|
2881
2894
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
2882
2895
|
action: 'create_user',
|
|
2883
2896
|
result: 'deny',
|
|
2884
|
-
|
|
2897
|
+
target: { email },
|
|
2898
|
+
detail: { reason: 'address taken' },
|
|
2885
2899
|
requestId,
|
|
2886
2900
|
});
|
|
2887
2901
|
const problem = new Problem({
|
|
@@ -2900,10 +2914,11 @@ async function handle(req, res, options) {
|
|
|
2900
2914
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
2901
2915
|
action: 'create_user',
|
|
2902
2916
|
result: 'allow',
|
|
2903
|
-
|
|
2904
|
-
//
|
|
2905
|
-
//
|
|
2906
|
-
|
|
2917
|
+
target: { user_id: created.user.id, email },
|
|
2918
|
+
// The role, never the password — this row is readable by anyone with
|
|
2919
|
+
// the audit log, and the password is not recoverable from anywhere
|
|
2920
|
+
// else by design.
|
|
2921
|
+
detail: { role },
|
|
2907
2922
|
requestId,
|
|
2908
2923
|
});
|
|
2909
2924
|
// The only time the password exists outside the caller's process.
|
|
@@ -2931,9 +2946,15 @@ async function handle(req, res, options) {
|
|
|
2931
2946
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
2932
2947
|
action: 'disable_user',
|
|
2933
2948
|
result: disabled === 'updated' ? 'allow' : 'deny',
|
|
2934
|
-
|
|
2949
|
+
target: { user_id: id },
|
|
2950
|
+
detail: disabled === 'updated' ? {} : { reason: disabled },
|
|
2935
2951
|
requestId,
|
|
2936
2952
|
});
|
|
2953
|
+
if (disabled === 'platform-admin') {
|
|
2954
|
+
const problem = notAdministeredHere(instance, requestId);
|
|
2955
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2937
2958
|
if (disabled === 'last-admin') {
|
|
2938
2959
|
const problem = new Problem({
|
|
2939
2960
|
type: 'https://nacre.work/errors/conflict',
|
|
@@ -2989,14 +3010,19 @@ async function handle(req, res, options) {
|
|
|
2989
3010
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
2990
3011
|
action: 'update_user',
|
|
2991
3012
|
result: changed === 'updated' ? 'allow' : 'deny',
|
|
3013
|
+
target: { user_id: id },
|
|
2992
3014
|
detail: {
|
|
2993
|
-
user_id: id,
|
|
2994
3015
|
...(wantsRole ? { role: fields.role } : {}),
|
|
2995
3016
|
...(wantsDisabled ? { disabled: fields.disabled } : {}),
|
|
2996
3017
|
...(changed === 'updated' ? {} : { reason: changed }),
|
|
2997
3018
|
},
|
|
2998
3019
|
requestId,
|
|
2999
3020
|
});
|
|
3021
|
+
if (changed === 'platform-admin') {
|
|
3022
|
+
const problem = notAdministeredHere(instance, requestId);
|
|
3023
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3000
3026
|
if (changed === 'last-admin') {
|
|
3001
3027
|
// 409 rather than 404: the caller is looking straight at this user —
|
|
3002
3028
|
// it is their own account or one they just listed — so the answer is
|
|
@@ -3026,17 +3052,29 @@ async function handle(req, res, options) {
|
|
|
3026
3052
|
const passwordMatch = /^\/v1\/users\/([^/]+)\/password$/.exec(instance);
|
|
3027
3053
|
if (req.method === 'POST' && passwordMatch && options.users !== undefined) {
|
|
3028
3054
|
const id = decodeURIComponent(passwordMatch[1]);
|
|
3029
|
-
const
|
|
3055
|
+
const reset = await options.users.resetPassword(auth, id);
|
|
3056
|
+
const refused = typeof reset === 'string' ? reset : undefined;
|
|
3030
3057
|
await options.audit.write({
|
|
3031
3058
|
orgId: auth.orgId,
|
|
3032
3059
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
3033
3060
|
action: 'reset_password',
|
|
3034
|
-
result:
|
|
3061
|
+
result: refused === undefined ? 'allow' : 'deny',
|
|
3035
3062
|
// That it happened and to whom. Never the value.
|
|
3036
|
-
|
|
3063
|
+
target: { user_id: id },
|
|
3064
|
+
detail: refused === undefined ? {} : { reason: refused },
|
|
3037
3065
|
requestId,
|
|
3038
3066
|
});
|
|
3039
|
-
|
|
3067
|
+
// The refusal that matters most on this route, and the one the whole
|
|
3068
|
+
// guard was written for. Demoting a platform administrator takes the role
|
|
3069
|
+
// away; resetting their password hands the caller the plaintext in the
|
|
3070
|
+
// response and lets them sign in as the account that administers the
|
|
3071
|
+
// installation.
|
|
3072
|
+
if (reset === 'platform-admin') {
|
|
3073
|
+
const problem = notAdministeredHere(instance, requestId);
|
|
3074
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
if (reset === 'no-user') {
|
|
3040
3078
|
const problem = notFound(instance, requestId);
|
|
3041
3079
|
send(res, problem.status, problem.toJSON(), requestId);
|
|
3042
3080
|
return;
|
|
@@ -3044,7 +3082,7 @@ async function handle(req, res, options) {
|
|
|
3044
3082
|
// Generated rather than chosen, for the reason `init` gives: an argument
|
|
3045
3083
|
// ends up in a shell history, and a password an administrator picked is
|
|
3046
3084
|
// one they know. This is the only time it exists outside the process.
|
|
3047
|
-
send(res, 200, { password }, requestId);
|
|
3085
|
+
send(res, 200, { password: reset.password }, requestId);
|
|
3048
3086
|
return;
|
|
3049
3087
|
}
|
|
3050
3088
|
if (instance === '/v1/groups' && options.groups !== undefined) {
|
|
@@ -3072,7 +3110,8 @@ async function handle(req, res, options) {
|
|
|
3072
3110
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
3073
3111
|
action: 'create_group',
|
|
3074
3112
|
result: 'deny',
|
|
3075
|
-
|
|
3113
|
+
target: { name: name.trim() },
|
|
3114
|
+
detail: { reason: 'name taken' },
|
|
3076
3115
|
requestId,
|
|
3077
3116
|
});
|
|
3078
3117
|
const problem = new Problem({
|
|
@@ -3091,7 +3130,8 @@ async function handle(req, res, options) {
|
|
|
3091
3130
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
3092
3131
|
action: 'create_group',
|
|
3093
3132
|
result: 'allow',
|
|
3094
|
-
|
|
3133
|
+
target: { group_id: created.id, name: created.name },
|
|
3134
|
+
detail: {},
|
|
3095
3135
|
requestId,
|
|
3096
3136
|
});
|
|
3097
3137
|
send(res, 201, groupJson(created), requestId);
|
|
@@ -3107,7 +3147,8 @@ async function handle(req, res, options) {
|
|
|
3107
3147
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
3108
3148
|
action: 'delete_group',
|
|
3109
3149
|
result: removed ? 'allow' : 'deny',
|
|
3110
|
-
|
|
3150
|
+
target: { group_id: id },
|
|
3151
|
+
detail: {},
|
|
3111
3152
|
requestId,
|
|
3112
3153
|
});
|
|
3113
3154
|
if (!removed) {
|
|
@@ -3151,10 +3192,8 @@ async function handle(req, res, options) {
|
|
|
3151
3192
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
3152
3193
|
action: 'add_group_member',
|
|
3153
3194
|
result: outcome === 'no-group' || outcome === 'no-member' ? 'deny' : 'allow',
|
|
3195
|
+
target: { group_id: groupId, member_type: type, member_id: memberId },
|
|
3154
3196
|
detail: {
|
|
3155
|
-
group_id: groupId,
|
|
3156
|
-
member_type: type,
|
|
3157
|
-
member_id: memberId,
|
|
3158
3197
|
...(outcome === 'already' ? { note: 'already a member' } : {}),
|
|
3159
3198
|
...(outcome === 'no-group' || outcome === 'no-member' ? { reason: outcome } : {}),
|
|
3160
3199
|
},
|
|
@@ -3187,7 +3226,8 @@ async function handle(req, res, options) {
|
|
|
3187
3226
|
actor: `${auth.principal.type}:${auth.principal.id}`,
|
|
3188
3227
|
action: 'remove_group_member',
|
|
3189
3228
|
result: removed ? 'allow' : 'deny',
|
|
3190
|
-
|
|
3229
|
+
target: { group_id: groupId, member_type: type, member_id: memberId },
|
|
3230
|
+
detail: {},
|
|
3191
3231
|
requestId,
|
|
3192
3232
|
});
|
|
3193
3233
|
if (!removed) {
|