@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/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
- detail: { path: instance },
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
- detail: { resource, limit: decision.limit },
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
- detail: { name: name.trim() },
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
- detail: { name: outcome.provider.name, model: outcome.provider.model },
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
- detail: { slug, outcome: outcome.kind },
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
- detail: { workspace_id: workspaceId, slug, outcome: outcome.kind },
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
- detail: {
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
- detail: { grant_id: id },
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
- detail: { name: name.trim(), reason: 'name taken' },
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: { service_account_id: account.id, key_prefix: account.keyPrefix },
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
- detail: { service_account_id: id },
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
- detail: { path: instance, method: req.method ?? 'GET', reason: 'not an org_admin' },
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
- detail: { email, reason: 'address taken' },
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
- // The address and the role. Never the password — this row is readable
2904
- // by anyone with the audit log, and the password is not recoverable
2905
- // from anywhere else by design.
2906
- detail: { user_id: created.user.id, email, role },
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
- detail: { user_id: id, ...(disabled === 'updated' ? {} : { reason: disabled }) },
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 password = await options.users.resetPassword(auth, id);
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: password === undefined ? 'deny' : 'allow',
3061
+ result: refused === undefined ? 'allow' : 'deny',
3035
3062
  // That it happened and to whom. Never the value.
3036
- detail: { user_id: id },
3063
+ target: { user_id: id },
3064
+ detail: refused === undefined ? {} : { reason: refused },
3037
3065
  requestId,
3038
3066
  });
3039
- if (password === undefined) {
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
- detail: { name: name.trim(), reason: 'name taken' },
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
- detail: { group_id: created.id, name: created.name },
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
- detail: { group_id: id },
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
- detail: { group_id: groupId, member_type: type, member_id: memberId },
3229
+ target: { group_id: groupId, member_type: type, member_id: memberId },
3230
+ detail: {},
3191
3231
  requestId,
3192
3232
  });
3193
3233
  if (!removed) {