@wairon/cli 5.0.2-dev.1 → 5.0.2-dev.10
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/cli/index.js +659 -57
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +512 -46
- package/dist/index.js.map +1 -1
- package/dist/templates/skills/sdd-architect.md +1 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -65,7 +65,7 @@ var init_defaults = __esm({
|
|
|
65
65
|
copilot: ".github/prompts",
|
|
66
66
|
codex: ".codex/agents"
|
|
67
67
|
};
|
|
68
|
-
WAIRON_VERSION = "5.0.2-dev.
|
|
68
|
+
WAIRON_VERSION = "5.0.2-dev.10";
|
|
69
69
|
GITHUB_REPO = "SYW-Apps/Waffle-AIron";
|
|
70
70
|
ARCHITECT_AGENT_ID = "agent-architect";
|
|
71
71
|
ARCHITECT_TEMPLATE_ID = "architect";
|
|
@@ -703,7 +703,7 @@ var init_template = __esm({
|
|
|
703
703
|
});
|
|
704
704
|
|
|
705
705
|
// src/models/specs.ts
|
|
706
|
-
var import_zod6, SpecIdSchema, SpecStatusSchema, BoundaryItemSchema, RequirementItemSchema, DatabaseSpecSchema, DiagramConfigSchema, SURFACE_AUDIENCES, SurfaceAudienceSchema, SystemPublicInterfaceSchema, SystemSpecSchema, PublicInterfaceTypeSchema, PublicInterfaceSchema, TrustedLinkSchema, LintAllowSchema, LintConfigSchema, ExtDataSchema, LifecycleEntrypointSchema, SubsystemSpecSchema, ComponentTypeSchema, PATTERN_TYPES, PortalTypeSchema, DispatchBindingSchema, DurabilitySchema, PatternRefSchema, EventBindingSchema, ComponentSpecSchema, HttpMethodSchema, TransportSchema, EndpointSchema, SEMANTIC_GUARANTEES, GuaranteeSchema, MethodParamSchema, MethodSignatureSchema, InterfaceSpecSchema, NarrativeStepTypeSchema, LoopKindSchema, SwitchCaseSchema, CatchClauseSchema, ParallelBranchSchema, NarrativeStepSchema, NarrativeDetailSchema, ConformanceTierSchema, MethodImplementationSchema, ImplementationSpecSchema, TypeKindSchema, TypeFieldSchema, TypeMethodSchema, InvariantSchema, TypeSpecSchema, SurfaceOriginSchema, SurfaceTypeDefSchema, SurfaceContractEntrySchema, SurfaceSnapshotSchema, GroupSpecSchema;
|
|
706
|
+
var import_zod6, SpecIdSchema, SpecStatusSchema, BoundaryItemSchema, RequirementItemSchema, DatabaseSpecSchema, DiagramConfigSchema, SURFACE_AUDIENCES, SurfaceAudienceSchema, SystemPublicInterfaceSchema, SystemSpecSchema, PublicInterfaceTypeSchema, PublicInterfaceSchema, TrustedLinkSchema, LintAllowSchema, LintConfigSchema, ExtDataSchema, LifecycleEntrypointSchema, SubsystemSpecSchema, ComponentTypeSchema, PATTERN_TYPES, PortalTypeSchema, DispatchBindingSchema, DurabilitySchema, PatternRefSchema, EventBindingSchema, ExternalLinkTypeSchema, ExternalLinkSchema, PortalAuthSchemeSchema, PortalAuthSchema, ComponentSpecSchema, HttpMethodSchema, TransportSchema, EndpointSchema, SEMANTIC_GUARANTEES, GuaranteeSchema, MethodParamSchema, MethodSignatureSchema, InterfaceSpecSchema, NarrativeStepTypeSchema, LoopKindSchema, SwitchCaseSchema, CatchClauseSchema, ParallelBranchSchema, NarrativeStepSchema, NarrativeDetailSchema, ConformanceTierSchema, MethodImplementationSchema, ImplementationSpecSchema, TypeKindSchema, TypeFieldSchema, TypeMethodSchema, InvariantSchema, TypeSpecSchema, SurfaceOriginSchema, SurfaceTypeDefSchema, SurfaceContractEntrySchema, SurfaceSnapshotSchema, NamedOpenApiSpecSchema, GroupSpecSchema;
|
|
707
707
|
var init_specs = __esm({
|
|
708
708
|
"src/models/specs.ts"() {
|
|
709
709
|
"use strict";
|
|
@@ -907,6 +907,28 @@ var init_specs = __esm({
|
|
|
907
907
|
event: import_zod6.z.string().optional(),
|
|
908
908
|
description: import_zod6.z.string().optional()
|
|
909
909
|
});
|
|
910
|
+
ExternalLinkTypeSchema = import_zod6.z.enum(["implementation", "informative"]);
|
|
911
|
+
ExternalLinkSchema = import_zod6.z.object({
|
|
912
|
+
url: import_zod6.z.string(),
|
|
913
|
+
/** Defaults to 'informative' so an untyped link never silently satisfies the source requirement. */
|
|
914
|
+
type: ExternalLinkTypeSchema.default("informative"),
|
|
915
|
+
label: import_zod6.z.string().optional()
|
|
916
|
+
});
|
|
917
|
+
PortalAuthSchemeSchema = import_zod6.z.enum(["none", "apiKey", "bearer", "basic", "oauth2", "openIdConnect", "custom"]);
|
|
918
|
+
PortalAuthSchema = import_zod6.z.object({
|
|
919
|
+
scheme: PortalAuthSchemeSchema,
|
|
920
|
+
in: import_zod6.z.enum(["header", "query", "cookie"]).optional(),
|
|
921
|
+
name: import_zod6.z.string().optional(),
|
|
922
|
+
bearerFormat: import_zod6.z.string().optional(),
|
|
923
|
+
authorizationUrl: import_zod6.z.string().optional(),
|
|
924
|
+
tokenUrl: import_zod6.z.string().optional(),
|
|
925
|
+
refreshUrl: import_zod6.z.string().optional(),
|
|
926
|
+
scopes: import_zod6.z.array(import_zod6.z.object({ name: import_zod6.z.string(), description: import_zod6.z.string() })).optional(),
|
|
927
|
+
flow: import_zod6.z.enum(["authorizationCode", "clientCredentials", "implicit", "password"]).optional(),
|
|
928
|
+
openIdConnectUrl: import_zod6.z.string().optional(),
|
|
929
|
+
description: import_zod6.z.string().optional(),
|
|
930
|
+
example: import_zod6.z.string().optional()
|
|
931
|
+
});
|
|
910
932
|
ComponentSpecSchema = import_zod6.z.object({
|
|
911
933
|
id: SpecIdSchema,
|
|
912
934
|
name: import_zod6.z.string(),
|
|
@@ -920,6 +942,10 @@ var init_specs = __esm({
|
|
|
920
942
|
dependsOn: import_zod6.z.array(import_zod6.z.string()).default([]),
|
|
921
943
|
portalType: PortalTypeSchema.optional(),
|
|
922
944
|
basePath: import_zod6.z.string().optional(),
|
|
945
|
+
/** Portal-only: the API's authentication scheme (see PortalAuthSchema) — projected
|
|
946
|
+
* into the generated OpenAPI's securitySchemes/security. Portals with different auth
|
|
947
|
+
* must be separate components (one auth per portal ⇒ one OpenAPI spec per portal). */
|
|
948
|
+
auth: PortalAuthSchema.optional(),
|
|
923
949
|
/** Portal-only: capability → component.method dispatch table (see DispatchBindingSchema). */
|
|
924
950
|
dispatch: import_zod6.z.array(DispatchBindingSchema).optional(),
|
|
925
951
|
/** Store-only: whether held state survives restart (see DurabilitySchema). */
|
|
@@ -932,6 +958,11 @@ var init_specs = __esm({
|
|
|
932
958
|
patterns: import_zod6.z.array(PatternRefSchema).optional(),
|
|
933
959
|
/** Optional component variant — a declared, base-anchored specialization of this component's stereotype (resolved against the variant registry; UNKNOWN_VARIANT / VARIANT_BASE_MISMATCH). */
|
|
934
960
|
variant: import_zod6.z.string().optional(),
|
|
961
|
+
/** Opaque external references (see ExternalLinkSchema) — documented URLs wairon does
|
|
962
|
+
* not fetch or validate. An `implementation` link is the external source-of-record and
|
|
963
|
+
* satisfies the source requirement for a source-less implementation (suppresses
|
|
964
|
+
* MISSING_SOURCE_PATH); `informative` links are context only. */
|
|
965
|
+
externalLinks: import_zod6.z.array(ExternalLinkSchema).optional(),
|
|
935
966
|
/** Per-spec lint suppressions (see LintConfigSchema). */
|
|
936
967
|
lint: LintConfigSchema.optional(),
|
|
937
968
|
/** Opaque pack/tool extension data (see ExtDataSchema) — preserved verbatim. */
|
|
@@ -1064,6 +1095,18 @@ var init_specs = __esm({
|
|
|
1064
1095
|
// Required if type is 'call', references Method name on target interface
|
|
1065
1096
|
capability: import_zod6.z.string().optional(),
|
|
1066
1097
|
// Required if type is 'dispatch': the capability routed through the target Portal's dispatch table
|
|
1098
|
+
/**
|
|
1099
|
+
* call/dispatch only: the credential this step presents to an authed callee
|
|
1100
|
+
* Portal, and WHERE it is loaded from (`from`). Two forms: an OPAQUE source
|
|
1101
|
+
* (`env:API_KEY`, a config key, `vault:path`, a free note) — a design note
|
|
1102
|
+
* wairon never resolves; or a MODELED reference `component:<id>` pointing at
|
|
1103
|
+
* the Adapter/Store that provides the secret — validated to resolve, be an
|
|
1104
|
+
* Adapter/Store, and be wired to the presenter (a checked graph edge). The
|
|
1105
|
+
* actual secret is never stored here. Absence on a call into a Portal whose
|
|
1106
|
+
* `auth ≠ none` warns (PORTAL_AUTH_UNMET), so credential loading is never
|
|
1107
|
+
* overlooked.
|
|
1108
|
+
*/
|
|
1109
|
+
auth: import_zod6.z.object({ from: import_zod6.z.string(), note: import_zod6.z.string().optional() }).optional(),
|
|
1067
1110
|
assertsGuarantees: import_zod6.z.array(GuaranteeSchema).optional(),
|
|
1068
1111
|
/**
|
|
1069
1112
|
* Declared entity invariants this step upholds, as "<type-id>.<invariant-id>"
|
|
@@ -1280,7 +1323,12 @@ var init_specs = __esm({
|
|
|
1280
1323
|
dispatch: import_zod6.z.array(DispatchBindingSchema).optional(),
|
|
1281
1324
|
details: import_zod6.z.string().default(""),
|
|
1282
1325
|
version: import_zod6.z.string().optional(),
|
|
1283
|
-
stability: import_zod6.z.string().optional()
|
|
1326
|
+
stability: import_zod6.z.string().optional(),
|
|
1327
|
+
/** Projected copy of the backing Portal's auth (see PortalAuthSchema) — the codec
|
|
1328
|
+
* emits it as OpenAPI securitySchemes/security. */
|
|
1329
|
+
auth: PortalAuthSchema.optional(),
|
|
1330
|
+
/** The backing Portal's basePath — becomes the per-portal OpenAPI `servers` url. */
|
|
1331
|
+
basePath: import_zod6.z.string().optional()
|
|
1284
1332
|
});
|
|
1285
1333
|
SurfaceSnapshotSchema = import_zod6.z.object({
|
|
1286
1334
|
/** Producing project/system name — the snapshot's resolution identity. */
|
|
@@ -1295,6 +1343,11 @@ var init_specs = __esm({
|
|
|
1295
1343
|
/** Transitive type closure of every exported signature — self-contained. */
|
|
1296
1344
|
types: import_zod6.z.array(SurfaceTypeDefSchema).default([])
|
|
1297
1345
|
});
|
|
1346
|
+
NamedOpenApiSpecSchema = import_zod6.z.object({
|
|
1347
|
+
portalId: import_zod6.z.string(),
|
|
1348
|
+
name: import_zod6.z.string(),
|
|
1349
|
+
document: import_zod6.z.string()
|
|
1350
|
+
});
|
|
1298
1351
|
GroupSpecSchema = import_zod6.z.object({
|
|
1299
1352
|
kind: import_zod6.z.literal("group"),
|
|
1300
1353
|
id: SpecIdSchema,
|
|
@@ -2652,6 +2705,7 @@ function buildCanvasModel(issues = []) {
|
|
|
2652
2705
|
...ownerOf.has(comp.id) ? { owner: ownerOf.get(comp.id) } : {},
|
|
2653
2706
|
owns: comp.owns.filter((o) => componentIds.has(o)),
|
|
2654
2707
|
dependsOn: comp.dependsOn,
|
|
2708
|
+
...comp.externalLinks && comp.externalLinks.length ? { externalLinks: comp.externalLinks } : {},
|
|
2655
2709
|
interfaces: compInterfaces.map((i) => ({
|
|
2656
2710
|
id: i.id,
|
|
2657
2711
|
name: i.name,
|
|
@@ -3304,12 +3358,21 @@ var MODEL = __MODEL_JSON__;
|
|
|
3304
3358
|
var configuredLineStyle = ['bezier', 'straight', 'taxi'].indexOf(diagramConfig.lineStyle) >= 0 ? diagramConfig.lineStyle : 'bezier';
|
|
3305
3359
|
var defaultViewKind = diagramConfig.defaultView === 'types' ? 'types' : (diagramConfig.defaultView === 'databases' && showDatabaseTab ? 'databases' : 'system');
|
|
3306
3360
|
|
|
3361
|
+
// Stage J: a deep link (opts.initialRoute) seeds the initial view directly so a
|
|
3362
|
+
// refresh / shared URL renders the right scope with NO root-first flash. Parsed
|
|
3363
|
+
// by the same resolver as openRoute (hoisted below). No onViewChange fires for
|
|
3364
|
+
// this initial seed.
|
|
3365
|
+
var initialView = { kind: defaultViewKind, id: null };
|
|
3366
|
+
if (typeof opts !== 'undefined' && opts && typeof opts.initialRoute === 'string' && opts.initialRoute.length) {
|
|
3367
|
+
initialView = resolveRoute(opts.initialRoute);
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3307
3370
|
var state = {
|
|
3308
|
-
view:
|
|
3309
|
-
internals: false,
|
|
3310
|
-
externals: true,
|
|
3311
|
-
dataCoupling: false,
|
|
3312
|
-
showIssues: false,
|
|
3371
|
+
view: initialView,
|
|
3372
|
+
internals: typeof saved.internals === 'boolean' ? saved.internals : false,
|
|
3373
|
+
externals: typeof saved.externals === 'boolean' ? saved.externals : true,
|
|
3374
|
+
dataCoupling: typeof saved.dataCoupling === 'boolean' ? saved.dataCoupling : false,
|
|
3375
|
+
showIssues: typeof saved.showIssues === 'boolean' ? saved.showIssues : false,
|
|
3313
3376
|
query: '',
|
|
3314
3377
|
selected: null,
|
|
3315
3378
|
selectedKind: null,
|
|
@@ -3324,6 +3387,9 @@ var MODEL = __MODEL_JSON__;
|
|
|
3324
3387
|
// Set by buildTypeElements when the ERD is degraded for performance (huge
|
|
3325
3388
|
// scopes); consumed by renderTypesNotice to explain the level-of-detail.
|
|
3326
3389
|
var typesNotice = '';
|
|
3390
|
+
// Stage J: true while openRoute is applying a URL-driven view change, so the
|
|
3391
|
+
// onViewChange callback is suppressed and we do not loop URL -> engine -> URL.
|
|
3392
|
+
var applyingRoute = false;
|
|
3327
3393
|
|
|
3328
3394
|
function viewKey() {
|
|
3329
3395
|
// 'types2' + detail level: table sizes differ per detail, and the prefix
|
|
@@ -3343,6 +3409,10 @@ var MODEL = __MODEL_JSON__;
|
|
|
3343
3409
|
lineStyle: state.lineStyle,
|
|
3344
3410
|
panelOpen: state.panelOpen,
|
|
3345
3411
|
panelWidth: state.panelWidth,
|
|
3412
|
+
internals: state.internals,
|
|
3413
|
+
externals: state.externals,
|
|
3414
|
+
dataCoupling: state.dataCoupling,
|
|
3415
|
+
showIssues: state.showIssues,
|
|
3346
3416
|
}));
|
|
3347
3417
|
} catch (e) { /* non-fatal */ }
|
|
3348
3418
|
}
|
|
@@ -4749,6 +4819,78 @@ var MODEL = __MODEL_JSON__;
|
|
|
4749
4819
|
state.typesRenderAll = false; // a fresh scope re-evaluates the LOD budget
|
|
4750
4820
|
rebuild(true);
|
|
4751
4821
|
renderPanel();
|
|
4822
|
+
notifyViewChange();
|
|
4823
|
+
}
|
|
4824
|
+
|
|
4825
|
+
// ---- Stage J: URL <-> view routing ----------------------------------------
|
|
4826
|
+
// The canvas navigation lives in the URL path (refresh-safe + shareable). A
|
|
4827
|
+
// route is the string AFTER /canvas/<project>: '' = system root; a '/'-joined
|
|
4828
|
+
// NAMESPACE (e.g. 'a/b') that resolves against the model to a subsystem or a
|
|
4829
|
+
// component; or the 'types'/'databases' view modes with an optional scope.
|
|
4830
|
+
// A segment is the '::' namespace with '/' as separator, each part encoded.
|
|
4831
|
+
function encSeg(s) { return encodeURIComponent(String(s)); }
|
|
4832
|
+
function routeOf(view) {
|
|
4833
|
+
if (!view) return '';
|
|
4834
|
+
var k = view.kind;
|
|
4835
|
+
if (k === 'system') return '';
|
|
4836
|
+
if (k === 'types' || k === 'databases') {
|
|
4837
|
+
return view.id ? k + '/' + view.id.split('::').map(encSeg).join('/') : k;
|
|
4838
|
+
}
|
|
4839
|
+
if (k === 'subsystem') {
|
|
4840
|
+
return view.id ? view.id.split('::').map(encSeg).join('/') : '';
|
|
4841
|
+
}
|
|
4842
|
+
if (k === 'component') {
|
|
4843
|
+
// A component's full namespace path IS its id: a chained subproject carries
|
|
4844
|
+
// the subsystem prefix in the id (a::b::comp), a flat project uses the bare
|
|
4845
|
+
// id (comp). Serializing comp.id split on '::' round-trips exactly via the
|
|
4846
|
+
// compById lookup in resolveRoute. Owner-pattern nesting is deliberately NOT
|
|
4847
|
+
// encoded (ownership is a separate axis; comp.id does not embed the owner).
|
|
4848
|
+
var c = compById[view.id];
|
|
4849
|
+
var full = c ? c.id : view.id;
|
|
4850
|
+
return full.split('::').map(encSeg).join('/');
|
|
4851
|
+
}
|
|
4852
|
+
return '';
|
|
4853
|
+
}
|
|
4854
|
+
function resolveRoute(routeStr) {
|
|
4855
|
+
var parts = String(routeStr || '').split('/').filter(function (s) { return s.length > 0; }).map(decodeURIComponent);
|
|
4856
|
+
if (!parts.length) return { kind: 'system', id: null };
|
|
4857
|
+
// NOTE: a subsystem literally named 'types'/'databases' is SHADOWED by these
|
|
4858
|
+
// view-mode routes (acceptable \u2014 the modes own those first segments).
|
|
4859
|
+
if (parts[0] === 'types') {
|
|
4860
|
+
return { kind: 'types', id: parts.length > 1 ? parts.slice(1).join('::') : null };
|
|
4861
|
+
}
|
|
4862
|
+
if (parts[0] === 'databases') {
|
|
4863
|
+
if (!showDatabaseTab) return { kind: 'system', id: null };
|
|
4864
|
+
return { kind: 'databases', id: parts.length > 1 ? parts.slice(1).join('::') : null };
|
|
4865
|
+
}
|
|
4866
|
+
var joined = parts.join('::');
|
|
4867
|
+
if (subById[joined]) return { kind: 'subsystem', id: joined };
|
|
4868
|
+
if (compById[joined]) return { kind: 'component', id: joined };
|
|
4869
|
+
return { kind: 'system', id: null }; // unknown id -> fall back to the root
|
|
4870
|
+
}
|
|
4871
|
+
// Apply a route (URL -> engine) WITHOUT echoing back through onViewChange.
|
|
4872
|
+
function openRoute(routeStr) {
|
|
4873
|
+
var v = resolveRoute(routeStr);
|
|
4874
|
+
if (state.view.kind === v.kind && state.view.id === v.id) return;
|
|
4875
|
+
applyingRoute = true;
|
|
4876
|
+
try {
|
|
4877
|
+
state.view = { kind: v.kind, id: v.id };
|
|
4878
|
+
state.selected = null;
|
|
4879
|
+
state.selectedKind = null;
|
|
4880
|
+
state.typesRenderAll = false;
|
|
4881
|
+
rebuild(true);
|
|
4882
|
+
renderPanel();
|
|
4883
|
+
} finally {
|
|
4884
|
+
applyingRoute = false;
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4887
|
+
// Notify the embedder (engine -> URL) of the current view; suppressed while a
|
|
4888
|
+
// route is being applied so the URL is not driven in a loop.
|
|
4889
|
+
function notifyViewChange() {
|
|
4890
|
+
if (applyingRoute) return;
|
|
4891
|
+
if (typeof opts !== 'undefined' && opts && typeof opts.onViewChange === 'function') {
|
|
4892
|
+
try { opts.onViewChange(routeOf(state.view)); } catch (e) { /* ignore */ }
|
|
4893
|
+
}
|
|
4752
4894
|
}
|
|
4753
4895
|
// Explain (and offer to override) a performance-degraded ERD.
|
|
4754
4896
|
function renderTypesNotice() {
|
|
@@ -4899,6 +5041,7 @@ var MODEL = __MODEL_JSON__;
|
|
|
4899
5041
|
if (t.ghost) {
|
|
4900
5042
|
state.view = parentViewOf(t.kind, t.id);
|
|
4901
5043
|
rebuild(true);
|
|
5044
|
+
notifyViewChange();
|
|
4902
5045
|
select(t.kind, t.id, true);
|
|
4903
5046
|
return;
|
|
4904
5047
|
}
|
|
@@ -4920,10 +5063,16 @@ var MODEL = __MODEL_JSON__;
|
|
|
4920
5063
|
}
|
|
4921
5064
|
|
|
4922
5065
|
document.getElementById('search').addEventListener('input', function (ev) { state.query = ev.target.value.trim(); rebuild(false); });
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
document.getElementById('
|
|
4926
|
-
document.getElementById('
|
|
5066
|
+
// Sync each View toggle's checkbox from the (possibly persisted) state, then
|
|
5067
|
+
// persist on change so the choices survive a refresh (see persist()/saved).
|
|
5068
|
+
document.getElementById('internalsToggle').checked = state.internals;
|
|
5069
|
+
document.getElementById('internalsToggle').addEventListener('change', function (ev) { state.internals = ev.target.checked; persist(); rebuild(true); });
|
|
5070
|
+
document.getElementById('externalsToggle').checked = state.externals;
|
|
5071
|
+
document.getElementById('externalsToggle').addEventListener('change', function (ev) { state.externals = ev.target.checked; persist(); rebuild(true); });
|
|
5072
|
+
document.getElementById('dataCouplingToggle').checked = state.dataCoupling;
|
|
5073
|
+
document.getElementById('dataCouplingToggle').addEventListener('change', function (ev) { state.dataCoupling = ev.target.checked; persist(); renderLegend(); rebuild(true); });
|
|
5074
|
+
document.getElementById('issuesToggle').checked = state.showIssues;
|
|
5075
|
+
document.getElementById('issuesToggle').addEventListener('change', function (ev) { state.showIssues = ev.target.checked; persist(); rebuild(false); renderPanel(); });
|
|
4927
5076
|
document.getElementById('dragToggle').addEventListener('change', function (ev) { cy.autolock(!ev.target.checked); });
|
|
4928
5077
|
// Mode seg: Components \u21C4 Types. Entering Types keeps the current subsystem
|
|
4929
5078
|
// scope, so a subsystem's own types (plus shared ones) show scoped.
|
|
@@ -5913,10 +6062,12 @@ var MODEL = __MODEL_JSON__;
|
|
|
5913
6062
|
if (kind === 'type' && state.view.kind !== 'types' && state.view.kind !== 'databases') {
|
|
5914
6063
|
state.view = { kind: 'types', id: typesScopeFromView() };
|
|
5915
6064
|
rebuild(true);
|
|
6065
|
+
notifyViewChange();
|
|
5916
6066
|
} else if (kind === 'component' && (state.view.kind === 'types' || state.view.kind === 'databases')) {
|
|
5917
6067
|
var c = compById[id];
|
|
5918
6068
|
state.view = { kind: 'subsystem', id: c ? c.subsystem : null };
|
|
5919
6069
|
rebuild(true);
|
|
6070
|
+
notifyViewChange();
|
|
5920
6071
|
}
|
|
5921
6072
|
state.selectedKind = kind;
|
|
5922
6073
|
state.selected = id;
|
|
@@ -5975,6 +6126,14 @@ var MODEL = __MODEL_JSON__;
|
|
|
5975
6126
|
if (!exposes || !openApiAllowed()) return '';
|
|
5976
6127
|
return '<div class="openbtn"><button class="tbtn" data-openapi-tag="' + esc(tag || '') + '">\\u25A4 View OpenAPI \\u2197</button></div>';
|
|
5977
6128
|
}
|
|
6129
|
+
// "Open in Specs" \u2014 deep-links the focused spec into the hosted Specs value
|
|
6130
|
+
// editor via a host hook (opts.onOpenSpec). Hidden when no host provides it
|
|
6131
|
+
// (standalone file, shared page): those have no editor to open. kind is the
|
|
6132
|
+
// spec layer, id its qualified spec id.
|
|
6133
|
+
function openSpecButton(kind, id) {
|
|
6134
|
+
if (typeof opts === 'undefined' || !opts || typeof opts.onOpenSpec !== 'function') return '';
|
|
6135
|
+
return '<div class="openbtn"><button class="tbtn" data-openspec-kind="' + kind + '" data-openspec-id="' + esc(id) + '">\\u270E Open in Specs \\u2197</button></div>';
|
|
6136
|
+
}
|
|
5978
6137
|
|
|
5979
6138
|
function renderPanel() {
|
|
5980
6139
|
var head = '', body = '';
|
|
@@ -5997,7 +6156,8 @@ var MODEL = __MODEL_JSON__;
|
|
|
5997
6156
|
+ (scopeFocus ? staticChip('current view') : '')
|
|
5998
6157
|
+ chip(c.subsystem, 'subsystem', c.subsystem)
|
|
5999
6158
|
+ (scopeFocus ? '' : openViewButton('component', c.id, c.owns.length > 0))
|
|
6000
|
-
+ openApiButton(componentExposesApi(c), c.
|
|
6159
|
+
+ openApiButton(componentExposesApi(c), c.id)
|
|
6160
|
+
+ openSpecButton('component', c.id);
|
|
6001
6161
|
|
|
6002
6162
|
var linkedTypes = MODEL.types.filter(function (t) { return t.componentClass === c.id; });
|
|
6003
6163
|
if (linkedTypes.length) {
|
|
@@ -6007,6 +6167,14 @@ var MODEL = __MODEL_JSON__;
|
|
|
6007
6167
|
}
|
|
6008
6168
|
body += '<p class="desc">' + esc(c.description) + '</p>';
|
|
6009
6169
|
|
|
6170
|
+
if (c.externalLinks && c.externalLinks.length) {
|
|
6171
|
+
body += section('External links', c.externalLinks.length, c.externalLinks.map(function (l) {
|
|
6172
|
+
var label = l.label || l.url;
|
|
6173
|
+
var tag = l.type === 'implementation' ? staticChip('source') : '';
|
|
6174
|
+
return '<div class="method">' + tag + '<a href="' + esc(l.url) + '" target="_blank" rel="noopener" style="color:var(--accent);word-break:break-all">' + esc(label) + ' \\u2197</a></div>';
|
|
6175
|
+
}).join(''), true);
|
|
6176
|
+
}
|
|
6177
|
+
|
|
6010
6178
|
var depInner = (c.dependsOn.length ? c.dependsOn.map(function (d) { return chip(d, 'component', d); }).join('') : '<span class="desc">none</span>')
|
|
6011
6179
|
+ (c.owns.length ? '<div style="margin-top:8px"><b style="font-size:11px">Owns:</b><br>' + c.owns.map(function (d) { return chip(d, 'component', d); }).join('') + '</div>' : '');
|
|
6012
6180
|
body += section('Dependencies', c.dependsOn.length + c.owns.length, depInner, true);
|
|
@@ -6081,7 +6249,8 @@ var MODEL = __MODEL_JSON__;
|
|
|
6081
6249
|
MODEL.types.forEach(function (t2) { if (t2.id === focusId) ty = t2; });
|
|
6082
6250
|
if (ty) {
|
|
6083
6251
|
head = '<h2>' + esc(ty.name) + '</h2>' + staticChip('\\u00AB' + ty.kind + '\\u00BB')
|
|
6084
|
-
+ (ty.subsystem ? chip(ty.subsystem, 'subsystem', ty.subsystem) : staticChip('system-level shared'))
|
|
6252
|
+
+ (ty.subsystem ? chip(ty.subsystem, 'subsystem', ty.subsystem) : staticChip('system-level shared'))
|
|
6253
|
+
+ openSpecButton('type', ty.id);
|
|
6085
6254
|
|
|
6086
6255
|
if (ty.componentClass) {
|
|
6087
6256
|
head += '<div style="margin-top:6px"><b style="font-size:11px">Class Component:</b> ' + chip(ty.componentClass, 'component', ty.componentClass) + '</div>';
|
|
@@ -6157,7 +6326,8 @@ var MODEL = __MODEL_JSON__;
|
|
|
6157
6326
|
+ (s.status ? staticChip(s.status) : '')
|
|
6158
6327
|
+ (scopeFocus ? staticChip('current view') : '')
|
|
6159
6328
|
+ (scopeFocus ? '' : openViewButton('subsystem', s.id, subKids > 0))
|
|
6160
|
-
+ openApiButton(subsystemExposesApi(s.id), '')
|
|
6329
|
+
+ openApiButton(subsystemExposesApi(s.id), '')
|
|
6330
|
+
+ openSpecButton('subsystem', s.id);
|
|
6161
6331
|
body += '<p class="desc">' + esc(s.description) + '</p>';
|
|
6162
6332
|
if (s.trustedLinks.length) {
|
|
6163
6333
|
body += section('Trusted links (fast lanes)', s.trustedLinks.length, s.trustedLinks.map(function (t2) {
|
|
@@ -6215,6 +6385,16 @@ var MODEL = __MODEL_JSON__;
|
|
|
6215
6385
|
});
|
|
6216
6386
|
})(oapis[oi]);
|
|
6217
6387
|
}
|
|
6388
|
+
var ospecs = panel.querySelectorAll('[data-openspec-kind]');
|
|
6389
|
+
for (var si = 0; si < ospecs.length; si++) {
|
|
6390
|
+
(function (b) {
|
|
6391
|
+
b.addEventListener('click', function () {
|
|
6392
|
+
if (typeof opts !== 'undefined' && opts && typeof opts.onOpenSpec === 'function') {
|
|
6393
|
+
opts.onOpenSpec(b.getAttribute('data-openspec-kind'), b.getAttribute('data-openspec-id') || '');
|
|
6394
|
+
}
|
|
6395
|
+
});
|
|
6396
|
+
})(ospecs[si]);
|
|
6397
|
+
}
|
|
6218
6398
|
var flows = panel.querySelectorAll('[data-flow-comp]');
|
|
6219
6399
|
for (var j = 0; j < flows.length; j++) {
|
|
6220
6400
|
(function (b) {
|
|
@@ -6227,6 +6407,23 @@ var MODEL = __MODEL_JSON__;
|
|
|
6227
6407
|
}
|
|
6228
6408
|
|
|
6229
6409
|
renderPanel();
|
|
6410
|
+
// Stage G: a deep link may focus a component and/or open a method's narrative
|
|
6411
|
+
// modal once the seeded view + DOM + cy graph exist. The host parses the URL hash
|
|
6412
|
+
// into opts.initialSelect / opts.initialFlow \u2014 both carry the component id, so this
|
|
6413
|
+
// works whether the seeded view is the component itself or its parent subsystem
|
|
6414
|
+
// (a leaf component has no meaningful "inside", so Specs deep-links open the parent
|
|
6415
|
+
// and focus the component here).
|
|
6416
|
+
(function () {
|
|
6417
|
+
if (typeof opts === 'undefined' || !opts) return;
|
|
6418
|
+
var f = opts.initialFlow, s = opts.initialSelect;
|
|
6419
|
+
var focusComp = (f && f.comp) || (s && s.comp);
|
|
6420
|
+
if (focusComp && compById[focusComp]) {
|
|
6421
|
+
try { select('component', focusComp, true); } catch (e) { /* ignore */ }
|
|
6422
|
+
}
|
|
6423
|
+
if (f && f.comp && f.method && compById[f.comp]) {
|
|
6424
|
+
try { openFlow(f.comp, f.method, f.mode === 'steps' ? 'steps' : 'flow'); } catch (e) { /* ignore */ }
|
|
6425
|
+
}
|
|
6426
|
+
})();
|
|
6230
6427
|
})();
|
|
6231
6428
|
</script>
|
|
6232
6429
|
</body>
|
|
@@ -6764,11 +6961,63 @@ function operationFor(method2, closureIds) {
|
|
|
6764
6961
|
}
|
|
6765
6962
|
return op;
|
|
6766
6963
|
}
|
|
6767
|
-
function
|
|
6768
|
-
|
|
6769
|
-
const
|
|
6964
|
+
function securitySchemeObject(auth) {
|
|
6965
|
+
if (auth.scheme === "none") return null;
|
|
6966
|
+
const desc = auth.description ? { description: auth.description } : {};
|
|
6967
|
+
switch (auth.scheme) {
|
|
6968
|
+
case "apiKey":
|
|
6969
|
+
return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "X-API-Key", ...desc };
|
|
6970
|
+
case "bearer":
|
|
6971
|
+
return { type: "http", scheme: "bearer", ...auth.bearerFormat ? { bearerFormat: auth.bearerFormat } : {}, ...desc };
|
|
6972
|
+
case "basic":
|
|
6973
|
+
return { type: "http", scheme: "basic", ...desc };
|
|
6974
|
+
case "oauth2": {
|
|
6975
|
+
const flow = { scopes: Object.fromEntries((auth.scopes ?? []).map((s) => [s.name, s.description])) };
|
|
6976
|
+
if (auth.authorizationUrl) flow.authorizationUrl = auth.authorizationUrl;
|
|
6977
|
+
if (auth.tokenUrl) flow.tokenUrl = auth.tokenUrl;
|
|
6978
|
+
if (auth.refreshUrl) flow.refreshUrl = auth.refreshUrl;
|
|
6979
|
+
return { type: "oauth2", flows: { [auth.flow ?? "authorizationCode"]: flow }, ...desc };
|
|
6980
|
+
}
|
|
6981
|
+
case "openIdConnect":
|
|
6982
|
+
return { type: "openIdConnect", openIdConnectUrl: auth.openIdConnectUrl ?? "", ...desc };
|
|
6983
|
+
case "custom":
|
|
6984
|
+
return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "Authorization", description: auth.description ?? auth.example ?? "Custom authentication scheme." };
|
|
6985
|
+
default:
|
|
6986
|
+
return null;
|
|
6987
|
+
}
|
|
6988
|
+
}
|
|
6989
|
+
function schemeBaseName(scheme) {
|
|
6990
|
+
return { apiKey: "ApiKeyAuth", bearer: "BearerAuth", basic: "BasicAuth", oauth2: "OAuth2", openIdConnect: "OpenIdConnect", custom: "CustomAuth" }[scheme] ?? "Auth";
|
|
6991
|
+
}
|
|
6992
|
+
function buildSecurity(entries) {
|
|
6993
|
+
const schemes = {};
|
|
6994
|
+
const nameByContent = /* @__PURE__ */ new Map();
|
|
6995
|
+
const securityByEntry = /* @__PURE__ */ new Map();
|
|
6996
|
+
for (const entry of entries) {
|
|
6997
|
+
if (!entry.auth) continue;
|
|
6998
|
+
const obj = securitySchemeObject(entry.auth);
|
|
6999
|
+
if (!obj) continue;
|
|
7000
|
+
const content = JSON.stringify(obj);
|
|
7001
|
+
let name = nameByContent.get(content);
|
|
7002
|
+
if (!name) {
|
|
7003
|
+
name = schemeBaseName(entry.auth.scheme);
|
|
7004
|
+
for (let n = 2; schemes[name]; n++) name = schemeBaseName(entry.auth.scheme) + n;
|
|
7005
|
+
schemes[name] = obj;
|
|
7006
|
+
nameByContent.set(content, name);
|
|
7007
|
+
}
|
|
7008
|
+
const scopeNames = entry.auth.scheme === "oauth2" ? (entry.auth.scopes ?? []).map((s) => s.name) : [];
|
|
7009
|
+
securityByEntry.set(entry.id, { [name]: scopeNames });
|
|
7010
|
+
}
|
|
7011
|
+
return { schemes, securityByEntry };
|
|
7012
|
+
}
|
|
7013
|
+
function httpEntriesOf(snapshot) {
|
|
7014
|
+
return snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
|
|
7015
|
+
}
|
|
7016
|
+
function renderDoc(snapshot, entries, closureIds, opts = {}) {
|
|
7017
|
+
const { schemes, securityByEntry } = buildSecurity(entries);
|
|
6770
7018
|
const paths = {};
|
|
6771
|
-
for (const entry of
|
|
7019
|
+
for (const entry of entries) {
|
|
7020
|
+
const security = securityByEntry.get(entry.id);
|
|
6772
7021
|
for (const method2 of entry.methods) {
|
|
6773
7022
|
const endpoint = method2.endpoint;
|
|
6774
7023
|
if (!endpoint || endpoint.transport !== "HTTP") continue;
|
|
@@ -6776,7 +7025,8 @@ function toOpenApi(snapshot) {
|
|
|
6776
7025
|
paths[p] = paths[p] ?? {};
|
|
6777
7026
|
paths[p][endpoint.method.toLowerCase()] = {
|
|
6778
7027
|
tags: [entry.id],
|
|
6779
|
-
...operationFor(method2, closureIds)
|
|
7028
|
+
...operationFor(method2, closureIds),
|
|
7029
|
+
...security ? { security: [security] } : {}
|
|
6780
7030
|
};
|
|
6781
7031
|
}
|
|
6782
7032
|
}
|
|
@@ -6789,19 +7039,41 @@ function toOpenApi(snapshot) {
|
|
|
6789
7039
|
required: t.fields.filter((f) => !f.optional).map((f) => f.name)
|
|
6790
7040
|
};
|
|
6791
7041
|
}
|
|
6792
|
-
const
|
|
7042
|
+
const components = {};
|
|
7043
|
+
if (Object.keys(schemas).length) components.schemas = schemas;
|
|
7044
|
+
if (Object.keys(schemes).length) components.securitySchemes = schemes;
|
|
7045
|
+
const basePaths = [...new Set(entries.map((e) => e.basePath).filter((b) => !!b))];
|
|
7046
|
+
const servers = opts.servers && basePaths.length === 1 ? [{ url: basePaths[0] }] : void 0;
|
|
7047
|
+
return {
|
|
6793
7048
|
openapi: "3.1.0",
|
|
6794
7049
|
info: {
|
|
6795
|
-
title: snapshot.projectName,
|
|
7050
|
+
title: opts.title ?? snapshot.projectName,
|
|
6796
7051
|
version: snapshot.version ?? "0.0.0",
|
|
6797
7052
|
...snapshot.stateId ? { "x-wairon-state-id": snapshot.stateId } : {},
|
|
6798
7053
|
"x-wairon-origin": snapshot.origin,
|
|
6799
7054
|
"x-wairon-generated-at": snapshot.generatedAt
|
|
6800
7055
|
},
|
|
7056
|
+
...servers ? { servers } : {},
|
|
6801
7057
|
paths,
|
|
6802
|
-
...Object.keys(
|
|
7058
|
+
...Object.keys(components).length ? { components } : {}
|
|
6803
7059
|
};
|
|
6804
|
-
|
|
7060
|
+
}
|
|
7061
|
+
function toOpenApiSet(snapshot) {
|
|
7062
|
+
const closureIds = new Set(snapshot.types.map((t) => t.id));
|
|
7063
|
+
const byPortal = /* @__PURE__ */ new Map();
|
|
7064
|
+
const order = [];
|
|
7065
|
+
for (const entry of httpEntriesOf(snapshot)) {
|
|
7066
|
+
if (!byPortal.has(entry.component)) {
|
|
7067
|
+
byPortal.set(entry.component, []);
|
|
7068
|
+
order.push(entry.component);
|
|
7069
|
+
}
|
|
7070
|
+
byPortal.get(entry.component).push(entry);
|
|
7071
|
+
}
|
|
7072
|
+
return order.map((portalId) => {
|
|
7073
|
+
const entries = byPortal.get(portalId);
|
|
7074
|
+
const name = entries[0]?.name ?? portalId;
|
|
7075
|
+
return { portalId, name, document: JSON.stringify(renderDoc(snapshot, entries, closureIds, { title: name, servers: true }), null, 2) };
|
|
7076
|
+
});
|
|
6805
7077
|
}
|
|
6806
7078
|
function isOpenApiDocument(body) {
|
|
6807
7079
|
try {
|
|
@@ -6823,6 +7095,39 @@ function typeRefFromSchema(schema) {
|
|
|
6823
7095
|
if (typeof t === "string" && t !== "object") return t;
|
|
6824
7096
|
return "json";
|
|
6825
7097
|
}
|
|
7098
|
+
function authFromSecurityScheme(scheme) {
|
|
7099
|
+
const desc = typeof scheme.description === "string" ? { description: scheme.description } : {};
|
|
7100
|
+
if (scheme.type === "apiKey") {
|
|
7101
|
+
return {
|
|
7102
|
+
scheme: "apiKey",
|
|
7103
|
+
...scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie" ? { in: scheme.in } : {},
|
|
7104
|
+
...typeof scheme.name === "string" ? { name: scheme.name } : {},
|
|
7105
|
+
...desc
|
|
7106
|
+
};
|
|
7107
|
+
}
|
|
7108
|
+
if (scheme.type === "http") {
|
|
7109
|
+
if (scheme.scheme === "bearer") return { scheme: "bearer", ...typeof scheme.bearerFormat === "string" ? { bearerFormat: scheme.bearerFormat } : {}, ...desc };
|
|
7110
|
+
if (scheme.scheme === "basic") return { scheme: "basic", ...desc };
|
|
7111
|
+
}
|
|
7112
|
+
if (scheme.type === "oauth2") {
|
|
7113
|
+
const flows = scheme.flows ?? {};
|
|
7114
|
+
const flowKey = Object.keys(flows)[0];
|
|
7115
|
+
const f = flows[flowKey] ?? {};
|
|
7116
|
+
return {
|
|
7117
|
+
scheme: "oauth2",
|
|
7118
|
+
...flowKey === "authorizationCode" || flowKey === "clientCredentials" || flowKey === "implicit" || flowKey === "password" ? { flow: flowKey } : {},
|
|
7119
|
+
...typeof f.authorizationUrl === "string" ? { authorizationUrl: f.authorizationUrl } : {},
|
|
7120
|
+
...typeof f.tokenUrl === "string" ? { tokenUrl: f.tokenUrl } : {},
|
|
7121
|
+
...typeof f.refreshUrl === "string" ? { refreshUrl: f.refreshUrl } : {},
|
|
7122
|
+
scopes: Object.entries(f.scopes ?? {}).map(([name, description]) => ({ name, description })),
|
|
7123
|
+
...desc
|
|
7124
|
+
};
|
|
7125
|
+
}
|
|
7126
|
+
if (scheme.type === "openIdConnect") {
|
|
7127
|
+
return { scheme: "openIdConnect", openIdConnectUrl: typeof scheme.openIdConnectUrl === "string" ? scheme.openIdConnectUrl : "", ...desc };
|
|
7128
|
+
}
|
|
7129
|
+
return void 0;
|
|
7130
|
+
}
|
|
6826
7131
|
function fromOpenApi(document, projectName) {
|
|
6827
7132
|
let parsed;
|
|
6828
7133
|
try {
|
|
@@ -6900,6 +7205,9 @@ function fromOpenApi(document, projectName) {
|
|
|
6900
7205
|
}))
|
|
6901
7206
|
});
|
|
6902
7207
|
}
|
|
7208
|
+
const securitySchemes = parsed.components?.securitySchemes ?? {};
|
|
7209
|
+
const firstScheme = Object.values(securitySchemes)[0];
|
|
7210
|
+
const importedAuth = firstScheme ? authFromSecurityScheme(firstScheme) : void 0;
|
|
6903
7211
|
const entry = {
|
|
6904
7212
|
id: `${projectName}-api`,
|
|
6905
7213
|
name: typeof info.title === "string" ? info.title : projectName,
|
|
@@ -6908,7 +7216,8 @@ function fromOpenApi(document, projectName) {
|
|
|
6908
7216
|
component: `${projectName}-api`,
|
|
6909
7217
|
methods,
|
|
6910
7218
|
details: typeof info.description === "string" ? info.description : `Imported OpenAPI surface of ${projectName}.`,
|
|
6911
|
-
...typeof info.version === "string" ? { version: info.version } : {}
|
|
7219
|
+
...typeof info.version === "string" ? { version: info.version } : {},
|
|
7220
|
+
...importedAuth ? { auth: importedAuth } : {}
|
|
6912
7221
|
};
|
|
6913
7222
|
return SurfaceSnapshotSchema.parse({
|
|
6914
7223
|
projectName,
|
|
@@ -7023,6 +7332,10 @@ function projectOwnSurface(maxAudience) {
|
|
|
7023
7332
|
component: comp.id,
|
|
7024
7333
|
methods,
|
|
7025
7334
|
...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
|
|
7335
|
+
// Project the backing Portal's auth + basePath so the codec can emit
|
|
7336
|
+
// OpenAPI security + per-portal servers self-contained from the snapshot.
|
|
7337
|
+
...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
|
|
7338
|
+
...comp.basePath ? { basePath: comp.basePath } : {},
|
|
7026
7339
|
details: raw.details ?? "",
|
|
7027
7340
|
...raw.version ? { version: raw.version } : {},
|
|
7028
7341
|
...raw.stability ? { stability: raw.stability } : {}
|
|
@@ -7066,20 +7379,28 @@ function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
|
|
|
7066
7379
|
function loadSurfaceSnapshots() {
|
|
7067
7380
|
return listSnapshots();
|
|
7068
7381
|
}
|
|
7382
|
+
function renderOpenApiForms(snapshot) {
|
|
7383
|
+
const renderedSet = toOpenApiSet(snapshot);
|
|
7384
|
+
return { renderedSet, ...renderedSet.length === 1 ? { rendered: renderedSet[0].document } : {} };
|
|
7385
|
+
}
|
|
7386
|
+
function writeSurfaceFile(outPath, body, snapshot) {
|
|
7387
|
+
const resolved = path10.resolve(outPath);
|
|
7388
|
+
fs9.mkdirSync(path10.dirname(resolved), { recursive: true });
|
|
7389
|
+
if (body !== void 0) fs9.writeFileSync(resolved, body);
|
|
7390
|
+
else writeYamlFile(resolved, snapshot);
|
|
7391
|
+
return resolved;
|
|
7392
|
+
}
|
|
7069
7393
|
function exportSurface(maxAudience, format, outPath) {
|
|
7070
7394
|
const snapshot = projectOwnSurface(maxAudience);
|
|
7071
|
-
const
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
}
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
writtenTo = path10.resolve(outPath);
|
|
7081
|
-
}
|
|
7082
|
-
return { snapshot, ...rendered !== void 0 ? { rendered } : {}, ...writtenTo ? { writtenTo } : {} };
|
|
7395
|
+
const openapi = format === "openapi" ? renderOpenApiForms(snapshot) : void 0;
|
|
7396
|
+
const body = openapi?.rendered ?? openapi?.renderedSet[0]?.document;
|
|
7397
|
+
const writtenTo = outPath ? writeSurfaceFile(outPath, body, snapshot) : void 0;
|
|
7398
|
+
return {
|
|
7399
|
+
snapshot,
|
|
7400
|
+
...openapi?.rendered !== void 0 ? { rendered: openapi.rendered } : {},
|
|
7401
|
+
...openapi ? { renderedSet: openapi.renderedSet } : {},
|
|
7402
|
+
...writtenTo ? { writtenTo } : {}
|
|
7403
|
+
};
|
|
7083
7404
|
}
|
|
7084
7405
|
function importSurface(sourcePath, origin) {
|
|
7085
7406
|
const resolved = path10.resolve(sourcePath);
|
|
@@ -8339,13 +8660,16 @@ var init_conformance = __esm({
|
|
|
8339
8660
|
if (isInChainedSubproject(component.subsystem, ctx)) continue;
|
|
8340
8661
|
const draft = ctx.isImplementationDraft(impl);
|
|
8341
8662
|
if (!impl.sourcePath) {
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8663
|
+
const hasExternalSource = (component.externalLinks ?? []).some((l) => l.type === "implementation");
|
|
8664
|
+
if (!hasExternalSource) {
|
|
8665
|
+
ctx.addIssue(
|
|
8666
|
+
"warning",
|
|
8667
|
+
"MISSING_SOURCE_PATH",
|
|
8668
|
+
`Implementation "${impl.id}" declares no sourcePath \u2014 its contract "${impl.contract}" cannot be structurally checked against code.`,
|
|
8669
|
+
impl.id,
|
|
8670
|
+
draft
|
|
8671
|
+
);
|
|
8672
|
+
}
|
|
8349
8673
|
continue;
|
|
8350
8674
|
}
|
|
8351
8675
|
const lookup = lookups.get(normalizeSourcePath(impl.sourcePath));
|
|
@@ -8734,13 +9058,14 @@ var init_portals = __esm({
|
|
|
8734
9058
|
};
|
|
8735
9059
|
portalsRule = {
|
|
8736
9060
|
name: "portal-endpoints",
|
|
8737
|
-
description: "A Portal declares its portalType and binds every interface method to a concrete endpoint of the matching transport. Non-Portal components carry no portalType, basePath, or
|
|
9061
|
+
description: "A Portal declares its portalType and binds every interface method to a concrete endpoint of the matching transport. Non-Portal components carry no portalType, basePath, endpoints, or auth (auth is inbound transport auth \u2014 a Gateway carries it on the Portal it owns).",
|
|
8738
9062
|
codes: [
|
|
8739
9063
|
{ code: "MISSING_PORTAL_TYPE", defaultSeverity: "error", summary: "Portal without a portalType" },
|
|
8740
9064
|
{ code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding" },
|
|
8741
9065
|
{ code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal portalType" },
|
|
8742
9066
|
{ code: "UNEXPECTED_PORTAL_FIELD", defaultSeverity: "error", summary: "Non-Portal component with portalType/basePath" },
|
|
8743
|
-
{ code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" }
|
|
9067
|
+
{ code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" },
|
|
9068
|
+
{ code: "AUTH_ON_NON_PORTAL", defaultSeverity: "warning", summary: "Non-Portal component declaring auth (auth is inbound transport auth, only meaningful on a Portal)" }
|
|
8744
9069
|
],
|
|
8745
9070
|
check(ctx) {
|
|
8746
9071
|
for (const comp of ctx.components) {
|
|
@@ -8792,6 +9117,15 @@ var init_portals = __esm({
|
|
|
8792
9117
|
isDraftCtx
|
|
8793
9118
|
);
|
|
8794
9119
|
}
|
|
9120
|
+
if (comp.auth !== void 0) {
|
|
9121
|
+
ctx.addIssue(
|
|
9122
|
+
"warning",
|
|
9123
|
+
"AUTH_ON_NON_PORTAL",
|
|
9124
|
+
`Component "${comp.id}" is a ${comp.componentType}, not a Portal, but declares "auth". Auth is inbound transport auth and is only meaningful on a Portal (a Gateway carries it on the Portal it owns). Move it to the exposed Portal, or remove it.`,
|
|
9125
|
+
comp.id,
|
|
9126
|
+
isDraftCtx
|
|
9127
|
+
);
|
|
9128
|
+
}
|
|
8795
9129
|
const compInterfaces = ctx.interfaces.filter((i) => i.component === comp.id);
|
|
8796
9130
|
for (const intf of compInterfaces) {
|
|
8797
9131
|
for (const m of intf.methods) {
|
|
@@ -11875,6 +12209,95 @@ var init_lint_allows = __esm({
|
|
|
11875
12209
|
}
|
|
11876
12210
|
});
|
|
11877
12211
|
|
|
12212
|
+
// src/core/rules/portal-call-auth.ts
|
|
12213
|
+
var COMPONENT_REF_PREFIX, portalCallAuthRule;
|
|
12214
|
+
var init_portal_call_auth = __esm({
|
|
12215
|
+
"src/core/rules/portal-call-auth.ts"() {
|
|
12216
|
+
"use strict";
|
|
12217
|
+
COMPONENT_REF_PREFIX = "component:";
|
|
12218
|
+
portalCallAuthRule = {
|
|
12219
|
+
name: "portal-call-auth",
|
|
12220
|
+
description: "Hardens authenticated cross-service calls. An OUTBOUND narrative `call` into ANOTHER component's Portal whose auth is not `none` must (a) be made by an Adapter \u2014 the only block that does external I/O \u2014 and (b) declare the credential source it presents via the step's `auth.from`. Absence of a source warns PORTAL_AUTH_UNMET; a non-Adapter presenter warns AUTH_PRESENTER_NOT_ADAPTER. When `auth.from` is a modeled reference (`component:<id>`) it must resolve to an Adapter/Store the presenter is wired to (UNKNOWN_AUTH_SOURCE / AUTH_SOURCE_NOT_PROVIDER / AUTH_SOURCE_UNWIRED). The actual secret is never stored in the spec. Dispatch steps (a portal's OWN inbound routing) and self-calls are not cross-service calls and are excluded.",
|
|
12221
|
+
codes: [
|
|
12222
|
+
{ code: "PORTAL_AUTH_UNMET", defaultSeverity: "warning", summary: "A narrative call into another component's authed Portal does not declare where its credential loads from" },
|
|
12223
|
+
{ code: "AUTH_PRESENTER_NOT_ADAPTER", defaultSeverity: "warning", summary: "A non-Adapter component authenticates an outbound call to a portal (external I/O must go through an Adapter)" },
|
|
12224
|
+
{ code: "UNKNOWN_AUTH_SOURCE", defaultSeverity: "warning", summary: "auth.from references a component: source that does not exist" },
|
|
12225
|
+
{ code: "AUTH_SOURCE_NOT_PROVIDER", defaultSeverity: "warning", summary: "auth.from references a component that is not an Adapter or Store" },
|
|
12226
|
+
{ code: "AUTH_SOURCE_UNWIRED", defaultSeverity: "warning", summary: "The presenter declares a component: credential source it does not depend on or own" }
|
|
12227
|
+
],
|
|
12228
|
+
check(ctx) {
|
|
12229
|
+
for (const impl of ctx.implementations) {
|
|
12230
|
+
const ownComponent = ctx.interfaceMap.get(impl.contract)?.component;
|
|
12231
|
+
const presenter = ownComponent ? ctx.componentMap.get(ownComponent) : void 0;
|
|
12232
|
+
const draft = ctx.isImplementationDraft(impl);
|
|
12233
|
+
for (const method2 of impl.methods ?? []) {
|
|
12234
|
+
for (const step of method2.narrative ?? []) {
|
|
12235
|
+
if (step.type !== "call") continue;
|
|
12236
|
+
if (!step.targetComponent || step.targetComponent === ownComponent) continue;
|
|
12237
|
+
const target = ctx.componentMap.get(step.targetComponent);
|
|
12238
|
+
if (!target || target.componentType !== "Portal") continue;
|
|
12239
|
+
if (!target.auth || target.auth.scheme === "none") continue;
|
|
12240
|
+
if (presenter && presenter.componentType !== "Adapter") {
|
|
12241
|
+
ctx.addIssue(
|
|
12242
|
+
"warning",
|
|
12243
|
+
"AUTH_PRESENTER_NOT_ADAPTER",
|
|
12244
|
+
`Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": "${presenter.id}" (${presenter.componentType}) authenticates an outbound call to portal "${target.id}". An authenticated cross-service call is external I/O and must be made by an Adapter (the only block that does external I/O) \u2014 route it through a client Adapter.`,
|
|
12245
|
+
impl.id,
|
|
12246
|
+
draft
|
|
12247
|
+
);
|
|
12248
|
+
}
|
|
12249
|
+
const from = step.auth?.from;
|
|
12250
|
+
if (!from) {
|
|
12251
|
+
ctx.addIssue(
|
|
12252
|
+
"warning",
|
|
12253
|
+
"PORTAL_AUTH_UNMET",
|
|
12254
|
+
`Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}" calls the authenticated portal "${target.id}" (auth scheme: ${target.auth.scheme}) but does not declare where its credential is loaded from. Add the credential source (the step's auth.from \u2014 a secret-store component, env var, config key, or vault ref), or drop the portal's auth if it needs none.`,
|
|
12255
|
+
impl.id,
|
|
12256
|
+
draft
|
|
12257
|
+
);
|
|
12258
|
+
continue;
|
|
12259
|
+
}
|
|
12260
|
+
if (from.startsWith(COMPONENT_REF_PREFIX)) {
|
|
12261
|
+
const srcId = from.slice(COMPONENT_REF_PREFIX.length);
|
|
12262
|
+
const src = ctx.componentMap.get(srcId);
|
|
12263
|
+
if (!src) {
|
|
12264
|
+
ctx.addIssue(
|
|
12265
|
+
"warning",
|
|
12266
|
+
"UNKNOWN_AUTH_SOURCE",
|
|
12267
|
+
`Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": auth.from references component "${srcId}", which does not exist.`,
|
|
12268
|
+
impl.id,
|
|
12269
|
+
draft
|
|
12270
|
+
);
|
|
12271
|
+
} else {
|
|
12272
|
+
if (src.componentType !== "Adapter" && src.componentType !== "Store") {
|
|
12273
|
+
ctx.addIssue(
|
|
12274
|
+
"warning",
|
|
12275
|
+
"AUTH_SOURCE_NOT_PROVIDER",
|
|
12276
|
+
`Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": auth.from references "${srcId}" (${src.componentType}); a credential source must be an Adapter (loads the secret via external I/O) or a Store (holds it).`,
|
|
12277
|
+
impl.id,
|
|
12278
|
+
draft
|
|
12279
|
+
);
|
|
12280
|
+
}
|
|
12281
|
+
const wired = (presenter?.dependsOn ?? []).includes(srcId) || (presenter?.owns ?? []).includes(srcId);
|
|
12282
|
+
if (presenter && !wired) {
|
|
12283
|
+
ctx.addIssue(
|
|
12284
|
+
"warning",
|
|
12285
|
+
"AUTH_SOURCE_UNWIRED",
|
|
12286
|
+
`Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": "${presenter.id}" loads its credential from "${srcId}" but neither depends on nor owns it \u2014 declare the dependsOn edge so the credential wiring is real.`,
|
|
12287
|
+
impl.id,
|
|
12288
|
+
draft
|
|
12289
|
+
);
|
|
12290
|
+
}
|
|
12291
|
+
}
|
|
12292
|
+
}
|
|
12293
|
+
}
|
|
12294
|
+
}
|
|
12295
|
+
}
|
|
12296
|
+
}
|
|
12297
|
+
};
|
|
12298
|
+
}
|
|
12299
|
+
});
|
|
12300
|
+
|
|
11878
12301
|
// src/core/rules/index.ts
|
|
11879
12302
|
function makeScopeFilter(opts) {
|
|
11880
12303
|
const { components, interfaces, implementations, types, scopeSubsystem } = opts;
|
|
@@ -12120,6 +12543,7 @@ var init_rules = __esm({
|
|
|
12120
12543
|
init_hidden_state();
|
|
12121
12544
|
init_dependency_conformance();
|
|
12122
12545
|
init_lint_allows();
|
|
12546
|
+
init_portal_call_auth();
|
|
12123
12547
|
init_source_analysis();
|
|
12124
12548
|
init_types();
|
|
12125
12549
|
init_type_analysis();
|
|
@@ -12141,6 +12565,9 @@ var init_rules = __esm({
|
|
|
12141
12565
|
narrativeAntipatternsRule,
|
|
12142
12566
|
narrativeDetailRule,
|
|
12143
12567
|
portalsRule,
|
|
12568
|
+
// Cross-call auth: a narrative call into an authed Portal must name its
|
|
12569
|
+
// credential source (rides with the portal family).
|
|
12570
|
+
portalCallAuthRule,
|
|
12144
12571
|
stereotypeDepsRule,
|
|
12145
12572
|
patternsRule,
|
|
12146
12573
|
// Facade shape rides with pattern ownership: same §7 doctrine, narrative side.
|
|
@@ -12203,6 +12630,7 @@ var init_rules = __esm({
|
|
|
12203
12630
|
// L4 expectations: implementations and their code linkage.
|
|
12204
12631
|
MISSING_IMPLEMENTATION_METHOD: "implementations",
|
|
12205
12632
|
MISSING_SOURCE_PATH: "implementations",
|
|
12633
|
+
PORTAL_AUTH_UNMET: "implementations",
|
|
12206
12634
|
MISSING_SOURCE_FILE: "implementations",
|
|
12207
12635
|
SOURCE_PATH_ESCAPES_ROOT: "implementations",
|
|
12208
12636
|
UNREALIZED_METHOD: "implementations",
|
|
@@ -12608,6 +13036,28 @@ function buildGraphModel(level) {
|
|
|
12608
13036
|
...t.subsystem ? { parentId: t.subsystem } : {}
|
|
12609
13037
|
});
|
|
12610
13038
|
}
|
|
13039
|
+
const componentByInterface = /* @__PURE__ */ new Map();
|
|
13040
|
+
for (const c of model.components) {
|
|
13041
|
+
for (const intf of c.interfaces) componentByInterface.set(intf.id, c.id);
|
|
13042
|
+
}
|
|
13043
|
+
for (const impl of loadImplementationSpecs()) {
|
|
13044
|
+
const componentId = componentByInterface.get(impl.contract);
|
|
13045
|
+
if (!componentId) continue;
|
|
13046
|
+
nodes.push({
|
|
13047
|
+
id: impl.id,
|
|
13048
|
+
label: impl.name,
|
|
13049
|
+
kind: "implementation",
|
|
13050
|
+
level: 4,
|
|
13051
|
+
parentId: componentId,
|
|
13052
|
+
...impl.status ? { status: impl.status } : {}
|
|
13053
|
+
});
|
|
13054
|
+
candidates.push({ from: componentId, to: impl.id, edgeKind: "owns" });
|
|
13055
|
+
}
|
|
13056
|
+
for (const c of model.components) {
|
|
13057
|
+
for (const memberId of c.owns) {
|
|
13058
|
+
candidates.push({ from: c.id, to: memberId, edgeKind: "owns" });
|
|
13059
|
+
}
|
|
13060
|
+
}
|
|
12611
13061
|
for (const e of model.edges) {
|
|
12612
13062
|
candidates.push({ from: e.from, to: e.to, edgeKind: "depends_on" });
|
|
12613
13063
|
}
|
|
@@ -16744,6 +17194,7 @@ NOTICE:
|
|
|
16744
17194
|
type: import_zod9.z.enum(["local", "call", "dispatch", "branch", "switch", "loop", "try", "parallel", "jump", "return", "throw"]),
|
|
16745
17195
|
targetComponent: import_zod9.z.string().optional().describe("call/dispatch: L2 component id (for dispatch, the Portal routed through)"),
|
|
16746
17196
|
targetMethod: import_zod9.z.string().optional().describe("call: method name on the target"),
|
|
17197
|
+
auth: import_zod9.z.object({ from: import_zod9.z.string(), note: import_zod9.z.string().optional() }).optional().describe("call/dispatch: the credential this step presents to an AUTHED callee Portal and WHERE it loads from (`from`). Opaque form (env:API_KEY, a config key, vault:path) = a design note wairon never resolves; modeled form `component:<id>` references the Adapter/Store that provides the secret and is validated (must resolve, be an Adapter/Store, and be wired to the presenter). Absence on a call into a Portal whose auth \u2260 none warns (PORTAL_AUTH_UNMET). The authenticated call itself should be made by an Adapter (AUTH_PRESENTER_NOT_ADAPTER)."),
|
|
16747
17198
|
detach: import_zod9.z.boolean().optional().describe("call/dispatch: fire-and-forget \u2014 issue the call and continue without awaiting the result (no later step consumes it)"),
|
|
16748
17199
|
capability: import_zod9.z.string().optional().describe("dispatch: the capability routed through the target Portal's dispatch table (validated against it \u2014 UNSERVED_CAPABILITY)"),
|
|
16749
17200
|
assertsGuarantees: import_zod9.z.array(import_zod9.z.string().min(1)).optional().describe("Semantic guarantees this step relies on \u2014 each must be declared in the called method's L3 guarantees (NARRATIVE_SEMANTIC_UNBACKED otherwise). Builtin tokens: idempotent | atomic | transactional | exactly-once; extension packs may declare more (any other token is UNKNOWN_GUARANTEE)"),
|
|
@@ -25456,6 +25907,7 @@ init_validation();
|
|
|
25456
25907
|
init_diagram();
|
|
25457
25908
|
init_loader();
|
|
25458
25909
|
init_extensions();
|
|
25910
|
+
init_types();
|
|
25459
25911
|
init_server();
|
|
25460
25912
|
|
|
25461
25913
|
// src/git/config.ts
|
|
@@ -25976,7 +26428,11 @@ var hostCore = {
|
|
|
25976
26428
|
checkDeclarativePack: (raw) => {
|
|
25977
26429
|
const result = DeclarativePackSchema.safeParse(raw);
|
|
25978
26430
|
return result.success ? null : result.error.issues[0]?.message ?? "shape mismatch";
|
|
25979
|
-
}
|
|
26431
|
+
},
|
|
26432
|
+
/** The ids of wairon's built-in architectural profiles, read from the core rules
|
|
26433
|
+
* registry's built-in profile set (BUILTIN_PROFILES) — a pure, side-effect-free
|
|
26434
|
+
* read of a bundled constant. */
|
|
26435
|
+
builtinProfileIds: () => [...BUILTIN_PROFILES]
|
|
25980
26436
|
};
|
|
25981
26437
|
function validateProjectAsComplete() {
|
|
25982
26438
|
const config = loadProjectConfig();
|
|
@@ -26385,7 +26841,12 @@ function probe(loadRef, baseRoot, scope, displayRef) {
|
|
|
26385
26841
|
ref: displayRef,
|
|
26386
26842
|
profiles: Object.keys(loaded.profiles).length,
|
|
26387
26843
|
languages: Object.keys(loaded.languages).length,
|
|
26388
|
-
rules: loaded.rules.length
|
|
26844
|
+
rules: loaded.rules.length,
|
|
26845
|
+
// Structured id lists paralleling the counts above — a UI can present a
|
|
26846
|
+
// pack's profiles/languages/rules as choices instead of a bare number.
|
|
26847
|
+
profileIds: Object.keys(loaded.profiles),
|
|
26848
|
+
languageIds: Object.keys(loaded.languages),
|
|
26849
|
+
ruleIds: loaded.rules.map((r) => r.name)
|
|
26389
26850
|
};
|
|
26390
26851
|
}
|
|
26391
26852
|
function writeFileAtomic(file, content) {
|
|
@@ -26415,6 +26876,31 @@ function storeListGlobalPacks() {
|
|
|
26415
26876
|
);
|
|
26416
26877
|
return [...instance, ...image];
|
|
26417
26878
|
}
|
|
26879
|
+
function storeListAvailableProfiles() {
|
|
26880
|
+
const out = [];
|
|
26881
|
+
const seen = /* @__PURE__ */ new Set();
|
|
26882
|
+
const emit = (id, source, family) => {
|
|
26883
|
+
const key = JSON.stringify([id, source]);
|
|
26884
|
+
if (seen.has(key)) return;
|
|
26885
|
+
seen.add(key);
|
|
26886
|
+
out.push(family ? { id, source, family } : { id, source });
|
|
26887
|
+
};
|
|
26888
|
+
for (const id of hostCore.builtinProfileIds()) emit(id, "builtin");
|
|
26889
|
+
const scanTier = (dir) => {
|
|
26890
|
+
for (const full of hostCore.discoverPacks(dir)) {
|
|
26891
|
+
try {
|
|
26892
|
+
const loaded = hostCore.loadExtensionPacks([{ ref: full, scope: "global" }], path45.dirname(full));
|
|
26893
|
+
if (loaded.errors.length) continue;
|
|
26894
|
+
const source = loaded.packNames[0] ?? path45.basename(full);
|
|
26895
|
+
for (const [id, def] of Object.entries(loaded.profiles)) emit(id, source, def.family);
|
|
26896
|
+
} catch {
|
|
26897
|
+
}
|
|
26898
|
+
}
|
|
26899
|
+
};
|
|
26900
|
+
scanTier(hostCore.globalPacksDir());
|
|
26901
|
+
scanTier(imagePacksDir());
|
|
26902
|
+
return out;
|
|
26903
|
+
}
|
|
26418
26904
|
function readPackContent(full) {
|
|
26419
26905
|
const st = fs36.statSync(full);
|
|
26420
26906
|
if (st.isFile()) return fs36.readFileSync(full, "utf8");
|
|
@@ -26601,6 +27087,23 @@ function installProjectPack(cfg, credential, project2, name, content) {
|
|
|
26601
27087
|
requireCap(cfg, credential, "project:admin", "project", project2, "Forbidden \u2014 installing a project pack requires project:admin over the project");
|
|
26602
27088
|
return executeApprovedInstallProjectPack(cfg, project2, name, content);
|
|
26603
27089
|
}
|
|
27090
|
+
function listAvailableProfiles(cfg, credential) {
|
|
27091
|
+
requirePrincipal2(cfg, credential);
|
|
27092
|
+
return storeListAvailableProfiles();
|
|
27093
|
+
}
|
|
27094
|
+
function listAdoptableProjectPacks(cfg, credential, project2) {
|
|
27095
|
+
requireCap(cfg, credential, "project:read", "project", project2, "Forbidden \u2014 listing adoptable packs requires project:read over the project");
|
|
27096
|
+
return storeListGlobalPacks();
|
|
27097
|
+
}
|
|
27098
|
+
function adoptProjectPack(cfg, credential, project2, name) {
|
|
27099
|
+
requireCap(cfg, credential, "project:admin", "project", project2, "Forbidden \u2014 adopting a project pack requires project:admin over the project");
|
|
27100
|
+
const resolution = executeApprovedResolveGlobalPacks([name]);
|
|
27101
|
+
if (resolution.resolved.length === 0) {
|
|
27102
|
+
throw new Error(`no such server-global pack "${name}" \u2014 cannot adopt a pack the instance does not carry`);
|
|
27103
|
+
}
|
|
27104
|
+
const resolved = resolution.resolved[0];
|
|
27105
|
+
return executeApprovedInstallProjectPack(cfg, project2, resolved.name, resolved.content);
|
|
27106
|
+
}
|
|
26604
27107
|
function executeApprovedListProjectPacks(cfg, project2) {
|
|
26605
27108
|
return runWithProjectRoot(boundProject2(cfg, project2), () => storeListProjectPacks());
|
|
26606
27109
|
}
|
|
@@ -27521,6 +28024,7 @@ function removeIdentityProviderRecord(dataDir, id) {
|
|
|
27521
28024
|
}
|
|
27522
28025
|
var PROJECT_CREATE_CAPABILITY = "project:create";
|
|
27523
28026
|
var PROJECT_WRITE_CAPABILITY = "project:write";
|
|
28027
|
+
var PROJECT_READ_CAPABILITY = "project:read";
|
|
27524
28028
|
var POLICY_MANAGE_CAPABILITY = "project:admin";
|
|
27525
28029
|
function carriesInstancePermission(cfg, principal, capability) {
|
|
27526
28030
|
return authorize(cfg.dataDir, principal, capability, "instance", "").value === "yes";
|
|
@@ -27585,6 +28089,19 @@ function recordProjectProfileSelection(root, selection) {
|
|
|
27585
28089
|
writeYamlFile(AI_PATHS.projectConfig(), raw);
|
|
27586
28090
|
});
|
|
27587
28091
|
}
|
|
28092
|
+
function readProjectType(root) {
|
|
28093
|
+
return runWithProjectRoot(root, () => {
|
|
28094
|
+
const raw = readYamlFile(AI_PATHS.projectConfig());
|
|
28095
|
+
return raw?.projectType ?? "backend";
|
|
28096
|
+
});
|
|
28097
|
+
}
|
|
28098
|
+
function writeProjectType(root, projectType) {
|
|
28099
|
+
runWithProjectRoot(root, () => {
|
|
28100
|
+
const raw = readYamlFile(AI_PATHS.projectConfig()) ?? {};
|
|
28101
|
+
raw["projectType"] = projectType;
|
|
28102
|
+
writeYamlFile(AI_PATHS.projectConfig(), raw);
|
|
28103
|
+
});
|
|
28104
|
+
}
|
|
27588
28105
|
function requestPackNames(request) {
|
|
27589
28106
|
const sel = request.profileSelection;
|
|
27590
28107
|
if (!sel) return [];
|
|
@@ -27779,6 +28296,32 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
|
|
|
27779
28296
|
);
|
|
27780
28297
|
return result;
|
|
27781
28298
|
}
|
|
28299
|
+
function getProjectConfig(cfg, credential, projectId) {
|
|
28300
|
+
const principal = requirePrincipal4(cfg, credential);
|
|
28301
|
+
if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY, "project", projectId).value !== "yes") {
|
|
28302
|
+
throw new ForbiddenError(
|
|
28303
|
+
"reading a project's configuration requires project:read over the project"
|
|
28304
|
+
);
|
|
28305
|
+
}
|
|
28306
|
+
const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
|
|
28307
|
+
if (!root) throw new Error(`Unknown project "${projectId}".`);
|
|
28308
|
+
const projectType = readProjectType(root);
|
|
28309
|
+
const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
|
|
28310
|
+
return { projectType, locked };
|
|
28311
|
+
}
|
|
28312
|
+
function setProjectType(cfg, credential, projectId, projectType) {
|
|
28313
|
+
const principal = requirePrincipal4(cfg, credential);
|
|
28314
|
+
if (authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY, "project", projectId).value !== "yes") {
|
|
28315
|
+
throw new ForbiddenError(
|
|
28316
|
+
"changing a project's configuration requires project:write over the project"
|
|
28317
|
+
);
|
|
28318
|
+
}
|
|
28319
|
+
const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
|
|
28320
|
+
if (!root) throw new Error(`Unknown project "${projectId}".`);
|
|
28321
|
+
writeProjectType(root, projectType);
|
|
28322
|
+
const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
|
|
28323
|
+
return { projectType, locked };
|
|
28324
|
+
}
|
|
27782
28325
|
function getPackPolicy(cfg, credential) {
|
|
27783
28326
|
requirePrincipal4(cfg, credential);
|
|
27784
28327
|
return getPackPolicyRecord(cfg.dataDir) ?? PERMISSIVE_DEFAULT_POLICY;
|
|
@@ -27842,7 +28385,7 @@ function handlePolicyRequest(cfg, credential, req, res, body, url) {
|
|
|
27842
28385
|
|
|
27843
28386
|
// src/server/identity.ts
|
|
27844
28387
|
var PROJECT_ADMIN_CAPABILITY2 = "project:admin";
|
|
27845
|
-
var
|
|
28388
|
+
var PROJECT_READ_CAPABILITY2 = "project:read";
|
|
27846
28389
|
var PROJECT_WRITE_CAPABILITY2 = "project:write";
|
|
27847
28390
|
function requireInstanceProjectAdmin(cfg, principal, what) {
|
|
27848
28391
|
if (authorize(cfg.dataDir, principal, PROJECT_ADMIN_CAPABILITY2, "instance", "").value !== "yes") {
|
|
@@ -27988,7 +28531,7 @@ function revokeToken(cfg, credential, tokenId) {
|
|
|
27988
28531
|
}
|
|
27989
28532
|
function mintSelfToken(cfg, credential, projectId, write) {
|
|
27990
28533
|
const principal = requirePrincipal5(cfg, credential);
|
|
27991
|
-
if (authorize(cfg.dataDir, principal,
|
|
28534
|
+
if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2, "project", projectId).value !== "yes") {
|
|
27992
28535
|
throw new ForbiddenError("caller lacks project:read on the requested project");
|
|
27993
28536
|
}
|
|
27994
28537
|
if (write && authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY2, "project", projectId).value !== "yes") {
|
|
@@ -28634,7 +29177,7 @@ function audienceDistance(resolution, targetProjectId) {
|
|
|
28634
29177
|
// src/server/landscape.ts
|
|
28635
29178
|
var yamlLib = __toESM(require("js-yaml"));
|
|
28636
29179
|
var PROJECT_ADMIN_CAPABILITY3 = "project:admin";
|
|
28637
|
-
var
|
|
29180
|
+
var PROJECT_READ_CAPABILITY3 = "project:read";
|
|
28638
29181
|
var PROJECT_WRITE_CAPABILITY3 = "project:write";
|
|
28639
29182
|
function requirePrincipal6(cfg, credential) {
|
|
28640
29183
|
const principal = authenticateCredential(cfg.dataDir, credential);
|
|
@@ -28648,7 +29191,7 @@ function readView(cfg, principal) {
|
|
|
28648
29191
|
if (isInstanceAdmin(principal)) {
|
|
28649
29192
|
return { all: true, projectIds: /* @__PURE__ */ new Set(), unitIds: /* @__PURE__ */ new Set(), contextUnitIds: /* @__PURE__ */ new Set() };
|
|
28650
29193
|
}
|
|
28651
|
-
const read = visibleScopes(cfg.dataDir, principal,
|
|
29194
|
+
const read = visibleScopes(cfg.dataDir, principal, PROJECT_READ_CAPABILITY3);
|
|
28652
29195
|
const admin = visibleScopes(cfg.dataDir, principal, PROJECT_ADMIN_CAPABILITY3);
|
|
28653
29196
|
const unitIds = /* @__PURE__ */ new Set([...actionableUnitIds(read), ...actionableUnitIds(admin)]);
|
|
28654
29197
|
const contextUnitIds = new Set(
|
|
@@ -28663,13 +29206,13 @@ function readView(cfg, principal) {
|
|
|
28663
29206
|
}
|
|
28664
29207
|
function requireLandscapeReach(cfg, principal, view, what) {
|
|
28665
29208
|
if (view.all || view.projectIds.size > 0 || view.unitIds.size > 0) return;
|
|
28666
|
-
if (permitsCap(cfg, principal,
|
|
29209
|
+
if (permitsCap(cfg, principal, PROJECT_READ_CAPABILITY3, "instance", "") || permitsCap(cfg, principal, PROJECT_ADMIN_CAPABILITY3, "instance", "")) {
|
|
28667
29210
|
return;
|
|
28668
29211
|
}
|
|
28669
29212
|
throw new ForbiddenError(`${what} requires project:read reach`);
|
|
28670
29213
|
}
|
|
28671
29214
|
function requireObserverScope(cfg, principal, observerProjectId) {
|
|
28672
|
-
const covered = permitsCap(cfg, principal,
|
|
29215
|
+
const covered = permitsCap(cfg, principal, PROJECT_READ_CAPABILITY3, "project", observerProjectId) || permitsCap(cfg, principal, PROJECT_WRITE_CAPABILITY3, "project", observerProjectId);
|
|
28673
29216
|
if (!covered) {
|
|
28674
29217
|
throw new ForbiddenError("caller lacks scope over the current project");
|
|
28675
29218
|
}
|
|
@@ -30706,6 +31249,15 @@ function installGlobalPackArchive2(cfg, credential, archive, name) {
|
|
|
30706
31249
|
function installProjectPackArchive2(cfg, credential, project2, archive, name) {
|
|
30707
31250
|
return installProjectPackArchive(cfg, credential, project2, archive, name);
|
|
30708
31251
|
}
|
|
31252
|
+
function listAvailableProfiles2(cfg, credential) {
|
|
31253
|
+
return listAvailableProfiles(cfg, credential);
|
|
31254
|
+
}
|
|
31255
|
+
function listAdoptableProjectPacks2(cfg, credential, project2) {
|
|
31256
|
+
return listAdoptableProjectPacks(cfg, credential, project2);
|
|
31257
|
+
}
|
|
31258
|
+
function adoptProjectPack2(cfg, credential, project2, name) {
|
|
31259
|
+
return adoptProjectPack(cfg, credential, project2, name);
|
|
31260
|
+
}
|
|
30709
31261
|
function getPackPolicy2(cfg, credential) {
|
|
30710
31262
|
return getPackPolicy(cfg, credential);
|
|
30711
31263
|
}
|
|
@@ -30718,6 +31270,12 @@ function evaluateProjectPolicy2(cfg, credential, projectId) {
|
|
|
30718
31270
|
function reconcileProjectPolicy2(cfg, credential, projectId) {
|
|
30719
31271
|
return reconcileProjectPolicy(cfg, credential, projectId);
|
|
30720
31272
|
}
|
|
31273
|
+
function getProjectConfig2(cfg, credential, projectId) {
|
|
31274
|
+
return getProjectConfig(cfg, credential, projectId);
|
|
31275
|
+
}
|
|
31276
|
+
function setProjectType2(cfg, credential, projectId, projectType) {
|
|
31277
|
+
return setProjectType(cfg, credential, projectId, projectType);
|
|
31278
|
+
}
|
|
30721
31279
|
function listProducers2(cfg, credential, project2) {
|
|
30722
31280
|
return listProducers(cfg, credential, project2);
|
|
30723
31281
|
}
|
|
@@ -31839,7 +32397,7 @@ function getWebProjectCanvasModel(cfg, sessionId, projectId) {
|
|
|
31839
32397
|
}
|
|
31840
32398
|
return runWithProjectRoot(root, () => hostCore.buildCanvasDataModel());
|
|
31841
32399
|
}
|
|
31842
|
-
function getWebProjectOpenApi(cfg, sessionId, projectId) {
|
|
32400
|
+
function getWebProjectOpenApi(cfg, sessionId, projectId, portalId) {
|
|
31843
32401
|
const principal = authenticateSession(cfg.dataDir, sessionId);
|
|
31844
32402
|
if (!principal.authenticated) throw new UnauthenticatedError();
|
|
31845
32403
|
if (!listProjects2(cfg, sessionId).some((r) => r.id === projectId)) {
|
|
@@ -31848,7 +32406,19 @@ function getWebProjectOpenApi(cfg, sessionId, projectId) {
|
|
|
31848
32406
|
const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
|
|
31849
32407
|
if (!root) throw new ForbiddenError("project not authorized or unknown");
|
|
31850
32408
|
const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface("project", "openapi"));
|
|
31851
|
-
|
|
32409
|
+
const specs = result.renderedSet ?? (result.rendered ? [{ portalId: "", name: projectId, document: result.rendered }] : []);
|
|
32410
|
+
if (specs.length === 0) return swaggerUiPage("{}", projectId);
|
|
32411
|
+
if (portalId) {
|
|
32412
|
+
const sel = specs.find((s) => s.portalId === portalId) ?? specs[0];
|
|
32413
|
+
return swaggerUiPage(sel.document, sel.name);
|
|
32414
|
+
}
|
|
32415
|
+
if (specs.length === 1) return swaggerUiPage(specs[0].document, specs[0].name);
|
|
32416
|
+
return openApiIndexPage(projectId, specs);
|
|
32417
|
+
}
|
|
32418
|
+
function openApiIndexPage(projectId, specs) {
|
|
32419
|
+
const esc2 = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
32420
|
+
const items = specs.map((s) => `<li><a href="/web/openapi?projectId=${encodeURIComponent(projectId)}&spec=${encodeURIComponent(s.portalId)}">${esc2(s.name)}</a> <code>${esc2(s.portalId)}</code></li>`).join("");
|
|
32421
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${esc2(projectId)} \u2014 API specs</title><style>body{font-family:system-ui,sans-serif;max-width:680px;margin:48px auto;padding:0 20px;color:#e6e6e6;background:#161616}h1{font-size:20px}a{color:#6ea8fe;text-decoration:none}a:hover{text-decoration:underline}li{margin:10px 0}code{color:#8a94a6;font-size:12px;margin-left:8px}</style></head><body><h1>${esc2(projectId)} \u2014 API specs</h1><p>This project exposes ${specs.length} separate public APIs, each with its own OpenAPI document and auth:</p><ul>${items}</ul></body></html>`;
|
|
31852
32422
|
}
|
|
31853
32423
|
function reshapeLandscapeGraph(model, level) {
|
|
31854
32424
|
const unitNodeIds = new Set(model.nodes.filter((n) => n.nodeKind === "orgUnit").map((n) => n.id));
|
|
@@ -33999,6 +34569,15 @@ function opsInstallGlobalPackArchive(cfg, sessionId, req, body, res) {
|
|
|
33999
34569
|
function opsInstallProjectPackArchive(cfg, sessionId, req, url, body, res) {
|
|
34000
34570
|
sendJson(res, 200, installProjectPackArchive2(cfg, sessionId, q(url, "projectId") ?? "", archiveBody(body), packNameOverride(req)));
|
|
34001
34571
|
}
|
|
34572
|
+
function opsListAvailableProfiles(cfg, sessionId, res) {
|
|
34573
|
+
sendJson(res, 200, { profiles: listAvailableProfiles2(cfg, sessionId) });
|
|
34574
|
+
}
|
|
34575
|
+
function opsListAdoptableProjectPacks(cfg, sessionId, url, res) {
|
|
34576
|
+
sendJson(res, 200, { packs: listAdoptableProjectPacks2(cfg, sessionId, q(url, "projectId") ?? "") });
|
|
34577
|
+
}
|
|
34578
|
+
function opsAdoptProjectPack(cfg, sessionId, body, res) {
|
|
34579
|
+
sendJson(res, 200, adoptProjectPack2(cfg, sessionId, String(body?.projectId ?? ""), String(body?.name ?? "")));
|
|
34580
|
+
}
|
|
34002
34581
|
function opsGetPackPolicy(cfg, sessionId, res) {
|
|
34003
34582
|
sendJson(res, 200, getPackPolicy2(cfg, sessionId));
|
|
34004
34583
|
}
|
|
@@ -34011,6 +34590,12 @@ function opsPolicyEvaluate(cfg, sessionId, url, res) {
|
|
|
34011
34590
|
function opsPolicyReconcile(cfg, sessionId, body, res) {
|
|
34012
34591
|
sendJson(res, 200, reconcileProjectPolicy2(cfg, sessionId, String(body?.projectId ?? "")));
|
|
34013
34592
|
}
|
|
34593
|
+
function opsGetProjectConfig(cfg, sessionId, url, res) {
|
|
34594
|
+
sendJson(res, 200, getProjectConfig2(cfg, sessionId, q(url, "projectId") ?? ""));
|
|
34595
|
+
}
|
|
34596
|
+
function opsSetProjectConfig(cfg, sessionId, body, res) {
|
|
34597
|
+
sendJson(res, 200, setProjectType2(cfg, sessionId, String(body?.projectId ?? ""), String(body?.projectType ?? "")));
|
|
34598
|
+
}
|
|
34014
34599
|
function opsListProducers(cfg, sessionId, url, res) {
|
|
34015
34600
|
sendJson(res, 200, { producers: listProducers2(cfg, sessionId, q(url, "projectId") ?? "") });
|
|
34016
34601
|
}
|
|
@@ -34195,7 +34780,8 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
|
|
|
34195
34780
|
}
|
|
34196
34781
|
if (req.method === "GET" && parts.length === 2 && parts[1] === "openapi") {
|
|
34197
34782
|
const projectId = url.searchParams.get("projectId") ?? "";
|
|
34198
|
-
const
|
|
34783
|
+
const spec = url.searchParams.get("spec") ?? void 0;
|
|
34784
|
+
const html = getWebProjectOpenApi(cfg, sessionId, projectId, spec);
|
|
34199
34785
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
34200
34786
|
res.end(html);
|
|
34201
34787
|
return;
|
|
@@ -34237,6 +34823,18 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
|
|
|
34237
34823
|
if (req.method === "POST" && parts.length === 4 && parts[2] === "packs" && parts[3] === "remove") {
|
|
34238
34824
|
return opsRemoveProjectPack(cfg, sessionId, body, res);
|
|
34239
34825
|
}
|
|
34826
|
+
if (req.method === "GET" && parts.length === 4 && parts[2] === "packs" && parts[3] === "adoptable") {
|
|
34827
|
+
return opsListAdoptableProjectPacks(cfg, sessionId, url, res);
|
|
34828
|
+
}
|
|
34829
|
+
if (req.method === "POST" && parts.length === 4 && parts[2] === "packs" && parts[3] === "adopt") {
|
|
34830
|
+
return opsAdoptProjectPack(cfg, sessionId, body, res);
|
|
34831
|
+
}
|
|
34832
|
+
if (req.method === "GET" && parts.length === 3 && parts[2] === "config") {
|
|
34833
|
+
return opsGetProjectConfig(cfg, sessionId, url, res);
|
|
34834
|
+
}
|
|
34835
|
+
if (req.method === "POST" && parts.length === 3 && parts[2] === "config") {
|
|
34836
|
+
return opsSetProjectConfig(cfg, sessionId, body, res);
|
|
34837
|
+
}
|
|
34240
34838
|
if (req.method === "GET" && parts.length === 3 && parts[2] === "policy") {
|
|
34241
34839
|
return opsPolicyEvaluate(cfg, sessionId, url, res);
|
|
34242
34840
|
}
|
|
@@ -34350,6 +34948,9 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
|
|
|
34350
34948
|
if (req.method === "POST" && parts.length === 4 && parts[2] === "packs" && parts[3] === "remove") {
|
|
34351
34949
|
return opsRemoveGlobalPack(cfg, sessionId, body, res);
|
|
34352
34950
|
}
|
|
34951
|
+
if (req.method === "GET" && parts.length === 3 && parts[2] === "profiles") {
|
|
34952
|
+
return opsListAvailableProfiles(cfg, sessionId, res);
|
|
34953
|
+
}
|
|
34353
34954
|
if (req.method === "GET" && parts.length === 3 && parts[2] === "policy") {
|
|
34354
34955
|
return opsGetPackPolicy(cfg, sessionId, res);
|
|
34355
34956
|
}
|
|
@@ -35151,6 +35752,7 @@ var WEB_MUTATION_PATHS = /* @__PURE__ */ new Set([
|
|
|
35151
35752
|
"/web/projects/packs",
|
|
35152
35753
|
"/web/projects/packs/upload",
|
|
35153
35754
|
"/web/projects/packs/remove",
|
|
35755
|
+
"/web/projects/packs/adopt",
|
|
35154
35756
|
"/web/projects/policy/reconcile",
|
|
35155
35757
|
"/web/projects/producers",
|
|
35156
35758
|
"/web/projects/producers/remove",
|