@nacre.work/api 0.33.0 → 0.34.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/admin-names.d.ts +52 -0
- package/dist/admin-names.d.ts.map +1 -0
- package/dist/admin-names.js +127 -0
- package/dist/admin-names.js.map +1 -0
- package/dist/admin-writes.d.ts +40 -0
- package/dist/admin-writes.d.ts.map +1 -0
- package/dist/admin-writes.js +724 -0
- package/dist/admin-writes.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/main.js +56 -1
- package/dist/main.js.map +1 -1
- package/dist/oauth-store.d.ts +2 -2
- package/dist/oauth-store.d.ts.map +1 -1
- package/dist/oauth-store.js +27 -1
- package/dist/oauth-store.js.map +1 -1
- package/dist/proposals.d.ts +166 -0
- package/dist/proposals.d.ts.map +1 -0
- package/dist/proposals.js +289 -0
- package/dist/proposals.js.map +1 -0
- package/dist/server.d.ts +41 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +103 -0
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -16,6 +16,7 @@ import { looksLikeEmail } from './principals.js';
|
|
|
16
16
|
import { clientSource } from './source.js';
|
|
17
17
|
import { auditFormat, auditJson, readAuditQuery, toCsv, toNdjson, } from './audit-export.js';
|
|
18
18
|
import { decodeCursor, readPage } from './pagination.js';
|
|
19
|
+
import { applyProposal, cancelProposal } from './proposals.js';
|
|
19
20
|
/**
|
|
20
21
|
* The default body cap, in bytes.
|
|
21
22
|
*
|
|
@@ -3155,6 +3156,38 @@ async function handle(req, res, options) {
|
|
|
3155
3156
|
}, requestId);
|
|
3156
3157
|
return;
|
|
3157
3158
|
}
|
|
3159
|
+
/*
|
|
3160
|
+
* Where a client connects: the REST base, the MCP endpoint, and — for an
|
|
3161
|
+
* organization administrator — the administrative one.
|
|
3162
|
+
*
|
|
3163
|
+
* Asked of the server rather than worked out in a browser, because the
|
|
3164
|
+
* page's own origin is the answer only when the console, the API and the
|
|
3165
|
+
* MCP transport share one, and the operator already told the server which
|
|
3166
|
+
* addresses are the public ones. Any credential may ask: a member connects
|
|
3167
|
+
* their own client, and an agent reading this learns nothing its token did
|
|
3168
|
+
* not already let it reach.
|
|
3169
|
+
*
|
|
3170
|
+
* `mcp_admin` is the administrative MCP, and only somebody who administers
|
|
3171
|
+
* this organization is told it — its consent screen refuses everybody
|
|
3172
|
+
* else, so offering it would be offering a connection that cannot be made.
|
|
3173
|
+
* The same `administers` the gated handlers call.
|
|
3174
|
+
*/
|
|
3175
|
+
if (instance === '/v1/endpoints') {
|
|
3176
|
+
if (req.method !== 'GET' || options.endpoints === undefined) {
|
|
3177
|
+
const problem = notFound(instance, requestId);
|
|
3178
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
const e = options.endpoints;
|
|
3182
|
+
send(res, 200, {
|
|
3183
|
+
api: e.api,
|
|
3184
|
+
mcp: e.mcp,
|
|
3185
|
+
...(administers(auth) ? { mcp_admin: e.mcpAdmin } : {}),
|
|
3186
|
+
contract: e.contract,
|
|
3187
|
+
version: e.version,
|
|
3188
|
+
}, requestId);
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3158
3191
|
/*
|
|
3159
3192
|
* The caller's own password.
|
|
3160
3193
|
*
|
|
@@ -4123,6 +4156,76 @@ async function handle(req, res, options) {
|
|
|
4123
4156
|
* is a denylist consulted on every request, which would make local
|
|
4124
4157
|
* verification not local.
|
|
4125
4158
|
*/
|
|
4159
|
+
// ── proposals ─────────────────────────────────────────────────────────
|
|
4160
|
+
//
|
|
4161
|
+
// docs/mcp-admin.md, "A change is proposed, and a person applies it". What
|
|
4162
|
+
// the person's administrative connection proposed, waiting for them, and
|
|
4163
|
+
// the two buttons. A person in their own session and nobody else: every
|
|
4164
|
+
// other caller — a connected application's token included — gets the
|
|
4165
|
+
// answer a path that does not exist gets.
|
|
4166
|
+
const proposalMatch = pathMatch(/^\/v1\/proposals(?:\/([0-9a-f-]{36})\/(apply|cancel))?$/i, instance);
|
|
4167
|
+
if (proposalMatch !== null && options.proposals !== undefined) {
|
|
4168
|
+
const own = auth.principal.type === 'user' && auth.delegation === undefined && administers(auth);
|
|
4169
|
+
const id = proposalMatch[1];
|
|
4170
|
+
const verb = proposalMatch[2];
|
|
4171
|
+
if (!own || (id === undefined ? req.method !== 'GET' : req.method !== 'POST')) {
|
|
4172
|
+
const problem = notFound(instance, requestId);
|
|
4173
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
4174
|
+
return;
|
|
4175
|
+
}
|
|
4176
|
+
const deps = { proposals: options.proposals.store, audit: options.audit, writes: options.proposals.writes };
|
|
4177
|
+
const by = { through: 'console', personId: auth.principal.id };
|
|
4178
|
+
if (id === undefined) {
|
|
4179
|
+
const items = await options.proposals.store.pending(auth);
|
|
4180
|
+
send(res, 200, {
|
|
4181
|
+
items: items.map((p) => ({
|
|
4182
|
+
id: p.id,
|
|
4183
|
+
tool: p.tool,
|
|
4184
|
+
module: p.module,
|
|
4185
|
+
summary: p.summary,
|
|
4186
|
+
details: p.details,
|
|
4187
|
+
created_at: p.createdAt,
|
|
4188
|
+
expires_at: p.expiresAt,
|
|
4189
|
+
connection: { id: p.connection.id, application: p.connection.application },
|
|
4190
|
+
})),
|
|
4191
|
+
}, requestId);
|
|
4192
|
+
return;
|
|
4193
|
+
}
|
|
4194
|
+
const outcome = verb === 'apply'
|
|
4195
|
+
? await applyProposal(deps, auth, id, by, requestId)
|
|
4196
|
+
: await cancelProposal(deps, auth, id, by, requestId);
|
|
4197
|
+
switch (outcome.kind) {
|
|
4198
|
+
case 'applied':
|
|
4199
|
+
send(res, 200, { applied: true, result: outcome.result }, requestId);
|
|
4200
|
+
return;
|
|
4201
|
+
case 'cancelled':
|
|
4202
|
+
send(res, 204, null, requestId);
|
|
4203
|
+
return;
|
|
4204
|
+
case 'refused': {
|
|
4205
|
+
// The proposal was theirs and is now spent; the change itself was
|
|
4206
|
+
// refused, in the tool's own words — the role was lost, the name is
|
|
4207
|
+
// taken, the last administrator would go. Not 404: they are looking
|
|
4208
|
+
// straight at it.
|
|
4209
|
+
const problem = new Problem({
|
|
4210
|
+
type: 'https://nacre.work/errors/proposal-refused',
|
|
4211
|
+
title: 'The change was refused',
|
|
4212
|
+
status: 409,
|
|
4213
|
+
detail: outcome.reason,
|
|
4214
|
+
instance,
|
|
4215
|
+
requestId,
|
|
4216
|
+
});
|
|
4217
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
4218
|
+
return;
|
|
4219
|
+
}
|
|
4220
|
+
default: {
|
|
4221
|
+
// Absent, somebody else's, decided, expired, or proposed through a
|
|
4222
|
+
// connection revoked since — one answer.
|
|
4223
|
+
const problem = notFound(instance, requestId);
|
|
4224
|
+
send(res, problem.status, problem.toJSON(), requestId);
|
|
4225
|
+
return;
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4126
4229
|
if (instance === '/v1/oauth/consents' && options.oauth !== undefined) {
|
|
4127
4230
|
if (req.method !== 'GET') {
|
|
4128
4231
|
const problem = notFound(instance, requestId);
|