@stackwright-pro/mcp 0.2.0-alpha.61 → 0.2.0-alpha.68
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/integrity.js +11 -11
- package/dist/integrity.js.map +1 -1
- package/dist/integrity.mjs +11 -11
- package/dist/integrity.mjs.map +1 -1
- package/dist/server.js +796 -63
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +813 -71
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1331,6 +1331,13 @@ function answersToManifestFormat(answers, questions) {
|
|
|
1331
1331
|
return result;
|
|
1332
1332
|
}
|
|
1333
1333
|
|
|
1334
|
+
// src/validation.ts
|
|
1335
|
+
var SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
1336
|
+
var SAFE_QUESTION_ID = /^[a-z0-9][a-z0-9-]{0,63}$/i;
|
|
1337
|
+
function isValidPhase(phase) {
|
|
1338
|
+
return SAFE_PHASE.test(phase);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1334
1341
|
// src/tools/questions.ts
|
|
1335
1342
|
var ManifestQuestionSchema = import_zod8.z.object({
|
|
1336
1343
|
id: import_zod8.z.string(),
|
|
@@ -1365,7 +1372,6 @@ function registerQuestionTools(server2) {
|
|
|
1365
1372
|
},
|
|
1366
1373
|
async ({ phase, questions, answers }) => {
|
|
1367
1374
|
let resolvedQuestions;
|
|
1368
|
-
const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
1369
1375
|
if (!SAFE_PHASE.test(phase)) {
|
|
1370
1376
|
return {
|
|
1371
1377
|
content: [
|
|
@@ -1467,7 +1473,6 @@ function registerQuestionTools(server2) {
|
|
|
1467
1473
|
"Returns the next unanswered question for a phase as a plain JSON object \u2014 one question at a time. Returns { done: true } when all questions are answered or the phase has no questions. Call this in a loop: present the question in plain chat, get user reply, call record_answer, repeat.",
|
|
1468
1474
|
{ phase: import_zod8.z.string().describe('Phase name e.g. "designer", "api", "auth"') },
|
|
1469
1475
|
async ({ phase }) => {
|
|
1470
|
-
const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
1471
1476
|
if (!SAFE_PHASE.test(phase)) {
|
|
1472
1477
|
return {
|
|
1473
1478
|
content: [
|
|
@@ -1532,7 +1537,6 @@ function registerQuestionTools(server2) {
|
|
|
1532
1537
|
answer: import_zod8.z.string().describe("The user's free-text answer")
|
|
1533
1538
|
},
|
|
1534
1539
|
async ({ phase, questionId, answer }) => {
|
|
1535
|
-
const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
1536
1540
|
if (!SAFE_PHASE.test(phase)) {
|
|
1537
1541
|
return {
|
|
1538
1542
|
content: [
|
|
@@ -1541,7 +1545,7 @@ function registerQuestionTools(server2) {
|
|
|
1541
1545
|
isError: true
|
|
1542
1546
|
};
|
|
1543
1547
|
}
|
|
1544
|
-
if (
|
|
1548
|
+
if (!SAFE_QUESTION_ID.test(questionId)) {
|
|
1545
1549
|
return {
|
|
1546
1550
|
content: [
|
|
1547
1551
|
{ type: "text", text: JSON.stringify({ error: "Invalid questionId" }) }
|
|
@@ -1694,14 +1698,78 @@ function handleSaveManifest(input) {
|
|
|
1694
1698
|
}
|
|
1695
1699
|
}
|
|
1696
1700
|
function handleSavePhaseAnswers(input) {
|
|
1701
|
+
if (!isValidPhase(input.phase)) {
|
|
1702
|
+
return {
|
|
1703
|
+
text: JSON.stringify({
|
|
1704
|
+
success: false,
|
|
1705
|
+
error: `Invalid phase name: "${input.phase.slice(0, 50)}". Must be lowercase alphanumeric with hyphens, max 31 chars.`
|
|
1706
|
+
}),
|
|
1707
|
+
isError: true
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
for (let i = 0; i < input.rawAnswers.length; i++) {
|
|
1711
|
+
const a = input.rawAnswers[i];
|
|
1712
|
+
if (!a.question_header || a.question_header.trim().length === 0) {
|
|
1713
|
+
return {
|
|
1714
|
+
text: JSON.stringify({
|
|
1715
|
+
success: false,
|
|
1716
|
+
error: `rawAnswers[${i}].question_header is empty \u2014 every answer must have a non-empty question_header.`
|
|
1717
|
+
}),
|
|
1718
|
+
isError: true
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
if (!a.selected_options || a.selected_options.length === 0) {
|
|
1722
|
+
return {
|
|
1723
|
+
text: JSON.stringify({
|
|
1724
|
+
success: false,
|
|
1725
|
+
error: `rawAnswers[${i}].selected_options is empty for question_header "${a.question_header}" \u2014 every answer must have at least one selected option.`
|
|
1726
|
+
}),
|
|
1727
|
+
isError: true
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
if (input.questions && input.questions.length > 0 && input.rawAnswers.length === 0) {
|
|
1732
|
+
return {
|
|
1733
|
+
text: JSON.stringify({
|
|
1734
|
+
success: false,
|
|
1735
|
+
error: `${input.questions.length} questions provided but rawAnswers is empty \u2014 this is a malformed payload. If the phase truly has no questions, omit the questions parameter.`
|
|
1736
|
+
}),
|
|
1737
|
+
isError: true
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1697
1740
|
const cwd = input._cwd ?? process.cwd();
|
|
1698
1741
|
const dir = (0, import_path3.join)(cwd, ".stackwright", "answers");
|
|
1699
1742
|
const filePath = (0, import_path3.join)(dir, `${input.phase}.json`);
|
|
1700
1743
|
try {
|
|
1701
1744
|
(0, import_fs3.mkdirSync)(dir, { recursive: true });
|
|
1745
|
+
const warnings = [];
|
|
1702
1746
|
let answers;
|
|
1703
1747
|
if (input.questions && input.questions.length > 0) {
|
|
1704
|
-
|
|
1748
|
+
try {
|
|
1749
|
+
answers = answersToManifestFormat(input.rawAnswers, input.questions);
|
|
1750
|
+
} catch (mapErr) {
|
|
1751
|
+
answers = {};
|
|
1752
|
+
warnings.push(
|
|
1753
|
+
`Reverse-mapping threw (${mapErr instanceof Error ? mapErr.message : String(mapErr)}) \u2014 falling back to direct key mapping.`
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
if (Object.keys(answers).length === 0 && input.rawAnswers.length > 0) {
|
|
1757
|
+
answers = Object.fromEntries(
|
|
1758
|
+
input.rawAnswers.map((a) => [
|
|
1759
|
+
a.question_header,
|
|
1760
|
+
a.selected_options.length > 1 ? a.selected_options : a.selected_options[0] ?? ""
|
|
1761
|
+
])
|
|
1762
|
+
);
|
|
1763
|
+
warnings.push(
|
|
1764
|
+
`Reverse-mapping produced empty answers despite ${input.rawAnswers.length} rawAnswers \u2014 used direct key mapping as fallback. Ensure question_header values match the manifest header format (e.g. "DESI-1", not descriptive keys).`
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
const requiredCount = input.questions.filter((q) => q.required !== false).length;
|
|
1768
|
+
if (input.rawAnswers.length < requiredCount) {
|
|
1769
|
+
warnings.push(
|
|
1770
|
+
`Only ${input.rawAnswers.length} answers provided for ${requiredCount} required questions (${input.questions.length} total) \u2014 answers may be incomplete.`
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1705
1773
|
} else {
|
|
1706
1774
|
answers = Object.fromEntries(
|
|
1707
1775
|
input.rawAnswers.map((a) => [a.question_header, a.selected_options[0] ?? ""])
|
|
@@ -1728,7 +1796,8 @@ function handleSavePhaseAnswers(input) {
|
|
|
1728
1796
|
text: JSON.stringify({
|
|
1729
1797
|
success: true,
|
|
1730
1798
|
path: filePath,
|
|
1731
|
-
answersCount: Object.keys(answers).length
|
|
1799
|
+
answersCount: Object.keys(answers).length,
|
|
1800
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
1732
1801
|
}),
|
|
1733
1802
|
isError: false
|
|
1734
1803
|
};
|
|
@@ -1875,15 +1944,31 @@ function registerOrchestrationTools(server2) {
|
|
|
1875
1944
|
rawAnswers: jsonCoerce(
|
|
1876
1945
|
import_zod9.z.array(
|
|
1877
1946
|
import_zod9.z.object({
|
|
1878
|
-
question_header: import_zod9.z.string(),
|
|
1879
|
-
selected_options: import_zod9.z.array(import_zod9.z.string()),
|
|
1947
|
+
question_header: import_zod9.z.string().min(1, "question_header must not be empty"),
|
|
1948
|
+
selected_options: import_zod9.z.array(import_zod9.z.string()).min(1, "selected_options must have at least one entry"),
|
|
1880
1949
|
other_text: import_zod9.z.string().nullable().optional()
|
|
1881
1950
|
})
|
|
1882
1951
|
)
|
|
1883
1952
|
).describe(
|
|
1884
1953
|
"Answers as returned by ask_user_question \u2014 pass the native array, not a JSON string"
|
|
1885
1954
|
),
|
|
1886
|
-
questions: jsonCoerce(
|
|
1955
|
+
questions: jsonCoerce(
|
|
1956
|
+
import_zod9.z.array(
|
|
1957
|
+
import_zod9.z.object({
|
|
1958
|
+
id: import_zod9.z.string(),
|
|
1959
|
+
question: import_zod9.z.string(),
|
|
1960
|
+
type: import_zod9.z.string(),
|
|
1961
|
+
options: import_zod9.z.array(import_zod9.z.object({ label: import_zod9.z.string(), value: import_zod9.z.string() })).optional(),
|
|
1962
|
+
required: import_zod9.z.boolean().optional(),
|
|
1963
|
+
default: import_zod9.z.union([import_zod9.z.string(), import_zod9.z.boolean(), import_zod9.z.array(import_zod9.z.string())]).optional(),
|
|
1964
|
+
help: import_zod9.z.string().optional(),
|
|
1965
|
+
dependsOn: import_zod9.z.object({
|
|
1966
|
+
questionId: import_zod9.z.string(),
|
|
1967
|
+
value: import_zod9.z.union([import_zod9.z.string(), import_zod9.z.array(import_zod9.z.string())])
|
|
1968
|
+
}).optional()
|
|
1969
|
+
}).passthrough()
|
|
1970
|
+
).optional()
|
|
1971
|
+
).describe(
|
|
1887
1972
|
"Original manifest questions for label\u2192value reverse-mapping \u2014 pass the native array, not a JSON string"
|
|
1888
1973
|
)
|
|
1889
1974
|
},
|
|
@@ -2285,12 +2370,13 @@ var PHASE_DEPENDENCIES = {
|
|
|
2285
2370
|
// workflow states as trigger sources. Produces OpenAPI specs consumed
|
|
2286
2371
|
// by pages and dashboard for typed data wiring.
|
|
2287
2372
|
services: ["api", "workflow"],
|
|
2288
|
-
// pages/dashboard:
|
|
2289
|
-
//
|
|
2290
|
-
//
|
|
2291
|
-
//
|
|
2292
|
-
|
|
2293
|
-
|
|
2373
|
+
// pages/dashboard: depend on services for typed endpoint wiring (OpenAPI
|
|
2374
|
+
// specs) and on geo so the specialist prompt includes geo-manifest.json
|
|
2375
|
+
// — page/dashboard otters see which page slugs are already claimed and
|
|
2376
|
+
// won't attempt to overwrite them (swp-73c). 'api' is still transitive
|
|
2377
|
+
// through 'data'; auth removed — page-otter has graceful fallback.
|
|
2378
|
+
pages: ["designer", "theme", "data", "services", "geo"],
|
|
2379
|
+
dashboard: ["designer", "theme", "data", "services", "geo"],
|
|
2294
2380
|
// auth is the penultimate phase — runs after all content-producing phases
|
|
2295
2381
|
// so it can read pages, dashboard, workflow, and geo artifacts for route
|
|
2296
2382
|
// protection and RBAC wiring. Skipped upstream phases still satisfy deps
|
|
@@ -2326,7 +2412,7 @@ var PHASE_TO_OTTER2 = {
|
|
|
2326
2412
|
polish: "stackwright-pro-polish-otter",
|
|
2327
2413
|
geo: "stackwright-pro-geo-otter"
|
|
2328
2414
|
};
|
|
2329
|
-
function
|
|
2415
|
+
function isValidPhase2(phase) {
|
|
2330
2416
|
return PHASE_ORDER.includes(phase);
|
|
2331
2417
|
}
|
|
2332
2418
|
function defaultPhaseStatus() {
|
|
@@ -2422,7 +2508,7 @@ function handleGetPipelineState(_cwd) {
|
|
|
2422
2508
|
}
|
|
2423
2509
|
function handleSetPipelineState(input) {
|
|
2424
2510
|
const cwd = input._cwd ?? process.cwd();
|
|
2425
|
-
if (input.phase && !
|
|
2511
|
+
if (input.phase && !isValidPhase2(input.phase)) {
|
|
2426
2512
|
return {
|
|
2427
2513
|
text: JSON.stringify({
|
|
2428
2514
|
error: true,
|
|
@@ -2441,6 +2527,28 @@ function handleSetPipelineState(input) {
|
|
|
2441
2527
|
isError: true
|
|
2442
2528
|
};
|
|
2443
2529
|
}
|
|
2530
|
+
if (input.updates && input.updates.length > 0) {
|
|
2531
|
+
for (const update of input.updates) {
|
|
2532
|
+
if (!isValidPhase2(update.phase)) {
|
|
2533
|
+
return {
|
|
2534
|
+
text: JSON.stringify({
|
|
2535
|
+
error: true,
|
|
2536
|
+
message: `Invalid phase in batch update: ${update.phase}. Valid phases are: ${PHASE_ORDER.join(", ")}`
|
|
2537
|
+
}),
|
|
2538
|
+
isError: true
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
if (!VALID_FIELDS.includes(update.field)) {
|
|
2542
|
+
return {
|
|
2543
|
+
text: JSON.stringify({
|
|
2544
|
+
error: true,
|
|
2545
|
+
message: `Invalid field in batch update: ${update.field}. Valid fields are: ${VALID_FIELDS.join(", ")}`
|
|
2546
|
+
}),
|
|
2547
|
+
isError: true
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2444
2552
|
try {
|
|
2445
2553
|
const state = readState(cwd);
|
|
2446
2554
|
if (input.status) {
|
|
@@ -2460,6 +2568,16 @@ function handleSetPipelineState(input) {
|
|
|
2460
2568
|
}
|
|
2461
2569
|
state.currentPhase = phase;
|
|
2462
2570
|
}
|
|
2571
|
+
if (input.updates && input.updates.length > 0) {
|
|
2572
|
+
for (const update of input.updates) {
|
|
2573
|
+
if (!state.phases[update.phase]) {
|
|
2574
|
+
state.phases[update.phase] = defaultPhaseStatus();
|
|
2575
|
+
}
|
|
2576
|
+
const batchPhaseState = state.phases[update.phase];
|
|
2577
|
+
batchPhaseState[update.field] = update.value;
|
|
2578
|
+
state.currentPhase = update.phase;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2463
2581
|
writeState(cwd, state);
|
|
2464
2582
|
return { text: JSON.stringify(state), isError: false };
|
|
2465
2583
|
} catch (err) {
|
|
@@ -2469,7 +2587,7 @@ function handleSetPipelineState(input) {
|
|
|
2469
2587
|
function handleCheckExecutionReady(_cwd, phase) {
|
|
2470
2588
|
const cwd = _cwd ?? process.cwd();
|
|
2471
2589
|
if (phase) {
|
|
2472
|
-
if (!
|
|
2590
|
+
if (!isValidPhase2(phase)) {
|
|
2473
2591
|
return {
|
|
2474
2592
|
text: JSON.stringify({
|
|
2475
2593
|
error: true,
|
|
@@ -2617,7 +2735,7 @@ function handleListArtifacts(_cwd) {
|
|
|
2617
2735
|
function handleWritePhaseQuestions(input) {
|
|
2618
2736
|
const cwd = input._cwd ?? process.cwd();
|
|
2619
2737
|
const { phase, responseText } = input;
|
|
2620
|
-
if (!
|
|
2738
|
+
if (!isValidPhase2(phase)) {
|
|
2621
2739
|
return {
|
|
2622
2740
|
text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
|
|
2623
2741
|
isError: true
|
|
@@ -2675,7 +2793,7 @@ function handleWritePhaseQuestions(input) {
|
|
|
2675
2793
|
function handleBuildSpecialistPrompt(input) {
|
|
2676
2794
|
const cwd = input._cwd ?? process.cwd();
|
|
2677
2795
|
const { phase } = input;
|
|
2678
|
-
if (!
|
|
2796
|
+
if (!isValidPhase2(phase)) {
|
|
2679
2797
|
return {
|
|
2680
2798
|
text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
|
|
2681
2799
|
isError: true
|
|
@@ -2768,6 +2886,18 @@ ${JSON.stringify(content, null, 2)}`
|
|
|
2768
2886
|
if (buildContextText) {
|
|
2769
2887
|
parts.push("BUILD_CONTEXT:", buildContextText, "");
|
|
2770
2888
|
}
|
|
2889
|
+
if (phase === "auth") {
|
|
2890
|
+
const initContextPath = (0, import_path5.join)(cwd, ".stackwright", "init-context.json");
|
|
2891
|
+
if ((0, import_fs5.existsSync)(initContextPath)) {
|
|
2892
|
+
try {
|
|
2893
|
+
const initContext = JSON.parse(safeReadSync(initContextPath));
|
|
2894
|
+
if (initContext.devOnly === true || initContext.nonInteractive === true) {
|
|
2895
|
+
answers["devOnly"] = true;
|
|
2896
|
+
}
|
|
2897
|
+
} catch {
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2771
2901
|
parts.push("ANSWERS:", JSON.stringify(answers, null, 2));
|
|
2772
2902
|
if (artifactSections.length > 0) {
|
|
2773
2903
|
parts.push("", "UPSTREAM ARTIFACTS:", "", ...artifactSections);
|
|
@@ -3050,6 +3180,12 @@ var PHASE_ARTIFACT_SCHEMA = {
|
|
|
3050
3180
|
protectedRoutes: ["/dashboard/:path*", "/procurement/:path*"],
|
|
3051
3181
|
auditEnabled: true,
|
|
3052
3182
|
auditRetentionDays: 90
|
|
3183
|
+
},
|
|
3184
|
+
// devScripts is OPTIONAL — only present when devOnly: true was used
|
|
3185
|
+
// Polish otter and foreman MUST check devScripts.written before referencing scripts
|
|
3186
|
+
devScripts: {
|
|
3187
|
+
written: true,
|
|
3188
|
+
scripts: { "dev:admin": "MOCK_USER=admin next dev" }
|
|
3053
3189
|
}
|
|
3054
3190
|
},
|
|
3055
3191
|
null,
|
|
@@ -3061,7 +3197,12 @@ var PHASE_ARTIFACT_SCHEMA = {
|
|
|
3061
3197
|
generatedBy: "stackwright-pro-polish-otter",
|
|
3062
3198
|
landingPage: { slug: "", rewritten: true },
|
|
3063
3199
|
navigation: { itemCount: 5, items: ["/dashboard", "/equipment", "/workflows"] },
|
|
3064
|
-
gettingStarted: "<rewritten|redirected|not-found>"
|
|
3200
|
+
gettingStarted: "<rewritten|redirected|not-found>",
|
|
3201
|
+
scaffoldCleanup: {
|
|
3202
|
+
deleted: ["content/posts/getting-started.yaml"],
|
|
3203
|
+
rewritten: ["app/not-found.tsx"],
|
|
3204
|
+
skipped: []
|
|
3205
|
+
}
|
|
3065
3206
|
},
|
|
3066
3207
|
null,
|
|
3067
3208
|
2
|
|
@@ -3070,7 +3211,7 @@ var PHASE_ARTIFACT_SCHEMA = {
|
|
|
3070
3211
|
function handleValidateArtifact(input) {
|
|
3071
3212
|
const cwd = input._cwd ?? process.cwd();
|
|
3072
3213
|
const { phase, responseText, artifact: directArtifact } = input;
|
|
3073
|
-
if (!
|
|
3214
|
+
if (!isValidPhase2(phase)) {
|
|
3074
3215
|
return {
|
|
3075
3216
|
text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
|
|
3076
3217
|
isError: true
|
|
@@ -3218,7 +3359,16 @@ function registerPipelineTools(server2) {
|
|
|
3218
3359
|
status: import_zod11.z.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level status override"),
|
|
3219
3360
|
incrementRetry: boolCoerce(import_zod11.z.boolean().optional()).describe(
|
|
3220
3361
|
"Bump retryCount by 1 \u2014 must be a JSON boolean"
|
|
3221
|
-
)
|
|
3362
|
+
),
|
|
3363
|
+
updates: jsonCoerce(
|
|
3364
|
+
import_zod11.z.array(
|
|
3365
|
+
import_zod11.z.object({
|
|
3366
|
+
phase: import_zod11.z.string(),
|
|
3367
|
+
field: import_zod11.z.enum(["questionsCollected", "answered", "executed", "artifactWritten"]),
|
|
3368
|
+
value: import_zod11.z.boolean()
|
|
3369
|
+
})
|
|
3370
|
+
).optional()
|
|
3371
|
+
).describe("Batch updates \u2014 apply multiple field changes in one atomic read\u2192modify\u2192write")
|
|
3222
3372
|
},
|
|
3223
3373
|
async (args) => res(
|
|
3224
3374
|
handleSetPipelineState({
|
|
@@ -3226,7 +3376,8 @@ function registerPipelineTools(server2) {
|
|
|
3226
3376
|
...args.field != null ? { field: args.field } : {},
|
|
3227
3377
|
...args.value != null ? { value: args.value } : {},
|
|
3228
3378
|
...args.status != null ? { status: args.status } : {},
|
|
3229
|
-
...args.incrementRetry != null ? { incrementRetry: args.incrementRetry } : {}
|
|
3379
|
+
...args.incrementRetry != null ? { incrementRetry: args.incrementRetry } : {},
|
|
3380
|
+
...args.updates != null ? { updates: args.updates } : {}
|
|
3230
3381
|
})
|
|
3231
3382
|
)
|
|
3232
3383
|
);
|
|
@@ -3262,7 +3413,6 @@ function registerPipelineTools(server2) {
|
|
|
3262
3413
|
},
|
|
3263
3414
|
async ({ phase, responseText, questions }) => {
|
|
3264
3415
|
if (phase && questions && Array.isArray(questions)) {
|
|
3265
|
-
const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
3266
3416
|
if (!SAFE_PHASE.test(phase)) {
|
|
3267
3417
|
return {
|
|
3268
3418
|
content: [
|
|
@@ -3347,6 +3497,16 @@ var OTTER_WRITE_ALLOWLISTS = {
|
|
|
3347
3497
|
prefix: ".env",
|
|
3348
3498
|
suffix: "",
|
|
3349
3499
|
description: "Dotenv files (.env, .env.local, .env.production, etc.)"
|
|
3500
|
+
},
|
|
3501
|
+
{
|
|
3502
|
+
prefix: "lib/mock-auth",
|
|
3503
|
+
suffix: ".ts",
|
|
3504
|
+
description: "Mock auth module (DEV_ONLY_MODE role updates)"
|
|
3505
|
+
},
|
|
3506
|
+
{
|
|
3507
|
+
prefix: "package.json",
|
|
3508
|
+
suffix: ".json",
|
|
3509
|
+
description: "Project package.json (DEV_ONLY_MODE script cleanup)"
|
|
3350
3510
|
}
|
|
3351
3511
|
],
|
|
3352
3512
|
"stackwright-pro-data-otter": [
|
|
@@ -3384,7 +3544,8 @@ var OTTER_WRITE_ALLOWLISTS = {
|
|
|
3384
3544
|
},
|
|
3385
3545
|
{ prefix: "pages/", suffix: "/content.yml", description: "Landing page content" },
|
|
3386
3546
|
{ prefix: "pages/", suffix: "/content.yaml", description: "Landing page content" },
|
|
3387
|
-
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "Polish artifact" }
|
|
3547
|
+
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "Polish artifact" },
|
|
3548
|
+
{ prefix: "README.md", suffix: "", description: "Project README rewrite" }
|
|
3388
3549
|
],
|
|
3389
3550
|
"stackwright-services-otter": [
|
|
3390
3551
|
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "Services config artifact" },
|
|
@@ -3404,7 +3565,9 @@ var PROTECTED_PATH_PREFIXES = [
|
|
|
3404
3565
|
".stackwright/artifacts/signatures.json",
|
|
3405
3566
|
// artifact signature manifest
|
|
3406
3567
|
".stackwright/questions/",
|
|
3407
|
-
".stackwright/answers/"
|
|
3568
|
+
".stackwright/answers/",
|
|
3569
|
+
".stackwright/page-registry.json"
|
|
3570
|
+
// page ownership — managed by safe_write internally
|
|
3408
3571
|
];
|
|
3409
3572
|
var MAX_SAFE_WRITE_BYTES_JSON = 512 * 1024;
|
|
3410
3573
|
var MAX_SAFE_WRITE_BYTES_YAML = 256 * 1024;
|
|
@@ -3418,6 +3581,58 @@ function getMaxBytesForPath(filePath) {
|
|
|
3418
3581
|
return { limit: MAX_SAFE_WRITE_BYTES_ENV, label: "env" };
|
|
3419
3582
|
return { limit: MAX_SAFE_WRITE_BYTES_DEFAULT, label: "default" };
|
|
3420
3583
|
}
|
|
3584
|
+
var PAGE_REGISTRY_FILE = ".stackwright/page-registry.json";
|
|
3585
|
+
function extractPageSlug(filePath) {
|
|
3586
|
+
const match = (0, import_path6.normalize)(filePath).match(/^pages\/(.+?)\/content\.ya?ml$/);
|
|
3587
|
+
return match?.[1] ?? null;
|
|
3588
|
+
}
|
|
3589
|
+
function extractContentTypes(yamlContent) {
|
|
3590
|
+
const typePattern = /^\s*-?\s*type:\s*(\S+)/gm;
|
|
3591
|
+
const types = [];
|
|
3592
|
+
let m;
|
|
3593
|
+
while ((m = typePattern.exec(yamlContent)) !== null) {
|
|
3594
|
+
const typeName = m[1];
|
|
3595
|
+
if (typeName && !types.includes(typeName)) {
|
|
3596
|
+
types.push(typeName);
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
return types.length > 0 ? types : ["unknown"];
|
|
3600
|
+
}
|
|
3601
|
+
function readPageRegistry(cwd) {
|
|
3602
|
+
const regPath = (0, import_path6.join)(cwd, PAGE_REGISTRY_FILE);
|
|
3603
|
+
if (!(0, import_fs6.existsSync)(regPath)) return {};
|
|
3604
|
+
try {
|
|
3605
|
+
const stat = (0, import_fs6.lstatSync)(regPath);
|
|
3606
|
+
if (stat.isSymbolicLink()) return {};
|
|
3607
|
+
return JSON.parse((0, import_fs6.readFileSync)(regPath, "utf-8"));
|
|
3608
|
+
} catch {
|
|
3609
|
+
return {};
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
function writePageRegistry(cwd, registry) {
|
|
3613
|
+
const dir = (0, import_path6.join)(cwd, ".stackwright");
|
|
3614
|
+
(0, import_fs6.mkdirSync)(dir, { recursive: true });
|
|
3615
|
+
const regPath = (0, import_path6.join)(cwd, PAGE_REGISTRY_FILE);
|
|
3616
|
+
if ((0, import_fs6.existsSync)(regPath)) {
|
|
3617
|
+
const stat = (0, import_fs6.lstatSync)(regPath);
|
|
3618
|
+
if (stat.isSymbolicLink()) {
|
|
3619
|
+
throw new Error("Refusing to write page registry through symlink");
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
(0, import_fs6.writeFileSync)(regPath, JSON.stringify(registry, null, 2) + "\n", { encoding: "utf-8" });
|
|
3623
|
+
}
|
|
3624
|
+
function checkPageOwnership(cwd, slug, callerOtter) {
|
|
3625
|
+
const registry = readPageRegistry(cwd);
|
|
3626
|
+
const existing = registry[slug];
|
|
3627
|
+
if (!existing || existing.claimedBy === callerOtter) {
|
|
3628
|
+
return { allowed: true };
|
|
3629
|
+
}
|
|
3630
|
+
return {
|
|
3631
|
+
allowed: false,
|
|
3632
|
+
owner: existing.claimedBy,
|
|
3633
|
+
contentTypes: existing.contentTypes
|
|
3634
|
+
};
|
|
3635
|
+
}
|
|
3421
3636
|
function checkPathAllowed(callerOtter, filePath) {
|
|
3422
3637
|
const normalized = (0, import_path6.normalize)(filePath);
|
|
3423
3638
|
if (normalized.includes("..")) {
|
|
@@ -3459,6 +3674,16 @@ function checkPathAllowed(callerOtter, filePath) {
|
|
|
3459
3674
|
continue;
|
|
3460
3675
|
}
|
|
3461
3676
|
}
|
|
3677
|
+
if (rule.prefix === "lib/mock-auth" && rule.suffix === ".ts") {
|
|
3678
|
+
if (normalized !== "lib/mock-auth.ts") {
|
|
3679
|
+
continue;
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
if (rule.prefix === "package.json" && rule.suffix === ".json") {
|
|
3683
|
+
if (normalized !== "package.json") {
|
|
3684
|
+
continue;
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3462
3687
|
return { allowed: true, rule: rule.description };
|
|
3463
3688
|
}
|
|
3464
3689
|
}
|
|
@@ -3584,11 +3809,38 @@ function handleSafeWrite(input) {
|
|
|
3584
3809
|
};
|
|
3585
3810
|
return { text: JSON.stringify(result), isError: true };
|
|
3586
3811
|
}
|
|
3812
|
+
const pageSlug = extractPageSlug(normalized);
|
|
3813
|
+
if (pageSlug) {
|
|
3814
|
+
const ownership = checkPageOwnership(cwd, pageSlug, callerOtter);
|
|
3815
|
+
if (!ownership.allowed) {
|
|
3816
|
+
const result = {
|
|
3817
|
+
success: false,
|
|
3818
|
+
error: `Page slug "${pageSlug}" is already claimed by ${ownership.owner} (content types: ${ownership.contentTypes.join(", ")}). Use a different slug or coordinate with the owning otter.`,
|
|
3819
|
+
callerOtter,
|
|
3820
|
+
attemptedPath: filePath,
|
|
3821
|
+
allowedPaths: []
|
|
3822
|
+
};
|
|
3823
|
+
return { text: JSON.stringify(result), isError: true };
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3587
3826
|
try {
|
|
3588
3827
|
if (createDirectories) {
|
|
3589
3828
|
(0, import_fs6.mkdirSync)((0, import_path6.dirname)(fullPath), { recursive: true });
|
|
3590
3829
|
}
|
|
3591
3830
|
(0, import_fs6.writeFileSync)(fullPath, content, { encoding: "utf-8" });
|
|
3831
|
+
if (pageSlug) {
|
|
3832
|
+
try {
|
|
3833
|
+
const registry = readPageRegistry(cwd);
|
|
3834
|
+
registry[pageSlug] = {
|
|
3835
|
+
slug: pageSlug,
|
|
3836
|
+
claimedBy: callerOtter,
|
|
3837
|
+
contentTypes: extractContentTypes(content),
|
|
3838
|
+
writtenAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3839
|
+
};
|
|
3840
|
+
writePageRegistry(cwd, registry);
|
|
3841
|
+
} catch {
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3592
3844
|
const result = {
|
|
3593
3845
|
success: true,
|
|
3594
3846
|
path: normalized,
|
|
@@ -3855,10 +4107,232 @@ ${routeLines}
|
|
|
3855
4107
|
${auditSection}
|
|
3856
4108
|
`;
|
|
3857
4109
|
}
|
|
4110
|
+
function generateDevOnlyMiddlewareContent(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
|
|
4111
|
+
const rbacBlock = ` rbac: {
|
|
4112
|
+
roles: ${JSON.stringify(roles)},
|
|
4113
|
+
defaultRole: '${defaultRole}',
|
|
4114
|
+
hierarchy: ${JSON.stringify(hierarchy, null, 4)},
|
|
4115
|
+
},`;
|
|
4116
|
+
const auditBlock = ` audit: {
|
|
4117
|
+
enabled: ${auditEnabled},
|
|
4118
|
+
retentionDays: ${auditRetentionDays},
|
|
4119
|
+
},`;
|
|
4120
|
+
const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
|
|
4121
|
+
const configBlock = `export const config = {
|
|
4122
|
+
matcher: ${JSON.stringify(protectedRoutes)},
|
|
4123
|
+
};`;
|
|
4124
|
+
const devHeader = [
|
|
4125
|
+
"// middleware.ts \u2014 generated by @stackwright-pro/auth-nextjs (dev-only mock)",
|
|
4126
|
+
"// DEV ONLY \u2014 uses mock authentication from lib/mock-auth.ts",
|
|
4127
|
+
"// Do NOT deploy to production.",
|
|
4128
|
+
"import { createProMiddleware } from '@stackwright-pro/auth-nextjs';",
|
|
4129
|
+
"import { mockAuthProvider } from './lib/mock-auth';"
|
|
4130
|
+
].join("\n");
|
|
4131
|
+
if (method === "cac") {
|
|
4132
|
+
const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
|
|
4133
|
+
const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
|
|
4134
|
+
const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
|
|
4135
|
+
const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
|
|
4136
|
+
return `${devHeader}
|
|
4137
|
+
|
|
4138
|
+
export const middleware = createProMiddleware({
|
|
4139
|
+
method: 'cac',
|
|
4140
|
+
cac: {
|
|
4141
|
+
caBundle: '${caBundle}',
|
|
4142
|
+
edipiLookup: '${edipiLookup}',
|
|
4143
|
+
ocspEndpoint: '${ocspEndpoint}',
|
|
4144
|
+
certHeader: '${certHeader}',
|
|
4145
|
+
provider: mockAuthProvider,
|
|
4146
|
+
},
|
|
4147
|
+
${rbacBlock}
|
|
4148
|
+
${auditBlock}
|
|
4149
|
+
${routesBlock}
|
|
4150
|
+
});
|
|
4151
|
+
|
|
4152
|
+
${configBlock}
|
|
4153
|
+
`;
|
|
4154
|
+
}
|
|
4155
|
+
if (method === "oidc") {
|
|
4156
|
+
const scopes2 = params.oidcScopes ?? "openid profile email";
|
|
4157
|
+
const roleClaim = params.oidcRoleClaim ?? "roles";
|
|
4158
|
+
return `${devHeader}
|
|
4159
|
+
|
|
4160
|
+
export const middleware = createProMiddleware({
|
|
4161
|
+
method: 'oidc',
|
|
4162
|
+
oidc: {
|
|
4163
|
+
discoveryUrl: 'https://dev-mock-oidc/.well-known/openid-configuration',
|
|
4164
|
+
clientId: 'dev-mock-client',
|
|
4165
|
+
clientSecret: 'dev-mock-secret',
|
|
4166
|
+
scopes: '${scopes2}',
|
|
4167
|
+
roleClaim: '${roleClaim}',
|
|
4168
|
+
provider: mockAuthProvider,
|
|
4169
|
+
},
|
|
4170
|
+
${rbacBlock}
|
|
4171
|
+
${auditBlock}
|
|
4172
|
+
${routesBlock}
|
|
4173
|
+
});
|
|
4174
|
+
|
|
4175
|
+
${configBlock}
|
|
4176
|
+
`;
|
|
4177
|
+
}
|
|
4178
|
+
const scopes = params.oauth2Scopes ?? "read write";
|
|
4179
|
+
return `${devHeader}
|
|
4180
|
+
|
|
4181
|
+
export const middleware = createProMiddleware({
|
|
4182
|
+
method: 'oauth2',
|
|
4183
|
+
oauth2: {
|
|
4184
|
+
authorizationUrl: 'https://dev-mock-oauth2/authorize',
|
|
4185
|
+
tokenUrl: 'https://dev-mock-oauth2/token',
|
|
4186
|
+
clientId: 'dev-mock-client',
|
|
4187
|
+
clientSecret: 'dev-mock-secret',
|
|
4188
|
+
scopes: '${scopes}',
|
|
4189
|
+
provider: mockAuthProvider,
|
|
4190
|
+
},
|
|
4191
|
+
${rbacBlock}
|
|
4192
|
+
${auditBlock}
|
|
4193
|
+
${routesBlock}
|
|
4194
|
+
});
|
|
4195
|
+
|
|
4196
|
+
${configBlock}
|
|
4197
|
+
`;
|
|
4198
|
+
}
|
|
4199
|
+
function generateDevOnlyYamlBlock(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
|
|
4200
|
+
const rbacSection = ` rbac:
|
|
4201
|
+
roles:
|
|
4202
|
+
${rolesToYaml(roles, " ")}
|
|
4203
|
+
defaultRole: ${defaultRole}
|
|
4204
|
+
hierarchy:
|
|
4205
|
+
${hierarchyToYaml(hierarchy, " ")}`;
|
|
4206
|
+
const auditSection = ` audit:
|
|
4207
|
+
enabled: ${auditEnabled}
|
|
4208
|
+
retentionDays: ${auditRetentionDays}`;
|
|
4209
|
+
const routeLines = protectedRoutes.map((r) => ` - pattern: ${r}
|
|
4210
|
+
requiredRole: ${defaultRole}`).join("\n");
|
|
4211
|
+
const providerLine = params.provider ? ` provider: ${params.provider}
|
|
4212
|
+
` : "";
|
|
4213
|
+
if (method === "cac") {
|
|
4214
|
+
const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
|
|
4215
|
+
const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
|
|
4216
|
+
const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
|
|
4217
|
+
const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
|
|
4218
|
+
return `auth:
|
|
4219
|
+
method: cac
|
|
4220
|
+
devOnly: true
|
|
4221
|
+
${providerLine} middleware: ./middleware.ts
|
|
4222
|
+
cac:
|
|
4223
|
+
caBundle: ${caBundle}
|
|
4224
|
+
edipiLookup: ${edipiLookup}
|
|
4225
|
+
ocspEndpoint: ${ocspEndpoint}
|
|
4226
|
+
certHeader: ${certHeader}
|
|
4227
|
+
${rbacSection}
|
|
4228
|
+
protectedRoutes:
|
|
4229
|
+
${routeLines}
|
|
4230
|
+
${auditSection}
|
|
4231
|
+
`;
|
|
4232
|
+
}
|
|
4233
|
+
if (method === "oidc") {
|
|
4234
|
+
const scopes2 = params.oidcScopes ?? "openid profile email";
|
|
4235
|
+
const roleClaim = params.oidcRoleClaim ?? "roles";
|
|
4236
|
+
return `auth:
|
|
4237
|
+
method: oidc
|
|
4238
|
+
devOnly: true
|
|
4239
|
+
${providerLine} middleware: ./middleware.ts
|
|
4240
|
+
oidc:
|
|
4241
|
+
discoveryUrl: https://dev-mock-oidc/.well-known/openid-configuration
|
|
4242
|
+
clientId: dev-mock-client
|
|
4243
|
+
clientSecret: dev-mock-secret
|
|
4244
|
+
scopes: ${scopes2}
|
|
4245
|
+
roleClaim: ${roleClaim}
|
|
4246
|
+
${rbacSection}
|
|
4247
|
+
protectedRoutes:
|
|
4248
|
+
${routeLines}
|
|
4249
|
+
${auditSection}
|
|
4250
|
+
`;
|
|
4251
|
+
}
|
|
4252
|
+
const scopes = params.oauth2Scopes ?? "read write";
|
|
4253
|
+
return `auth:
|
|
4254
|
+
method: oauth2
|
|
4255
|
+
devOnly: true
|
|
4256
|
+
${providerLine} middleware: ./middleware.ts
|
|
4257
|
+
oauth2:
|
|
4258
|
+
authorizationUrl: https://dev-mock-oauth2/authorize
|
|
4259
|
+
tokenUrl: https://dev-mock-oauth2/token
|
|
4260
|
+
clientId: dev-mock-client
|
|
4261
|
+
clientSecret: dev-mock-secret
|
|
4262
|
+
scopes: ${scopes}
|
|
4263
|
+
${rbacSection}
|
|
4264
|
+
protectedRoutes:
|
|
4265
|
+
${routeLines}
|
|
4266
|
+
${auditSection}
|
|
4267
|
+
`;
|
|
4268
|
+
}
|
|
4269
|
+
function deriveDevKey(roleName) {
|
|
4270
|
+
const segment = roleName.split("_")[0];
|
|
4271
|
+
return (segment ?? roleName).toLowerCase();
|
|
4272
|
+
}
|
|
4273
|
+
function generateMockAuthContent(roles, mockUsers) {
|
|
4274
|
+
const entries = [];
|
|
4275
|
+
const scriptLines = [];
|
|
4276
|
+
for (let i = 0; i < roles.length; i++) {
|
|
4277
|
+
const role = roles[i];
|
|
4278
|
+
const devKey = deriveDevKey(role);
|
|
4279
|
+
const persona = mockUsers?.[i];
|
|
4280
|
+
const name = persona?.name ?? `Dev ${role}`;
|
|
4281
|
+
const email = persona?.email ?? `dev-${devKey}@example.mil`;
|
|
4282
|
+
const edipi = String(i + 1).padStart(10, "0");
|
|
4283
|
+
entries.push(` ${devKey}: {
|
|
4284
|
+
id: 'mock-${devKey}-${String(i + 1).padStart(3, "0")}',
|
|
4285
|
+
name: '${name}',
|
|
4286
|
+
email: '${email}',
|
|
4287
|
+
roles: ['${role}'],
|
|
4288
|
+
edipi: '${edipi}',
|
|
4289
|
+
}`);
|
|
4290
|
+
scriptLines.push(` * pnpm dev:${devKey} \u2014 ${name}`);
|
|
4291
|
+
}
|
|
4292
|
+
return `/**
|
|
4293
|
+
* Mock authentication for development mode.
|
|
4294
|
+
*
|
|
4295
|
+
* DEV_ONLY_MODE \u2014 no real auth provider. Select a persona via MOCK_USER env var
|
|
4296
|
+
* or use the convenience scripts in package.json:
|
|
4297
|
+
${scriptLines.join("\n")}
|
|
4298
|
+
*/
|
|
4299
|
+
|
|
4300
|
+
export const MOCK_USERS = {
|
|
4301
|
+
${entries.join(",\n")}
|
|
4302
|
+
} as const;
|
|
4303
|
+
|
|
4304
|
+
export type MockRole = keyof typeof MOCK_USERS;
|
|
4305
|
+
|
|
4306
|
+
export function getMockUser(role?: string) {
|
|
4307
|
+
const key = (role ?? process.env.MOCK_USER) as MockRole | undefined;
|
|
4308
|
+
if (!key) return null;
|
|
4309
|
+
return MOCK_USERS[key] ?? null;
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4312
|
+
export function mockAuthProvider() {
|
|
4313
|
+
return getMockUser();
|
|
4314
|
+
}
|
|
4315
|
+
`;
|
|
4316
|
+
}
|
|
4317
|
+
function updatePackageJsonScripts(existingJson, roles, mockUsers) {
|
|
4318
|
+
const pkg = JSON.parse(existingJson);
|
|
4319
|
+
const scripts = pkg.scripts ?? {};
|
|
4320
|
+
delete scripts["dev:admin"];
|
|
4321
|
+
delete scripts["dev:analyst"];
|
|
4322
|
+
delete scripts["dev:viewer"];
|
|
4323
|
+
for (let i = 0; i < roles.length; i++) {
|
|
4324
|
+
const devKey = deriveDevKey(roles[i]);
|
|
4325
|
+
scripts[`dev:${devKey}`] = `MOCK_USER=${devKey} next dev`;
|
|
4326
|
+
}
|
|
4327
|
+
pkg.scripts = scripts;
|
|
4328
|
+
return JSON.stringify(pkg, null, 2) + "\n";
|
|
4329
|
+
}
|
|
3858
4330
|
async function configureAuthHandler(params, cwd) {
|
|
3859
4331
|
const {
|
|
3860
4332
|
method,
|
|
3861
4333
|
provider,
|
|
4334
|
+
devOnly = false,
|
|
4335
|
+
mockUsers,
|
|
3862
4336
|
rbacRoles = ["SUPER_ADMIN", "ADMIN", "ANALYST"],
|
|
3863
4337
|
auditEnabled = true,
|
|
3864
4338
|
auditRetentionDays = 90,
|
|
@@ -3888,7 +4362,16 @@ async function configureAuthHandler(params, cwd) {
|
|
|
3888
4362
|
}
|
|
3889
4363
|
const filesWritten = [];
|
|
3890
4364
|
try {
|
|
3891
|
-
const middlewareContent =
|
|
4365
|
+
const middlewareContent = devOnly ? generateDevOnlyMiddlewareContent(
|
|
4366
|
+
method,
|
|
4367
|
+
params,
|
|
4368
|
+
roles,
|
|
4369
|
+
defaultRole,
|
|
4370
|
+
hierarchy,
|
|
4371
|
+
auditEnabled,
|
|
4372
|
+
auditRetentionDays,
|
|
4373
|
+
protectedRoutes
|
|
4374
|
+
) : generateMiddlewareContent(
|
|
3892
4375
|
method,
|
|
3893
4376
|
params,
|
|
3894
4377
|
roles,
|
|
@@ -3912,30 +4395,87 @@ async function configureAuthHandler(params, cwd) {
|
|
|
3912
4395
|
isError: true
|
|
3913
4396
|
};
|
|
3914
4397
|
}
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
4398
|
+
if (!devOnly) {
|
|
4399
|
+
try {
|
|
4400
|
+
const envBlock = generateEnvBlock(method, params);
|
|
4401
|
+
const envPath = (0, import_path7.join)(cwd, ".env.example");
|
|
4402
|
+
if ((0, import_fs7.existsSync)(envPath)) {
|
|
4403
|
+
const existing = (0, import_fs7.readFileSync)(envPath, "utf8");
|
|
4404
|
+
(0, import_fs7.writeFileSync)(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
|
|
4405
|
+
} else {
|
|
4406
|
+
(0, import_fs7.writeFileSync)(envPath, envBlock, "utf8");
|
|
4407
|
+
}
|
|
4408
|
+
filesWritten.push(".env.example");
|
|
4409
|
+
} catch (err) {
|
|
4410
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4411
|
+
return {
|
|
4412
|
+
content: [
|
|
4413
|
+
{
|
|
4414
|
+
type: "text",
|
|
4415
|
+
text: JSON.stringify({ success: false, error: `Failed writing .env.example: ${msg}` })
|
|
4416
|
+
}
|
|
4417
|
+
],
|
|
4418
|
+
isError: true
|
|
4419
|
+
};
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
if (devOnly) {
|
|
4423
|
+
try {
|
|
4424
|
+
const mockAuthDir = (0, import_path7.join)(cwd, "lib");
|
|
4425
|
+
(0, import_fs7.mkdirSync)(mockAuthDir, { recursive: true });
|
|
4426
|
+
const mockAuthContent = generateMockAuthContent(roles, mockUsers);
|
|
4427
|
+
(0, import_fs7.writeFileSync)((0, import_path7.join)(mockAuthDir, "mock-auth.ts"), mockAuthContent, "utf8");
|
|
4428
|
+
filesWritten.push("lib/mock-auth.ts");
|
|
4429
|
+
} catch (err) {
|
|
4430
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4431
|
+
return {
|
|
4432
|
+
content: [
|
|
4433
|
+
{
|
|
4434
|
+
type: "text",
|
|
4435
|
+
text: JSON.stringify({
|
|
4436
|
+
success: false,
|
|
4437
|
+
error: `Failed writing lib/mock-auth.ts: ${msg}`
|
|
4438
|
+
})
|
|
4439
|
+
}
|
|
4440
|
+
],
|
|
4441
|
+
isError: true
|
|
4442
|
+
};
|
|
4443
|
+
}
|
|
4444
|
+
try {
|
|
4445
|
+
const pkgPath = (0, import_path7.join)(cwd, "package.json");
|
|
4446
|
+
if ((0, import_fs7.existsSync)(pkgPath)) {
|
|
4447
|
+
const existingPkg = (0, import_fs7.readFileSync)(pkgPath, "utf8");
|
|
4448
|
+
const updatedPkg = updatePackageJsonScripts(existingPkg, roles, mockUsers);
|
|
4449
|
+
(0, import_fs7.writeFileSync)(pkgPath, updatedPkg, "utf8");
|
|
4450
|
+
filesWritten.push("package.json");
|
|
4451
|
+
}
|
|
4452
|
+
} catch (err) {
|
|
4453
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4454
|
+
return {
|
|
4455
|
+
content: [
|
|
4456
|
+
{
|
|
4457
|
+
type: "text",
|
|
4458
|
+
text: JSON.stringify({
|
|
4459
|
+
success: false,
|
|
4460
|
+
error: `Failed updating package.json: ${msg}`
|
|
4461
|
+
})
|
|
4462
|
+
}
|
|
4463
|
+
],
|
|
4464
|
+
isError: true
|
|
4465
|
+
};
|
|
3923
4466
|
}
|
|
3924
|
-
filesWritten.push(".env.example");
|
|
3925
|
-
} catch (err) {
|
|
3926
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
3927
|
-
return {
|
|
3928
|
-
content: [
|
|
3929
|
-
{
|
|
3930
|
-
type: "text",
|
|
3931
|
-
text: JSON.stringify({ success: false, error: `Failed writing .env.example: ${msg}` })
|
|
3932
|
-
}
|
|
3933
|
-
],
|
|
3934
|
-
isError: true
|
|
3935
|
-
};
|
|
3936
4467
|
}
|
|
3937
4468
|
try {
|
|
3938
|
-
const authYaml =
|
|
4469
|
+
const authYaml = devOnly ? generateDevOnlyYamlBlock(
|
|
4470
|
+
method,
|
|
4471
|
+
params,
|
|
4472
|
+
roles,
|
|
4473
|
+
defaultRole,
|
|
4474
|
+
hierarchy,
|
|
4475
|
+
auditEnabled,
|
|
4476
|
+
auditRetentionDays,
|
|
4477
|
+
protectedRoutes
|
|
4478
|
+
) : generateYamlBlock(
|
|
3939
4479
|
method,
|
|
3940
4480
|
params,
|
|
3941
4481
|
roles,
|
|
@@ -3978,7 +4518,21 @@ async function configureAuthHandler(params, cwd) {
|
|
|
3978
4518
|
rbacDefaultRole: defaultRole,
|
|
3979
4519
|
protectedRoutesCount: protectedRoutes.length,
|
|
3980
4520
|
filesWritten,
|
|
3981
|
-
securityWarning
|
|
4521
|
+
securityWarning,
|
|
4522
|
+
...devOnly ? {
|
|
4523
|
+
devScripts: {
|
|
4524
|
+
written: filesWritten.includes("package.json"),
|
|
4525
|
+
scripts: filesWritten.includes("package.json") ? Object.fromEntries(
|
|
4526
|
+
roles.map((r) => {
|
|
4527
|
+
const k = deriveDevKey(r);
|
|
4528
|
+
return [`dev:${k}`, `MOCK_USER=${k} next dev`];
|
|
4529
|
+
})
|
|
4530
|
+
) : {},
|
|
4531
|
+
...filesWritten.includes("package.json") ? {} : {
|
|
4532
|
+
warning: "package.json not found \u2014 dev scripts were not written. Run the auth phase again after scaffolding creates package.json."
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
} : {}
|
|
3982
4536
|
})
|
|
3983
4537
|
}
|
|
3984
4538
|
]
|
|
@@ -4017,7 +4571,9 @@ function registerAuthTools(server2) {
|
|
|
4017
4571
|
// Routes
|
|
4018
4572
|
protectedRoutes: jsonCoerce(import_zod13.z.array(import_zod13.z.string()).optional()),
|
|
4019
4573
|
// Injection for tests
|
|
4020
|
-
_cwd: import_zod13.z.string().optional()
|
|
4574
|
+
_cwd: import_zod13.z.string().optional(),
|
|
4575
|
+
devOnly: boolCoerce(import_zod13.z.boolean().optional()),
|
|
4576
|
+
mockUsers: jsonCoerce(import_zod13.z.array(import_zod13.z.object({ name: import_zod13.z.string(), email: import_zod13.z.string() })).optional())
|
|
4021
4577
|
},
|
|
4022
4578
|
async (params) => {
|
|
4023
4579
|
const cwd = params._cwd ?? process.cwd();
|
|
@@ -4037,15 +4593,15 @@ var _checksums = /* @__PURE__ */ new Map([
|
|
|
4037
4593
|
],
|
|
4038
4594
|
[
|
|
4039
4595
|
"stackwright-pro-auth-otter.json",
|
|
4040
|
-
"
|
|
4596
|
+
"c3be215f05cc48ec51ed75541184b89a8ac123463b80fe02f2adb6c1b5163853"
|
|
4041
4597
|
],
|
|
4042
4598
|
[
|
|
4043
4599
|
"stackwright-pro-dashboard-otter.json",
|
|
4044
|
-
"
|
|
4600
|
+
"9c319d311801730e8dc9bc142eebb8fc5a7f48da48fa0b8d8c3b7431652447be"
|
|
4045
4601
|
],
|
|
4046
4602
|
[
|
|
4047
4603
|
"stackwright-pro-data-otter.json",
|
|
4048
|
-
"
|
|
4604
|
+
"4d9369277685a4acc484116920c9622ad8a1838012a493fcfe42a6ae5abe53cf"
|
|
4049
4605
|
],
|
|
4050
4606
|
[
|
|
4051
4607
|
"stackwright-pro-designer-otter.json",
|
|
@@ -4053,35 +4609,35 @@ var _checksums = /* @__PURE__ */ new Map([
|
|
|
4053
4609
|
],
|
|
4054
4610
|
[
|
|
4055
4611
|
"stackwright-pro-domain-expert-otter.json",
|
|
4056
|
-
"
|
|
4612
|
+
"41e3a5838f05f6a51ed272860dd2d5b1df1f20bfc847eb8f39be109b89738e99"
|
|
4057
4613
|
],
|
|
4058
4614
|
[
|
|
4059
4615
|
"stackwright-pro-foreman-otter.json",
|
|
4060
|
-
"
|
|
4616
|
+
"5f18e37ee3f2064c62a2d01dfc541285f27e5762d0b1011e2a37a48af96c74c8"
|
|
4061
4617
|
],
|
|
4062
4618
|
[
|
|
4063
4619
|
"stackwright-pro-geo-otter.json",
|
|
4064
|
-
"
|
|
4620
|
+
"ff1191af8108cc70c09afe6a99b008dcb46f71e20f884da1ee424e431868a1b6"
|
|
4065
4621
|
],
|
|
4066
4622
|
[
|
|
4067
4623
|
"stackwright-pro-page-otter.json",
|
|
4068
|
-
"
|
|
4624
|
+
"1dcdf866be76d18f3aa2a9e56b8bf57a3d43477b43c410eb6d1feac0e7683658"
|
|
4069
4625
|
],
|
|
4070
4626
|
[
|
|
4071
4627
|
"stackwright-pro-polish-otter.json",
|
|
4072
|
-
"
|
|
4628
|
+
"72a52901b4781c9f0be97024ba6e19415cdfbd92369d9274aa3abe7efd208bc8"
|
|
4073
4629
|
],
|
|
4074
4630
|
[
|
|
4075
4631
|
"stackwright-pro-theme-otter.json",
|
|
4076
|
-
"
|
|
4632
|
+
"e8530a1146abc57eb31d2f036ba057d5eae7d4a2d9dca00e415d1c352ef4c5b5"
|
|
4077
4633
|
],
|
|
4078
4634
|
[
|
|
4079
4635
|
"stackwright-pro-workflow-otter.json",
|
|
4080
|
-
"
|
|
4636
|
+
"e02c1a5f492dd6b11f68e293d92f5f661dbb22e57570dcf133a9d0ffc7a8be7d"
|
|
4081
4637
|
],
|
|
4082
4638
|
[
|
|
4083
4639
|
"stackwright-services-otter.json",
|
|
4084
|
-
"
|
|
4640
|
+
"b8a252d6a2c5090138899925d3ea95bf3339b6e49adc276a1f5f7304b4ae5134"
|
|
4085
4641
|
]
|
|
4086
4642
|
]);
|
|
4087
4643
|
Object.freeze(_checksums);
|
|
@@ -4791,6 +5347,182 @@ function registerTypeSchemasTool(server2) {
|
|
|
4791
5347
|
);
|
|
4792
5348
|
}
|
|
4793
5349
|
|
|
5350
|
+
// src/tools/scaffold-cleanup.ts
|
|
5351
|
+
var import_fs10 = require("fs");
|
|
5352
|
+
var import_path10 = require("path");
|
|
5353
|
+
var SCAFFOLD_FILES_TO_DELETE = [
|
|
5354
|
+
"content/posts/getting-started.yaml",
|
|
5355
|
+
"content/posts/hello-world.yaml"
|
|
5356
|
+
];
|
|
5357
|
+
var SCAFFOLD_DIRS_TO_PRUNE = [
|
|
5358
|
+
"content/posts"
|
|
5359
|
+
// Don't prune 'content' — user may have other content directories
|
|
5360
|
+
];
|
|
5361
|
+
var NOT_FOUND_PATH = "app/not-found.tsx";
|
|
5362
|
+
var FALLBACK_COLORS = {
|
|
5363
|
+
background: "#ffffff",
|
|
5364
|
+
foreground: "#171717",
|
|
5365
|
+
primary: "#525252",
|
|
5366
|
+
mutedForeground: "#737373"
|
|
5367
|
+
};
|
|
5368
|
+
function readThemeColors(cwd) {
|
|
5369
|
+
const tokenPath = (0, import_path10.join)(cwd, ".stackwright", "artifacts", "theme-tokens.json");
|
|
5370
|
+
if (!(0, import_fs10.existsSync)(tokenPath)) return { ...FALLBACK_COLORS };
|
|
5371
|
+
const stat = (0, import_fs10.lstatSync)(tokenPath);
|
|
5372
|
+
if (stat.isSymbolicLink()) return { ...FALLBACK_COLORS };
|
|
5373
|
+
try {
|
|
5374
|
+
const raw = JSON.parse((0, import_fs10.readFileSync)(tokenPath, "utf-8"));
|
|
5375
|
+
const css = raw?.cssVariables ?? {};
|
|
5376
|
+
const toHsl = (hslValue) => {
|
|
5377
|
+
if (!hslValue || typeof hslValue !== "string") return null;
|
|
5378
|
+
return `hsl(${hslValue})`;
|
|
5379
|
+
};
|
|
5380
|
+
return {
|
|
5381
|
+
background: toHsl(css["--background"]) ?? FALLBACK_COLORS.background,
|
|
5382
|
+
foreground: toHsl(css["--foreground"]) ?? FALLBACK_COLORS.foreground,
|
|
5383
|
+
primary: toHsl(css["--primary"]) ?? FALLBACK_COLORS.primary,
|
|
5384
|
+
mutedForeground: toHsl(css["--muted-foreground"]) ?? FALLBACK_COLORS.mutedForeground
|
|
5385
|
+
};
|
|
5386
|
+
} catch {
|
|
5387
|
+
return { ...FALLBACK_COLORS };
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
function generateNotFoundPage(colors) {
|
|
5391
|
+
return `export default function NotFound() {
|
|
5392
|
+
return (
|
|
5393
|
+
<div
|
|
5394
|
+
style={{
|
|
5395
|
+
display: 'flex',
|
|
5396
|
+
flexDirection: 'column',
|
|
5397
|
+
alignItems: 'center',
|
|
5398
|
+
justifyContent: 'center',
|
|
5399
|
+
minHeight: '100vh',
|
|
5400
|
+
backgroundColor: '${colors.background}',
|
|
5401
|
+
color: '${colors.foreground}',
|
|
5402
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
5403
|
+
padding: '2rem',
|
|
5404
|
+
textAlign: 'center',
|
|
5405
|
+
}}
|
|
5406
|
+
>
|
|
5407
|
+
<h1 style={{ fontSize: '4rem', fontWeight: 700, margin: 0, color: '${colors.primary}' }}>
|
|
5408
|
+
404
|
|
5409
|
+
</h1>
|
|
5410
|
+
<p style={{ fontSize: '1.25rem', color: '${colors.mutedForeground}', marginTop: '0.5rem' }}>
|
|
5411
|
+
Page not found
|
|
5412
|
+
</p>
|
|
5413
|
+
<a
|
|
5414
|
+
href="/"
|
|
5415
|
+
style={{
|
|
5416
|
+
marginTop: '2rem',
|
|
5417
|
+
padding: '0.75rem 1.5rem',
|
|
5418
|
+
backgroundColor: '${colors.primary}',
|
|
5419
|
+
color: '${colors.background}',
|
|
5420
|
+
borderRadius: '0.5rem',
|
|
5421
|
+
textDecoration: 'none',
|
|
5422
|
+
fontSize: '0.875rem',
|
|
5423
|
+
fontWeight: 500,
|
|
5424
|
+
}}
|
|
5425
|
+
>
|
|
5426
|
+
Go Home
|
|
5427
|
+
</a>
|
|
5428
|
+
</div>
|
|
5429
|
+
);
|
|
5430
|
+
}
|
|
5431
|
+
`;
|
|
5432
|
+
}
|
|
5433
|
+
function isSafePath(fullPath) {
|
|
5434
|
+
if (!(0, import_fs10.existsSync)(fullPath)) return true;
|
|
5435
|
+
const stat = (0, import_fs10.lstatSync)(fullPath);
|
|
5436
|
+
return !stat.isSymbolicLink();
|
|
5437
|
+
}
|
|
5438
|
+
function isDirEmpty(dirPath) {
|
|
5439
|
+
if (!(0, import_fs10.existsSync)(dirPath)) return false;
|
|
5440
|
+
const stat = (0, import_fs10.lstatSync)(dirPath);
|
|
5441
|
+
if (!stat.isDirectory()) return false;
|
|
5442
|
+
return (0, import_fs10.readdirSync)(dirPath).length === 0;
|
|
5443
|
+
}
|
|
5444
|
+
function handleCleanupScaffold(_cwd) {
|
|
5445
|
+
const cwd = _cwd ?? process.cwd();
|
|
5446
|
+
const result = {
|
|
5447
|
+
deleted: [],
|
|
5448
|
+
rewritten: [],
|
|
5449
|
+
skipped: [],
|
|
5450
|
+
errors: [],
|
|
5451
|
+
prunedDirs: []
|
|
5452
|
+
};
|
|
5453
|
+
for (const relPath of SCAFFOLD_FILES_TO_DELETE) {
|
|
5454
|
+
const fullPath = (0, import_path10.join)(cwd, relPath);
|
|
5455
|
+
if (!(0, import_fs10.existsSync)(fullPath)) {
|
|
5456
|
+
result.skipped.push(relPath);
|
|
5457
|
+
continue;
|
|
5458
|
+
}
|
|
5459
|
+
if (!isSafePath(fullPath)) {
|
|
5460
|
+
result.errors.push(`Refusing to delete symlink: ${relPath}`);
|
|
5461
|
+
continue;
|
|
5462
|
+
}
|
|
5463
|
+
try {
|
|
5464
|
+
(0, import_fs10.unlinkSync)(fullPath);
|
|
5465
|
+
result.deleted.push(relPath);
|
|
5466
|
+
} catch (err) {
|
|
5467
|
+
result.errors.push(`Failed to delete ${relPath}: ${String(err)}`);
|
|
5468
|
+
}
|
|
5469
|
+
}
|
|
5470
|
+
for (const relDir of SCAFFOLD_DIRS_TO_PRUNE) {
|
|
5471
|
+
const fullDir = (0, import_path10.join)(cwd, relDir);
|
|
5472
|
+
if (!(0, import_fs10.existsSync)(fullDir)) continue;
|
|
5473
|
+
if (!isSafePath(fullDir)) {
|
|
5474
|
+
result.errors.push(`Refusing to prune symlink directory: ${relDir}`);
|
|
5475
|
+
continue;
|
|
5476
|
+
}
|
|
5477
|
+
if (isDirEmpty(fullDir)) {
|
|
5478
|
+
try {
|
|
5479
|
+
(0, import_fs10.rmdirSync)(fullDir);
|
|
5480
|
+
result.prunedDirs.push(relDir);
|
|
5481
|
+
} catch (err) {
|
|
5482
|
+
result.errors.push(`Failed to prune directory ${relDir}: ${String(err)}`);
|
|
5483
|
+
}
|
|
5484
|
+
}
|
|
5485
|
+
}
|
|
5486
|
+
{
|
|
5487
|
+
const fullPath = (0, import_path10.join)(cwd, NOT_FOUND_PATH);
|
|
5488
|
+
if (!(0, import_fs10.existsSync)(fullPath)) {
|
|
5489
|
+
result.skipped.push(NOT_FOUND_PATH);
|
|
5490
|
+
} else if (!isSafePath(fullPath)) {
|
|
5491
|
+
result.errors.push(`Refusing to rewrite symlink: ${NOT_FOUND_PATH}`);
|
|
5492
|
+
} else {
|
|
5493
|
+
try {
|
|
5494
|
+
const colors = readThemeColors(cwd);
|
|
5495
|
+
const content = generateNotFoundPage(colors);
|
|
5496
|
+
const dir = (0, import_path10.dirname)(fullPath);
|
|
5497
|
+
(0, import_fs10.mkdirSync)(dir, { recursive: true });
|
|
5498
|
+
(0, import_fs10.writeFileSync)(fullPath, content, "utf-8");
|
|
5499
|
+
result.rewritten.push(NOT_FOUND_PATH);
|
|
5500
|
+
} catch (err) {
|
|
5501
|
+
result.errors.push(`Failed to rewrite ${NOT_FOUND_PATH}: ${String(err)}`);
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
return {
|
|
5506
|
+
text: JSON.stringify(result),
|
|
5507
|
+
isError: false
|
|
5508
|
+
};
|
|
5509
|
+
}
|
|
5510
|
+
function registerScaffoldCleanupTools(server2) {
|
|
5511
|
+
const DESC = "Deterministic scaffold remnant cleanup \u2014 removes stale OSS scaffold files after raft run.";
|
|
5512
|
+
server2.tool(
|
|
5513
|
+
"stackwright_pro_cleanup_scaffold",
|
|
5514
|
+
`Remove/rewrite stale scaffold files (blog posts, hardcoded not-found page) after the raft pipeline completes. ${DESC}`,
|
|
5515
|
+
{},
|
|
5516
|
+
async () => {
|
|
5517
|
+
const r = handleCleanupScaffold();
|
|
5518
|
+
return {
|
|
5519
|
+
content: [{ type: "text", text: r.text }],
|
|
5520
|
+
isError: r.isError
|
|
5521
|
+
};
|
|
5522
|
+
}
|
|
5523
|
+
);
|
|
5524
|
+
}
|
|
5525
|
+
|
|
4794
5526
|
// package.json
|
|
4795
5527
|
var package_default = {
|
|
4796
5528
|
dependencies: {
|
|
@@ -4814,7 +5546,7 @@ var package_default = {
|
|
|
4814
5546
|
"test:coverage": "vitest run --coverage"
|
|
4815
5547
|
},
|
|
4816
5548
|
name: "@stackwright-pro/mcp",
|
|
4817
|
-
version: "0.2.0-alpha.
|
|
5549
|
+
version: "0.2.0-alpha.68",
|
|
4818
5550
|
description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
|
|
4819
5551
|
license: "SEE LICENSE IN LICENSE",
|
|
4820
5552
|
main: "./dist/server.js",
|
|
@@ -4868,6 +5600,7 @@ registerIntegrityTools(server);
|
|
|
4868
5600
|
registerArtifactSigningTools(server);
|
|
4869
5601
|
registerDomainTools(server);
|
|
4870
5602
|
registerTypeSchemasTool(server);
|
|
5603
|
+
registerScaffoldCleanupTools(server);
|
|
4871
5604
|
async function main() {
|
|
4872
5605
|
const transport = new import_stdio.StdioServerTransport();
|
|
4873
5606
|
try {
|