@snagset/server 0.1.2 → 0.1.4
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/app.js +45 -6
- package/dist/app.js.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/db/scope.js +23 -0
- package/dist/db/scope.js.map +1 -0
- package/dist/instance/mcpHttp.js +42 -0
- package/dist/instance/mcpHttp.js.map +1 -0
- package/dist/mail/unsubscribe.js +5 -4
- package/dist/mail/unsubscribe.js.map +1 -1
- package/dist/orgs.js +87 -0
- package/dist/orgs.js.map +1 -0
- package/dist/plan.js +82 -0
- package/dist/plan.js.map +1 -0
- package/dist/projectCache.js +3 -2
- package/dist/projectCache.js.map +1 -1
- package/dist/reviewerCache.js +3 -2
- package/dist/reviewerCache.js.map +1 -1
- package/dist/routes/account.js +5 -4
- package/dist/routes/account.js.map +1 -1
- package/dist/routes/anchorReports.js +3 -2
- package/dist/routes/anchorReports.js.map +1 -1
- package/dist/routes/attachments.js +17 -6
- package/dist/routes/attachments.js.map +1 -1
- package/dist/routes/commentEdit.js +3 -2
- package/dist/routes/commentEdit.js.map +1 -1
- package/dist/routes/comments.js +7 -6
- package/dist/routes/comments.js.map +1 -1
- package/dist/routes/cors.js +5 -4
- package/dist/routes/cors.js.map +1 -1
- package/dist/routes/instance.js +29 -0
- package/dist/routes/instance.js.map +1 -1
- package/dist/routes/integrations.js +10 -5
- package/dist/routes/integrations.js.map +1 -1
- package/dist/routes/mcp.js +45 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/mcpAuth.js +59 -3
- package/dist/routes/mcpAuth.js.map +1 -1
- package/dist/routes/mcpToken.js +10 -9
- package/dist/routes/mcpToken.js.map +1 -1
- package/dist/routes/mcpTokens.js +10 -3
- package/dist/routes/mcpTokens.js.map +1 -1
- package/dist/routes/members.js +11 -0
- package/dist/routes/members.js.map +1 -1
- package/dist/routes/orgs.js +128 -0
- package/dist/routes/orgs.js.map +1 -0
- package/dist/routes/owner.js +34 -12
- package/dist/routes/owner.js.map +1 -1
- package/dist/routes/reviewLinks.js +11 -10
- package/dist/routes/reviewLinks.js.map +1 -1
- package/dist/routes/rounds.js +7 -6
- package/dist/routes/rounds.js.map +1 -1
- package/dist/routes/sessions.js +16 -15
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/sites.js +45 -26
- package/dist/routes/sites.js.map +1 -1
- package/dist/routes/threads.js +7 -11
- package/dist/routes/threads.js.map +1 -1
- package/dist/serverless.js +86 -120
- package/dist/serverless.js.map +1 -1
- package/dist/storage/settings.js +8 -7
- package/dist/storage/settings.js.map +1 -1
- package/dist/vendor/mcp/bin.js +72 -0
- package/dist/vendor/mcp/client.js +107 -0
- package/dist/vendor/mcp/server.js +242 -0
- package/dist/vendor/mcp/source.js +284 -0
- package/dist/vendor/mcp/view.js +199 -0
- package/dist/vendor/types/names.js +1 -0
- package/package.json +4 -2
- package/static/dashboard/assets/CompositeItem-DooiJvYh.js +1 -0
- package/static/dashboard/assets/{ConfirmDialog-CfUnmf4e.js → ConfirmDialog-B5Kq--VU.js} +1 -1
- package/static/dashboard/assets/{CopyButton-0ANrU8mS.js → CopyButton-BX1ldjjz.js} +1 -1
- package/static/dashboard/assets/FieldItemContext-DkOlIwHg.js +1 -0
- package/static/dashboard/assets/{InstallTag-CFvRuiwX.js → InstallTag-DahspVU0.js} +1 -1
- package/static/dashboard/assets/{SaveState-RDbpfaEm.js → SaveState-Q9xJjbqL.js} +1 -1
- package/static/dashboard/assets/_app-CRQzX614.js +56 -0
- package/static/dashboard/assets/{_app-DpPrYyh8.js → _app-Cd9_WR6L.js} +1 -1
- package/static/dashboard/assets/{activity-DwTWlcUi.js → activity-B-59HYua.js} +1 -1
- package/static/dashboard/assets/{arrow-left-2PKG3ITQ.js → arrow-left-aeWIKOlS.js} +1 -1
- package/static/dashboard/assets/{avatar-AfucjvZ4.js → avatar-KLKmXBKs.js} +1 -1
- package/static/dashboard/assets/{badge-CErha5Qd.js → badge-Dly0Vqky.js} +1 -1
- package/static/dashboard/assets/{card-DxFqxFQf.js → card-D3S4369l.js} +1 -1
- package/static/dashboard/assets/{check-Yc6VFTfm.js → check-CjehM7M4.js} +1 -1
- package/static/dashboard/assets/{chevron-right-Go23FN_b.js → chevron-right-CtWPB3ox.js} +1 -1
- package/static/dashboard/assets/{copy-BjvQHEe0.js → copy-DbrG17rp.js} +1 -1
- package/static/dashboard/assets/{createLucideIcon-DwbZIZsa.js → createLucideIcon-CJO80vPf.js} +1 -1
- package/static/dashboard/assets/{dialog-CgkCHO0g.js → dialog-GyPeOagK.js} +1 -1
- package/static/dashboard/assets/{dropdown-menu-DRa3ikv4.js → dropdown-menu-BOoXJZ3W.js} +3 -3
- package/static/dashboard/assets/{image-off-ByN_ZOK_.js → image-off-BYE-vNh9.js} +1 -1
- package/static/dashboard/assets/{index-BRTBtV91.css → index-BR4Mnw-o.css} +1 -1
- package/static/dashboard/assets/{index-DG2IWFx-.js → index-D4yHhKPM.js} +30 -30
- package/static/dashboard/assets/label-BGuYH_Ll.js +1 -0
- package/static/dashboard/assets/{layout-grid-Dj0cm_PO.js → layout-grid-DJ_OjGVc.js} +1 -1
- package/static/dashboard/assets/me-B-0XXdLF.js +1 -0
- package/static/dashboard/assets/{parts-B3hL1LIW.js → parts-DJaBw8EG.js} +1 -1
- package/static/dashboard/assets/{plus-CRLkl9fr.js → plus-x0wkLget.js} +1 -1
- package/static/dashboard/assets/s._site-s4IRnE6u.js +1 -0
- package/static/dashboard/assets/{s._site.comments-DCpM8f18.js → s._site.comments-Bt0AsQ8j.js} +3 -3
- package/static/dashboard/assets/s._site.reviewers-BzJsotII.js +1 -0
- package/static/dashboard/assets/s._site.settings-C6ei77kQ.js +1 -0
- package/static/dashboard/assets/setup-DTi8FBnt.js +1 -0
- package/static/dashboard/assets/sign-in-CGOMDkYg.js +1 -0
- package/static/dashboard/assets/sign-up-CfU4RCfo.js +1 -0
- package/static/dashboard/assets/sites-zZ5gTDCZ.js +1 -0
- package/static/dashboard/assets/studio-DaMq1jOg.js +1 -0
- package/static/dashboard/assets/{studio-BIDpGywS.js → studio-r6xIIciH.js} +1 -1
- package/static/dashboard/assets/studio.index-CsF3xaoh.js +1 -0
- package/static/dashboard/assets/studio.integrations-BnPEkEA3.js +1 -0
- package/static/dashboard/assets/studio.members-DegH-I8c.js +1 -0
- package/static/dashboard/assets/{sun-CyU37by7.js → sun-BLhaKyhb.js} +1 -1
- package/static/dashboard/assets/switch-En3PhTzh.js +1 -0
- package/static/dashboard/assets/{table-DEYsEr1D.js → table-DuU_ZP-u.js} +1 -1
- package/static/dashboard/assets/useAriaLabelledBy-C3DXCwhM.js +1 -0
- package/static/dashboard/assets/{useBaseQuery-Cyp8ADaN.js → useBaseQuery-B_iu1J2b.js} +1 -1
- package/static/dashboard/assets/useQuery-yeUPHKKg.js +1 -0
- package/static/dashboard/assets/useRender-Bl1M9SFS.js +1 -0
- package/static/dashboard/assets/useValueChanged-BwMBSSVy.js +1 -0
- package/static/dashboard/assets/{users-CcRaZGiI.js → users-c6wDSgLD.js} +1 -1
- package/static/dashboard/assets/{welcome-DFcmcB5m.js → welcome-Ddm5B8Xh.js} +2 -2
- package/static/dashboard/assets/you-BmjZYMtz.js +1 -0
- package/static/dashboard/assets/{zod-qEjeM9Eg.js → zod-CBoGbUzv.js} +1 -1
- package/static/dashboard/index.html +2 -2
- package/static/dashboard/assets/CompositeItem-CrOf38Oz.js +0 -1
- package/static/dashboard/assets/FieldItemContext-CFDZff0c.js +0 -1
- package/static/dashboard/assets/_app-CyFpAeiD.js +0 -56
- package/static/dashboard/assets/createBaseUIEventDetails-BWJgXhnd.js +0 -1
- package/static/dashboard/assets/label-Bombrpc6.js +0 -1
- package/static/dashboard/assets/me-Bno2H07j.js +0 -1
- package/static/dashboard/assets/s._site-CP12hsxz.js +0 -1
- package/static/dashboard/assets/s._site.reviewers-WT9D4fr3.js +0 -1
- package/static/dashboard/assets/s._site.settings-DaxVWDmT.js +0 -1
- package/static/dashboard/assets/setup-Ch-Vt7_T.js +0 -1
- package/static/dashboard/assets/sign-in-CdGjL0Ha.js +0 -1
- package/static/dashboard/assets/sites-8S1PAUfv.js +0 -1
- package/static/dashboard/assets/studio-DREMbrco.js +0 -1
- package/static/dashboard/assets/studio.index-B4-MAfEP.js +0 -1
- package/static/dashboard/assets/studio.integrations-D1bP5fqp.js +0 -1
- package/static/dashboard/assets/studio.members-DKl8oRd9.js +0 -1
- package/static/dashboard/assets/useQuery-DuAHuwx9.js +0 -1
- package/static/dashboard/assets/useRender-BCvsiXRL.js +0 -1
- package/static/dashboard/assets/you-ClBoja9X.js +0 -1
package/dist/serverless.js
CHANGED
|
@@ -4,129 +4,95 @@ import { loadConfig } from "./config.js";
|
|
|
4
4
|
import { applyPlatformDefaults } from "./platform.js";
|
|
5
5
|
import { createApp } from "./app.js";
|
|
6
6
|
import { resolveSecretKey } from "./boot.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*/
|
|
39
|
-
let cached = null;
|
|
40
|
-
function boot() {
|
|
41
|
-
if (cached)
|
|
7
|
+
export function createServerlessHandler(options = {}) {
|
|
8
|
+
let cached = null;
|
|
9
|
+
function boot() {
|
|
10
|
+
if (cached)
|
|
11
|
+
return cached;
|
|
12
|
+
applyPlatformDefaults(process.env); // D105 — Vercel names its own deployment
|
|
13
|
+
const cfg = loadConfig(process.env);
|
|
14
|
+
if (!cfg.get("SNAGSET_SECRET_KEY") && !cfg.get("SNAGSET_SECRET_KEY_FILE")) {
|
|
15
|
+
throw new Error("SNAGSET_SECRET_KEY is required on serverless. The filesystem here is ephemeral, so " +
|
|
16
|
+
"the bin's generate-a-key-on-first-boot path cannot work: each instance would invent " +
|
|
17
|
+
"a different key, and a session or an encrypted credential written by one would fail " +
|
|
18
|
+
"to decrypt on the next. Generate 32 random bytes, base64url them, and set it in the " +
|
|
19
|
+
"deploy's environment (08 §4.2).");
|
|
20
|
+
}
|
|
21
|
+
const sql = connect(cfg.required("DATABASE_URL"), { max: cfg.int("SNAGSET_DATABASE_POOL_MAX") });
|
|
22
|
+
const app = createApp({
|
|
23
|
+
sql,
|
|
24
|
+
cfg,
|
|
25
|
+
secretKey: resolveSecretKey(cfg, null, () => { }),
|
|
26
|
+
// A function instance cannot run the boot state machine — that reads and
|
|
27
|
+
// writes the claim token and would race across instances. The state is
|
|
28
|
+
// derived per request from the database instead, which is what `createApp`
|
|
29
|
+
// does when it is told nothing.
|
|
30
|
+
state: "READY",
|
|
31
|
+
version: options.version ?? process.env["SNAGSET_VERSION"] ?? "0.0.0",
|
|
32
|
+
commit: process.env["VERCEL_GIT_COMMIT_SHA"] ?? "unknown",
|
|
33
|
+
sourceUrl: options.sourceUrl ?? "https://github.com/snagset/snagset",
|
|
34
|
+
modified: false,
|
|
35
|
+
...(options.planResolver ? { planResolver: options.planResolver } : {}),
|
|
36
|
+
});
|
|
37
|
+
cached = { app, sql };
|
|
42
38
|
return cached;
|
|
43
|
-
applyPlatformDefaults(process.env); // D105 — Vercel names its own deployment
|
|
44
|
-
const cfg = loadConfig(process.env);
|
|
45
|
-
if (!cfg.get("SNAGSET_SECRET_KEY") && !cfg.get("SNAGSET_SECRET_KEY_FILE")) {
|
|
46
|
-
throw new Error("SNAGSET_SECRET_KEY is required on serverless. The filesystem here is ephemeral, so " +
|
|
47
|
-
"the bin's generate-a-key-on-first-boot path cannot work: each instance would invent " +
|
|
48
|
-
"a different key, and a session or an encrypted credential written by one would fail " +
|
|
49
|
-
"to decrypt on the next. Generate 32 random bytes, base64url them, and set it in the " +
|
|
50
|
-
"deploy's environment (08 §4.2).");
|
|
51
39
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
* function logs the operator may not know exist. On the deploy we bless, on
|
|
85
|
-
* the very first request, for the most ordinary mistake there is.
|
|
86
|
-
*
|
|
87
|
-
* The bin has had `explain()` for this since it shipped: an unreachable
|
|
88
|
-
* database prints "✗ no Postgres answered at host:port. Start one, or point
|
|
89
|
-
* DATABASE_URL at a database that is running." Serverless had nothing, so the
|
|
90
|
-
* same operator got a sentence on the bin and a shrug on Vercel.
|
|
91
|
-
*
|
|
92
|
-
* NAMES ONLY, never values — this response is public, and the whole point of
|
|
93
|
-
* the variable being missing is that somebody is about to paste a secret into
|
|
94
|
-
* a dashboard. `503` rather than `500`: the instance is not broken, it is not
|
|
95
|
-
* configured, and a proxy should feel free to retry once it is.
|
|
96
|
-
*/
|
|
97
|
-
function configFailure(e) {
|
|
98
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
99
|
-
return new Response(`Snagset is deployed but not configured.\n\n${message}\n\n` +
|
|
100
|
-
`Set it in your platform's environment settings and redeploy.\n` +
|
|
101
|
-
`\`snagset env\` lists every variable this build accepts.\n`, { status: 503, headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" } });
|
|
102
|
-
}
|
|
103
|
-
function serve(request) {
|
|
104
|
-
let app;
|
|
105
|
-
try {
|
|
106
|
-
app = boot().app;
|
|
40
|
+
/**
|
|
41
|
+
* The Web-standard handler every serverless target accepts.
|
|
42
|
+
*
|
|
43
|
+
* A `fetch(Request) => Response` rather than a Vercel-shaped adapter, because
|
|
44
|
+
* that is the same signature Cloudflare Workers, Deno and Bun take — D24 makes
|
|
45
|
+
* Workers the next hosting milestone on this same app factory, and an entry
|
|
46
|
+
* point written to one vendor's helper is one that has to be rewritten for it.
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* A CONFIGURATION FAILURE SHOULD SAY SO, not crash.
|
|
50
|
+
*
|
|
51
|
+
* `boot()` throws when a required variable is missing, and until 2026-09-01
|
|
52
|
+
* that throw reached the platform — which renders "This Serverless Function
|
|
53
|
+
* has crashed. FUNCTION_INVOCATION_FAILED" and puts the real message in
|
|
54
|
+
* function logs the operator may not know exist. On the deploy we bless, on
|
|
55
|
+
* the very first request, for the most ordinary mistake there is.
|
|
56
|
+
*
|
|
57
|
+
* The bin has had `explain()` for this since it shipped: an unreachable
|
|
58
|
+
* database prints "✗ no Postgres answered at host:port. Start one, or point
|
|
59
|
+
* DATABASE_URL at a database that is running." Serverless had nothing, so the
|
|
60
|
+
* same operator got a sentence on the bin and a shrug on Vercel.
|
|
61
|
+
*
|
|
62
|
+
* NAMES ONLY, never values — this response is public, and the whole point of
|
|
63
|
+
* the variable being missing is that somebody is about to paste a secret into
|
|
64
|
+
* a dashboard. `503` rather than `500`: the instance is not broken, it is not
|
|
65
|
+
* configured, and a proxy should feel free to retry once it is.
|
|
66
|
+
*/
|
|
67
|
+
function configFailure(e) {
|
|
68
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
69
|
+
return new Response(`Snagset is deployed but not configured.\n\n${message}\n\n` +
|
|
70
|
+
`Set it in your platform's environment settings and redeploy.\n` +
|
|
71
|
+
`\`snagset env\` lists every variable this build accepts.\n`, { status: 503, headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" } });
|
|
107
72
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
73
|
+
function serve(request) {
|
|
74
|
+
let app;
|
|
75
|
+
try {
|
|
76
|
+
app = boot().app;
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
// Nothing is cached on this path, so the NEXT request boots again — set
|
|
80
|
+
// the variable and the instance recovers without a redeploy.
|
|
81
|
+
return configFailure(e);
|
|
82
|
+
}
|
|
83
|
+
return app.fetch(request);
|
|
112
84
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
* down by the platform and never calls this.
|
|
121
|
-
*
|
|
122
|
-
* It exists because the cache above is module scope, which is exactly what
|
|
123
|
-
* makes a warm instance cheap and what makes a test process hang: without it
|
|
124
|
-
* node's runner waits forever on a pool nothing will close.
|
|
125
|
-
*/
|
|
126
|
-
export async function closeForTests() {
|
|
127
|
-
const held = cached;
|
|
128
|
-
cached = null;
|
|
129
|
-
if (held)
|
|
130
|
-
await held.sql.end({ timeout: 5 });
|
|
85
|
+
async function closeForTests() {
|
|
86
|
+
const held = cached;
|
|
87
|
+
cached = null;
|
|
88
|
+
if (held)
|
|
89
|
+
await held.sql.end({ timeout: 5 });
|
|
90
|
+
}
|
|
91
|
+
return { fetch: serve, closeForTests };
|
|
131
92
|
}
|
|
93
|
+
/** The blessed deploy: the engine as it is, no resolver, unlimited — every self-host's serverless entry. */
|
|
94
|
+
const standard = createServerlessHandler();
|
|
95
|
+
export default { fetch: standard.fetch };
|
|
96
|
+
export const handler = standard.fetch;
|
|
97
|
+
export const closeForTests = standard.closeForTests;
|
|
132
98
|
//# sourceMappingURL=serverless.js.map
|
package/dist/serverless.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serverless.js","sourceRoot":"","sources":["../src/serverless.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,SAAS,EAAY,MAAM,UAAU,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"serverless.js","sourceRoot":"","sources":["../src/serverless.ts"],"names":[],"mappings":"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"}
|
package/dist/storage/settings.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { open, seal } from "../crypto.js";
|
|
2
2
|
import { createBucket } from "./bucket.js";
|
|
3
|
+
import { maintenance } from "../db/scope.js";
|
|
3
4
|
/**
|
|
4
5
|
* The bucket, from the environment — D60, and the PRIMARY path.
|
|
5
6
|
*
|
|
@@ -47,10 +48,10 @@ export function envBucketConfig(env) {
|
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
const orgRow = async (sql) => {
|
|
50
|
-
const rows = await sql `
|
|
51
|
+
const rows = await maintenance(sql, (tx) => tx `
|
|
51
52
|
SELECT id, storage_config, storage_credentials, storage_credentials_iv, storage_connected_at
|
|
52
53
|
FROM orgs WHERE deleted_at IS NULL ORDER BY created_at LIMIT 1
|
|
53
|
-
|
|
54
|
+
`);
|
|
54
55
|
return rows[0] ?? null;
|
|
55
56
|
};
|
|
56
57
|
export async function storageStatus(sql, keys, env = process.env) {
|
|
@@ -102,15 +103,15 @@ export async function connectBucket(sql, keys, config, creds) {
|
|
|
102
103
|
if (!org)
|
|
103
104
|
return { ok: false, step: "instance", detail: "this instance is not claimed yet" };
|
|
104
105
|
const sealed = seal(keys, JSON.stringify(creds));
|
|
105
|
-
await sql `
|
|
106
|
+
await maintenance(sql, (tx) => tx `
|
|
106
107
|
UPDATE orgs
|
|
107
|
-
SET storage_config = ${
|
|
108
|
+
SET storage_config = ${tx.json({ ...config })},
|
|
108
109
|
storage_credentials = ${sealed.ciphertext},
|
|
109
110
|
storage_credentials_iv = ${sealed.iv},
|
|
110
111
|
storage_connected_at = now(),
|
|
111
112
|
updated_at = now()
|
|
112
113
|
WHERE id = ${org.id}
|
|
113
|
-
|
|
114
|
+
`);
|
|
114
115
|
return { ok: true };
|
|
115
116
|
}
|
|
116
117
|
/**
|
|
@@ -122,11 +123,11 @@ export async function connectBucket(sql, keys, config, creds) {
|
|
|
122
123
|
* costing money and recoverable by nobody.
|
|
123
124
|
*/
|
|
124
125
|
export async function disconnectBucket(sql) {
|
|
125
|
-
await sql `
|
|
126
|
+
await maintenance(sql, (tx) => tx `
|
|
126
127
|
UPDATE orgs
|
|
127
128
|
SET storage_connected_at = NULL, storage_credentials = NULL,
|
|
128
129
|
storage_credentials_iv = NULL, updated_at = now()
|
|
129
130
|
WHERE deleted_at IS NULL
|
|
130
|
-
|
|
131
|
+
`);
|
|
131
132
|
}
|
|
132
133
|
//# sourceMappingURL=settings.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/storage/settings.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,YAAY,EAA0D,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/storage/settings.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { createServer } from "./server.js";
|
|
4
|
+
/**
|
|
5
|
+
* The stdio entry point — 07 §8.1's supported transport.
|
|
6
|
+
*
|
|
7
|
+
* | Transport | Where it runs | Default |
|
|
8
|
+
* |---|---|---|
|
|
9
|
+
* | **stdio** | the developer's own machine, spawned by their MCP client | **the supported path** |
|
|
10
|
+
* | streamable HTTP | our server, at `POST /mcp` (D107) | **off** until the switch on Studio › Integrations › MCP, or `SNAGSET_MCP_HTTP_ENABLED` (D108) |
|
|
11
|
+
*
|
|
12
|
+
* This file is the first. stdio is the default and §8.5 is the reason, not
|
|
13
|
+
* a scheduling accident: source mapping needs the checkout, the checkout is
|
|
14
|
+
* on the developer's machine, and a remote server cannot offer `source:map`
|
|
15
|
+
* at all — the instance serves the second without it.
|
|
16
|
+
*
|
|
17
|
+
* ── NOTHING MAY BE WRITTEN TO STDOUT ──
|
|
18
|
+
*
|
|
19
|
+
* stdout IS the protocol channel. A `console.log` anywhere in this package
|
|
20
|
+
* corrupts the JSON-RPC stream and the client reports a parse error with no
|
|
21
|
+
* indication of where it came from. Diagnostics go to stderr, which the client
|
|
22
|
+
* shows in its logs. This is the single most common way an MCP server that
|
|
23
|
+
* works in a test fails in a real client.
|
|
24
|
+
*/
|
|
25
|
+
const env = process.env;
|
|
26
|
+
/**
|
|
27
|
+
* Config comes from the MCP CLIENT's process env (§8.2), not ours — it is
|
|
28
|
+
* written into the client's config file:
|
|
29
|
+
*
|
|
30
|
+
* ```jsonc
|
|
31
|
+
* { "mcpServers": { "snagset": { "command": "npx", "args": ["-y", "./server.js"],
|
|
32
|
+
* "env": { "SNAGSET_MCP_TOKEN": "…", "SNAGSET_MCP_HOST": "https://review.example.com" } } } }
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* `CLICKREVIEW_TOKEN` and `CLICKREVIEW_HOST` are DELETED — the last
|
|
36
|
+
* un-namespaced spelling in 07, kept there only as a deletion record. They are
|
|
37
|
+
* not read here, not even as a fallback: a fallback is how a deleted name stays
|
|
38
|
+
* alive for years.
|
|
39
|
+
*/
|
|
40
|
+
const token = env["SNAGSET_MCP_TOKEN"];
|
|
41
|
+
const host = env["SNAGSET_MCP_HOST"];
|
|
42
|
+
function die(message) {
|
|
43
|
+
// stderr, and a non-zero exit. A server that starts and then answers every
|
|
44
|
+
// tool call with "no token" looks to the agent like a product that does not
|
|
45
|
+
// work; failing at startup puts the message where the developer configuring
|
|
46
|
+
// it will actually see it.
|
|
47
|
+
process.stderr.write(`snagset-mcp: ${message}\n`);
|
|
48
|
+
process.exit(2);
|
|
49
|
+
}
|
|
50
|
+
if (!token) {
|
|
51
|
+
die("SNAGSET_MCP_TOKEN is not set. Mint one on the instance with `snagset mcp-token create <name>` " +
|
|
52
|
+
"and put it in your MCP client's config under env.");
|
|
53
|
+
}
|
|
54
|
+
if (!host) {
|
|
55
|
+
die("SNAGSET_MCP_HOST is not set — the base URL of your Snagset instance, e.g. https://review.example.com");
|
|
56
|
+
}
|
|
57
|
+
if (!token.startsWith("snagmcp_")) {
|
|
58
|
+
// Named early because the two credentials look nothing alike and the failure
|
|
59
|
+
// otherwise arrives as a 401 on the first tool call, three steps later.
|
|
60
|
+
die("SNAGSET_MCP_TOKEN does not look like an MCP token — they begin `snagmcp_`. An API key will not work here.");
|
|
61
|
+
}
|
|
62
|
+
const server = createServer({
|
|
63
|
+
host,
|
|
64
|
+
token,
|
|
65
|
+
// The client spawns us in the developer's project directory, which is what
|
|
66
|
+
// makes `map_thread_to_source` work with no configuration at all.
|
|
67
|
+
repoRoot: env["SNAGSET_MCP_REPO_ROOT"] ?? process.cwd(),
|
|
68
|
+
project: env["SNAGSET_MCP_PROJECT"],
|
|
69
|
+
version: env["SNAGSET_MCP_VERSION"] ?? "0.0.0",
|
|
70
|
+
});
|
|
71
|
+
await server.connect(new StdioServerTransport());
|
|
72
|
+
process.stderr.write(`snagset-mcp: connected to ${host}\n`);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP client — 07 §8.1's load-bearing sentence made literal.
|
|
3
|
+
*
|
|
4
|
+
* > *"It is not a second API. Every read the MCP server performs goes through
|
|
5
|
+
* > the same `/api/v1` routes with the same authorisation as any other caller;
|
|
6
|
+
* > it holds no database connection, no credential store and no cache of its
|
|
7
|
+
* > own. A bug in this package cannot read a thread its token could not already
|
|
8
|
+
* > read."*
|
|
9
|
+
*
|
|
10
|
+
* That last clause is a claim about THIS FILE, and it is only true while this
|
|
11
|
+
* is the only way out of the package. There is no `postgres` dependency here
|
|
12
|
+
* and there must never be one: the day this package can reach the database, its
|
|
13
|
+
* scopes become advisory and every mitigation in §8.6 rests on the agent
|
|
14
|
+
* behaving.
|
|
15
|
+
*
|
|
16
|
+
* The whole surface is one method. `fetch` is the platform's, so the package
|
|
17
|
+
* carries no HTTP dependency either.
|
|
18
|
+
*/
|
|
19
|
+
export class Client {
|
|
20
|
+
#host;
|
|
21
|
+
#token;
|
|
22
|
+
#fetch;
|
|
23
|
+
constructor(cfg) {
|
|
24
|
+
// Trailing slash removed once, here, rather than at eleven call sites —
|
|
25
|
+
// `https://x/` + `/api/v1` is a double slash, which some proxies 404 and
|
|
26
|
+
// some silently normalise, so it fails on somebody else's deployment and
|
|
27
|
+
// not on ours.
|
|
28
|
+
this.#host = cfg.host.replace(/\/+$/, "");
|
|
29
|
+
this.#token = cfg.token;
|
|
30
|
+
this.#fetch = cfg.fetch ?? globalThis.fetch;
|
|
31
|
+
}
|
|
32
|
+
get host() {
|
|
33
|
+
return this.#host;
|
|
34
|
+
}
|
|
35
|
+
async call(path, init = {}) {
|
|
36
|
+
const url = new URL(this.#host + path);
|
|
37
|
+
for (const [k, v] of Object.entries(init.query ?? {})) {
|
|
38
|
+
if (v !== undefined && v !== "")
|
|
39
|
+
url.searchParams.set(k, v);
|
|
40
|
+
}
|
|
41
|
+
let res;
|
|
42
|
+
try {
|
|
43
|
+
res = await this.#fetch(url, {
|
|
44
|
+
method: init.method ?? "GET",
|
|
45
|
+
headers: {
|
|
46
|
+
Authorization: `Bearer ${this.#token}`,
|
|
47
|
+
...(init.body === undefined ? {} : { "content-type": "application/json" }),
|
|
48
|
+
},
|
|
49
|
+
...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
// A network failure is the single most likely thing to go wrong here —
|
|
54
|
+
// the agent is on a laptop, the instance is somewhere else — and the
|
|
55
|
+
// message names the host, because "fetch failed" sends a developer to
|
|
56
|
+
// look at their code rather than at their VPN.
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
status: 0,
|
|
60
|
+
message: `Could not reach ${this.#host}: ${e instanceof Error ? e.message : String(e)}. Check SNAGSET_MCP_HOST and that the instance is running.`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const text = await res.text();
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
const parsed = safeJson(text);
|
|
66
|
+
const code = parsed?.error?.code;
|
|
67
|
+
const message = parsed?.error?.message ?? text.slice(0, 400);
|
|
68
|
+
// 07 §8.4: on 429 the tool layer must tell the agent to STOP and ask its
|
|
69
|
+
// user — *"never a silent retry, never a sleep-and-retry loop. An agent
|
|
70
|
+
// that has been talked into mass-resolving threads must hit a wall it has
|
|
71
|
+
// to report, because the wall is the only part of the mitigation a human
|
|
72
|
+
// sees."* This client therefore has no retry logic at all, on any status.
|
|
73
|
+
if (res.status === 429) {
|
|
74
|
+
// Appended only when the server did not already say it. The server's
|
|
75
|
+
// own budget message ends "do not retry", and adding ours produced
|
|
76
|
+
// "…do not retry. Do not retry — tell your user." — a stop signal that
|
|
77
|
+
// reads like a stutter, which is the opposite of what it is for.
|
|
78
|
+
const stop = /do not retry/i.test(message) ? "" : " Do not retry — tell your user.";
|
|
79
|
+
return { ok: false, status: 429, message: message + stop };
|
|
80
|
+
}
|
|
81
|
+
if (code === "mcp_scope_denied") {
|
|
82
|
+
return { ok: false, status: 403, message };
|
|
83
|
+
}
|
|
84
|
+
return { ok: false, status: res.status, message: `${res.status}: ${message}` };
|
|
85
|
+
}
|
|
86
|
+
const value = safeJson(text);
|
|
87
|
+
if (value === null && text.trim() !== "null") {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
status: res.status,
|
|
91
|
+
// The exact failure the server's own widget route had: a 200 with an
|
|
92
|
+
// HTML body, because an SPA fallback answered a path nobody had
|
|
93
|
+
// registered. Named, because "unexpected token <" is not a diagnosis.
|
|
94
|
+
message: `${this.#host} answered ${res.status} with something that is not JSON. Is SNAGSET_MCP_HOST pointing at a Snagset instance?`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return { ok: true, value: value };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function safeJson(text) {
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(text);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|