@wairon/cli 5.0.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 +769 -102
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +525 -57
- 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.
|
|
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.
|
|
@@ -5139,18 +5288,20 @@ var MODEL = __MODEL_JSON__;
|
|
|
5139
5288
|
return markers[id];
|
|
5140
5289
|
}
|
|
5141
5290
|
var collapsed = [];
|
|
5142
|
-
//
|
|
5143
|
-
//
|
|
5144
|
-
//
|
|
5145
|
-
//
|
|
5291
|
+
// Signed fit measure in px: positive = overflowing, negative = headroom.
|
|
5292
|
+
// The header is a flex row whose ONLY flex:1 child is the .spacer, so the
|
|
5293
|
+
// spacer's rendered width IS the free space -- it grows to absorb all slack
|
|
5294
|
+
// and collapses to 0 the instant the row is full. That makes a right-edge
|
|
5295
|
+
// measurement useless (the spacer keeps the trailing controls pinned to the
|
|
5296
|
+
// right padding at every width, so their edge always reads as a bare fit),
|
|
5297
|
+
// and scrollWidth clamps at clientWidth so it can't report headroom either.
|
|
5298
|
+
// So take headroom from the spacer's own (fractional) width, and true
|
|
5299
|
+
// overflow from scrollWidth - clientWidth (only ever > 0 once the spacer has
|
|
5300
|
+
// already collapsed to 0). The two terms are mutually exclusive.
|
|
5146
5301
|
function overflowPx() {
|
|
5147
|
-
var
|
|
5148
|
-
var
|
|
5149
|
-
|
|
5150
|
-
var cr = c.getBoundingClientRect();
|
|
5151
|
-
if (cr.width > 0 && cr.right > edge) edge = cr.right;
|
|
5152
|
-
}
|
|
5153
|
-
return edge - (box.right - 14); // 14 = the header's right padding
|
|
5302
|
+
var spacer = hdr.querySelector('.spacer');
|
|
5303
|
+
var slack = spacer ? spacer.getBoundingClientRect().width : 0;
|
|
5304
|
+
return (hdr.scrollWidth - hdr.clientWidth) - slack;
|
|
5154
5305
|
}
|
|
5155
5306
|
function reflow() {
|
|
5156
5307
|
// Not laid out (hidden tab, non-browser DOM) \u2014 measuring would misfire.
|
|
@@ -5911,10 +6062,12 @@ var MODEL = __MODEL_JSON__;
|
|
|
5911
6062
|
if (kind === 'type' && state.view.kind !== 'types' && state.view.kind !== 'databases') {
|
|
5912
6063
|
state.view = { kind: 'types', id: typesScopeFromView() };
|
|
5913
6064
|
rebuild(true);
|
|
6065
|
+
notifyViewChange();
|
|
5914
6066
|
} else if (kind === 'component' && (state.view.kind === 'types' || state.view.kind === 'databases')) {
|
|
5915
6067
|
var c = compById[id];
|
|
5916
6068
|
state.view = { kind: 'subsystem', id: c ? c.subsystem : null };
|
|
5917
6069
|
rebuild(true);
|
|
6070
|
+
notifyViewChange();
|
|
5918
6071
|
}
|
|
5919
6072
|
state.selectedKind = kind;
|
|
5920
6073
|
state.selected = id;
|
|
@@ -5973,6 +6126,14 @@ var MODEL = __MODEL_JSON__;
|
|
|
5973
6126
|
if (!exposes || !openApiAllowed()) return '';
|
|
5974
6127
|
return '<div class="openbtn"><button class="tbtn" data-openapi-tag="' + esc(tag || '') + '">\\u25A4 View OpenAPI \\u2197</button></div>';
|
|
5975
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
|
+
}
|
|
5976
6137
|
|
|
5977
6138
|
function renderPanel() {
|
|
5978
6139
|
var head = '', body = '';
|
|
@@ -5995,7 +6156,8 @@ var MODEL = __MODEL_JSON__;
|
|
|
5995
6156
|
+ (scopeFocus ? staticChip('current view') : '')
|
|
5996
6157
|
+ chip(c.subsystem, 'subsystem', c.subsystem)
|
|
5997
6158
|
+ (scopeFocus ? '' : openViewButton('component', c.id, c.owns.length > 0))
|
|
5998
|
-
+ openApiButton(componentExposesApi(c), c.
|
|
6159
|
+
+ openApiButton(componentExposesApi(c), c.id)
|
|
6160
|
+
+ openSpecButton('component', c.id);
|
|
5999
6161
|
|
|
6000
6162
|
var linkedTypes = MODEL.types.filter(function (t) { return t.componentClass === c.id; });
|
|
6001
6163
|
if (linkedTypes.length) {
|
|
@@ -6005,6 +6167,14 @@ var MODEL = __MODEL_JSON__;
|
|
|
6005
6167
|
}
|
|
6006
6168
|
body += '<p class="desc">' + esc(c.description) + '</p>';
|
|
6007
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
|
+
|
|
6008
6178
|
var depInner = (c.dependsOn.length ? c.dependsOn.map(function (d) { return chip(d, 'component', d); }).join('') : '<span class="desc">none</span>')
|
|
6009
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>' : '');
|
|
6010
6180
|
body += section('Dependencies', c.dependsOn.length + c.owns.length, depInner, true);
|
|
@@ -6079,7 +6249,8 @@ var MODEL = __MODEL_JSON__;
|
|
|
6079
6249
|
MODEL.types.forEach(function (t2) { if (t2.id === focusId) ty = t2; });
|
|
6080
6250
|
if (ty) {
|
|
6081
6251
|
head = '<h2>' + esc(ty.name) + '</h2>' + staticChip('\\u00AB' + ty.kind + '\\u00BB')
|
|
6082
|
-
+ (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);
|
|
6083
6254
|
|
|
6084
6255
|
if (ty.componentClass) {
|
|
6085
6256
|
head += '<div style="margin-top:6px"><b style="font-size:11px">Class Component:</b> ' + chip(ty.componentClass, 'component', ty.componentClass) + '</div>';
|
|
@@ -6155,7 +6326,8 @@ var MODEL = __MODEL_JSON__;
|
|
|
6155
6326
|
+ (s.status ? staticChip(s.status) : '')
|
|
6156
6327
|
+ (scopeFocus ? staticChip('current view') : '')
|
|
6157
6328
|
+ (scopeFocus ? '' : openViewButton('subsystem', s.id, subKids > 0))
|
|
6158
|
-
+ openApiButton(subsystemExposesApi(s.id), '')
|
|
6329
|
+
+ openApiButton(subsystemExposesApi(s.id), '')
|
|
6330
|
+
+ openSpecButton('subsystem', s.id);
|
|
6159
6331
|
body += '<p class="desc">' + esc(s.description) + '</p>';
|
|
6160
6332
|
if (s.trustedLinks.length) {
|
|
6161
6333
|
body += section('Trusted links (fast lanes)', s.trustedLinks.length, s.trustedLinks.map(function (t2) {
|
|
@@ -6213,6 +6385,16 @@ var MODEL = __MODEL_JSON__;
|
|
|
6213
6385
|
});
|
|
6214
6386
|
})(oapis[oi]);
|
|
6215
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
|
+
}
|
|
6216
6398
|
var flows = panel.querySelectorAll('[data-flow-comp]');
|
|
6217
6399
|
for (var j = 0; j < flows.length; j++) {
|
|
6218
6400
|
(function (b) {
|
|
@@ -6225,6 +6407,23 @@ var MODEL = __MODEL_JSON__;
|
|
|
6225
6407
|
}
|
|
6226
6408
|
|
|
6227
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
|
+
})();
|
|
6228
6427
|
})();
|
|
6229
6428
|
</script>
|
|
6230
6429
|
</body>
|
|
@@ -6762,11 +6961,63 @@ function operationFor(method2, closureIds) {
|
|
|
6762
6961
|
}
|
|
6763
6962
|
return op;
|
|
6764
6963
|
}
|
|
6765
|
-
function
|
|
6766
|
-
|
|
6767
|
-
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);
|
|
6768
7018
|
const paths = {};
|
|
6769
|
-
for (const entry of
|
|
7019
|
+
for (const entry of entries) {
|
|
7020
|
+
const security = securityByEntry.get(entry.id);
|
|
6770
7021
|
for (const method2 of entry.methods) {
|
|
6771
7022
|
const endpoint = method2.endpoint;
|
|
6772
7023
|
if (!endpoint || endpoint.transport !== "HTTP") continue;
|
|
@@ -6774,7 +7025,8 @@ function toOpenApi(snapshot) {
|
|
|
6774
7025
|
paths[p] = paths[p] ?? {};
|
|
6775
7026
|
paths[p][endpoint.method.toLowerCase()] = {
|
|
6776
7027
|
tags: [entry.id],
|
|
6777
|
-
...operationFor(method2, closureIds)
|
|
7028
|
+
...operationFor(method2, closureIds),
|
|
7029
|
+
...security ? { security: [security] } : {}
|
|
6778
7030
|
};
|
|
6779
7031
|
}
|
|
6780
7032
|
}
|
|
@@ -6787,19 +7039,41 @@ function toOpenApi(snapshot) {
|
|
|
6787
7039
|
required: t.fields.filter((f) => !f.optional).map((f) => f.name)
|
|
6788
7040
|
};
|
|
6789
7041
|
}
|
|
6790
|
-
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 {
|
|
6791
7048
|
openapi: "3.1.0",
|
|
6792
7049
|
info: {
|
|
6793
|
-
title: snapshot.projectName,
|
|
7050
|
+
title: opts.title ?? snapshot.projectName,
|
|
6794
7051
|
version: snapshot.version ?? "0.0.0",
|
|
6795
7052
|
...snapshot.stateId ? { "x-wairon-state-id": snapshot.stateId } : {},
|
|
6796
7053
|
"x-wairon-origin": snapshot.origin,
|
|
6797
7054
|
"x-wairon-generated-at": snapshot.generatedAt
|
|
6798
7055
|
},
|
|
7056
|
+
...servers ? { servers } : {},
|
|
6799
7057
|
paths,
|
|
6800
|
-
...Object.keys(
|
|
7058
|
+
...Object.keys(components).length ? { components } : {}
|
|
6801
7059
|
};
|
|
6802
|
-
|
|
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
|
+
});
|
|
6803
7077
|
}
|
|
6804
7078
|
function isOpenApiDocument(body) {
|
|
6805
7079
|
try {
|
|
@@ -6821,6 +7095,39 @@ function typeRefFromSchema(schema) {
|
|
|
6821
7095
|
if (typeof t === "string" && t !== "object") return t;
|
|
6822
7096
|
return "json";
|
|
6823
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
|
+
}
|
|
6824
7131
|
function fromOpenApi(document, projectName) {
|
|
6825
7132
|
let parsed;
|
|
6826
7133
|
try {
|
|
@@ -6898,6 +7205,9 @@ function fromOpenApi(document, projectName) {
|
|
|
6898
7205
|
}))
|
|
6899
7206
|
});
|
|
6900
7207
|
}
|
|
7208
|
+
const securitySchemes = parsed.components?.securitySchemes ?? {};
|
|
7209
|
+
const firstScheme = Object.values(securitySchemes)[0];
|
|
7210
|
+
const importedAuth = firstScheme ? authFromSecurityScheme(firstScheme) : void 0;
|
|
6901
7211
|
const entry = {
|
|
6902
7212
|
id: `${projectName}-api`,
|
|
6903
7213
|
name: typeof info.title === "string" ? info.title : projectName,
|
|
@@ -6906,7 +7216,8 @@ function fromOpenApi(document, projectName) {
|
|
|
6906
7216
|
component: `${projectName}-api`,
|
|
6907
7217
|
methods,
|
|
6908
7218
|
details: typeof info.description === "string" ? info.description : `Imported OpenAPI surface of ${projectName}.`,
|
|
6909
|
-
...typeof info.version === "string" ? { version: info.version } : {}
|
|
7219
|
+
...typeof info.version === "string" ? { version: info.version } : {},
|
|
7220
|
+
...importedAuth ? { auth: importedAuth } : {}
|
|
6910
7221
|
};
|
|
6911
7222
|
return SurfaceSnapshotSchema.parse({
|
|
6912
7223
|
projectName,
|
|
@@ -7021,6 +7332,10 @@ function projectOwnSurface(maxAudience) {
|
|
|
7021
7332
|
component: comp.id,
|
|
7022
7333
|
methods,
|
|
7023
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 } : {},
|
|
7024
7339
|
details: raw.details ?? "",
|
|
7025
7340
|
...raw.version ? { version: raw.version } : {},
|
|
7026
7341
|
...raw.stability ? { stability: raw.stability } : {}
|
|
@@ -7064,20 +7379,28 @@ function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
|
|
|
7064
7379
|
function loadSurfaceSnapshots() {
|
|
7065
7380
|
return listSnapshots();
|
|
7066
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
|
+
}
|
|
7067
7393
|
function exportSurface(maxAudience, format, outPath) {
|
|
7068
7394
|
const snapshot = projectOwnSurface(maxAudience);
|
|
7069
|
-
const
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
}
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
writtenTo = path10.resolve(outPath);
|
|
7079
|
-
}
|
|
7080
|
-
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
|
+
};
|
|
7081
7404
|
}
|
|
7082
7405
|
function importSurface(sourcePath, origin) {
|
|
7083
7406
|
const resolved = path10.resolve(sourcePath);
|
|
@@ -8337,13 +8660,16 @@ var init_conformance = __esm({
|
|
|
8337
8660
|
if (isInChainedSubproject(component.subsystem, ctx)) continue;
|
|
8338
8661
|
const draft = ctx.isImplementationDraft(impl);
|
|
8339
8662
|
if (!impl.sourcePath) {
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
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
|
+
}
|
|
8347
8673
|
continue;
|
|
8348
8674
|
}
|
|
8349
8675
|
const lookup = lookups.get(normalizeSourcePath(impl.sourcePath));
|
|
@@ -8732,13 +9058,14 @@ var init_portals = __esm({
|
|
|
8732
9058
|
};
|
|
8733
9059
|
portalsRule = {
|
|
8734
9060
|
name: "portal-endpoints",
|
|
8735
|
-
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).",
|
|
8736
9062
|
codes: [
|
|
8737
9063
|
{ code: "MISSING_PORTAL_TYPE", defaultSeverity: "error", summary: "Portal without a portalType" },
|
|
8738
9064
|
{ code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding" },
|
|
8739
9065
|
{ code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal portalType" },
|
|
8740
9066
|
{ code: "UNEXPECTED_PORTAL_FIELD", defaultSeverity: "error", summary: "Non-Portal component with portalType/basePath" },
|
|
8741
|
-
{ 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)" }
|
|
8742
9069
|
],
|
|
8743
9070
|
check(ctx) {
|
|
8744
9071
|
for (const comp of ctx.components) {
|
|
@@ -8790,6 +9117,15 @@ var init_portals = __esm({
|
|
|
8790
9117
|
isDraftCtx
|
|
8791
9118
|
);
|
|
8792
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
|
+
}
|
|
8793
9129
|
const compInterfaces = ctx.interfaces.filter((i) => i.component === comp.id);
|
|
8794
9130
|
for (const intf of compInterfaces) {
|
|
8795
9131
|
for (const m of intf.methods) {
|
|
@@ -11873,6 +12209,95 @@ var init_lint_allows = __esm({
|
|
|
11873
12209
|
}
|
|
11874
12210
|
});
|
|
11875
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
|
+
|
|
11876
12301
|
// src/core/rules/index.ts
|
|
11877
12302
|
function makeScopeFilter(opts) {
|
|
11878
12303
|
const { components, interfaces, implementations, types, scopeSubsystem } = opts;
|
|
@@ -12118,6 +12543,7 @@ var init_rules = __esm({
|
|
|
12118
12543
|
init_hidden_state();
|
|
12119
12544
|
init_dependency_conformance();
|
|
12120
12545
|
init_lint_allows();
|
|
12546
|
+
init_portal_call_auth();
|
|
12121
12547
|
init_source_analysis();
|
|
12122
12548
|
init_types();
|
|
12123
12549
|
init_type_analysis();
|
|
@@ -12139,6 +12565,9 @@ var init_rules = __esm({
|
|
|
12139
12565
|
narrativeAntipatternsRule,
|
|
12140
12566
|
narrativeDetailRule,
|
|
12141
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,
|
|
12142
12571
|
stereotypeDepsRule,
|
|
12143
12572
|
patternsRule,
|
|
12144
12573
|
// Facade shape rides with pattern ownership: same §7 doctrine, narrative side.
|
|
@@ -12201,6 +12630,7 @@ var init_rules = __esm({
|
|
|
12201
12630
|
// L4 expectations: implementations and their code linkage.
|
|
12202
12631
|
MISSING_IMPLEMENTATION_METHOD: "implementations",
|
|
12203
12632
|
MISSING_SOURCE_PATH: "implementations",
|
|
12633
|
+
PORTAL_AUTH_UNMET: "implementations",
|
|
12204
12634
|
MISSING_SOURCE_FILE: "implementations",
|
|
12205
12635
|
SOURCE_PATH_ESCAPES_ROOT: "implementations",
|
|
12206
12636
|
UNREALIZED_METHOD: "implementations",
|
|
@@ -12606,6 +13036,28 @@ function buildGraphModel(level) {
|
|
|
12606
13036
|
...t.subsystem ? { parentId: t.subsystem } : {}
|
|
12607
13037
|
});
|
|
12608
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
|
+
}
|
|
12609
13061
|
for (const e of model.edges) {
|
|
12610
13062
|
candidates.push({ from: e.from, to: e.to, edgeKind: "depends_on" });
|
|
12611
13063
|
}
|
|
@@ -16742,6 +17194,7 @@ NOTICE:
|
|
|
16742
17194
|
type: import_zod9.z.enum(["local", "call", "dispatch", "branch", "switch", "loop", "try", "parallel", "jump", "return", "throw"]),
|
|
16743
17195
|
targetComponent: import_zod9.z.string().optional().describe("call/dispatch: L2 component id (for dispatch, the Portal routed through)"),
|
|
16744
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)."),
|
|
16745
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)"),
|
|
16746
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)"),
|
|
16747
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)"),
|
|
@@ -22804,8 +23257,8 @@ function walkForPackages(projectRoot2, currentDir, depth, results) {
|
|
|
22804
23257
|
}
|
|
22805
23258
|
}
|
|
22806
23259
|
function pathToId(relPath) {
|
|
22807
|
-
const
|
|
22808
|
-
return
|
|
23260
|
+
const basename12 = path28.basename(relPath);
|
|
23261
|
+
return basename12.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
22809
23262
|
}
|
|
22810
23263
|
function pathToName(relPath) {
|
|
22811
23264
|
const id = pathToId(relPath);
|
|
@@ -25454,6 +25907,7 @@ init_validation();
|
|
|
25454
25907
|
init_diagram();
|
|
25455
25908
|
init_loader();
|
|
25456
25909
|
init_extensions();
|
|
25910
|
+
init_types();
|
|
25457
25911
|
init_server();
|
|
25458
25912
|
|
|
25459
25913
|
// src/git/config.ts
|
|
@@ -25974,7 +26428,11 @@ var hostCore = {
|
|
|
25974
26428
|
checkDeclarativePack: (raw) => {
|
|
25975
26429
|
const result = DeclarativePackSchema.safeParse(raw);
|
|
25976
26430
|
return result.success ? null : result.error.issues[0]?.message ?? "shape mismatch";
|
|
25977
|
-
}
|
|
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]
|
|
25978
26436
|
};
|
|
25979
26437
|
function validateProjectAsComplete() {
|
|
25980
26438
|
const config = loadProjectConfig();
|
|
@@ -26383,7 +26841,12 @@ function probe(loadRef, baseRoot, scope, displayRef) {
|
|
|
26383
26841
|
ref: displayRef,
|
|
26384
26842
|
profiles: Object.keys(loaded.profiles).length,
|
|
26385
26843
|
languages: Object.keys(loaded.languages).length,
|
|
26386
|
-
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)
|
|
26387
26850
|
};
|
|
26388
26851
|
}
|
|
26389
26852
|
function writeFileAtomic(file, content) {
|
|
@@ -26413,6 +26876,71 @@ function storeListGlobalPacks() {
|
|
|
26413
26876
|
);
|
|
26414
26877
|
return [...instance, ...image];
|
|
26415
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
|
+
}
|
|
26904
|
+
function readPackContent(full) {
|
|
26905
|
+
const st = fs36.statSync(full);
|
|
26906
|
+
if (st.isFile()) return fs36.readFileSync(full, "utf8");
|
|
26907
|
+
if (st.isDirectory()) {
|
|
26908
|
+
for (const entry of ["pack.yaml", "pack.yml"]) {
|
|
26909
|
+
const p = path45.join(full, entry);
|
|
26910
|
+
if (fs36.existsSync(p) && fs36.statSync(p).isFile()) return fs36.readFileSync(p, "utf8");
|
|
26911
|
+
}
|
|
26912
|
+
}
|
|
26913
|
+
return null;
|
|
26914
|
+
}
|
|
26915
|
+
function storeResolveGlobalPacks(names) {
|
|
26916
|
+
const index = /* @__PURE__ */ new Map();
|
|
26917
|
+
const indexTier = (dir, tier) => {
|
|
26918
|
+
for (const full of hostCore.discoverPacks(dir)) {
|
|
26919
|
+
const manifestName = probe(full, path45.dirname(full), "global", path45.basename(full)).name;
|
|
26920
|
+
const candidate = { full, manifestName, tier };
|
|
26921
|
+
for (const key of [manifestName, stem(full), path45.basename(full)]) {
|
|
26922
|
+
if (!index.has(key)) index.set(key, candidate);
|
|
26923
|
+
}
|
|
26924
|
+
}
|
|
26925
|
+
};
|
|
26926
|
+
indexTier(hostCore.globalPacksDir(), "instance");
|
|
26927
|
+
indexTier(imagePacksDir(), "image");
|
|
26928
|
+
const resolved = [];
|
|
26929
|
+
const resolvedCanonical = /* @__PURE__ */ new Set();
|
|
26930
|
+
const unresolved = [];
|
|
26931
|
+
for (const requested of new Set(names)) {
|
|
26932
|
+
const candidate = index.get(requested);
|
|
26933
|
+
const content = candidate ? readPackContent(candidate.full) : null;
|
|
26934
|
+
if (!candidate || content == null) {
|
|
26935
|
+
unresolved.push(requested);
|
|
26936
|
+
continue;
|
|
26937
|
+
}
|
|
26938
|
+
if (resolvedCanonical.has(candidate.manifestName)) continue;
|
|
26939
|
+
resolvedCanonical.add(candidate.manifestName);
|
|
26940
|
+
resolved.push({ requestedName: requested, name: candidate.manifestName, content, tier: candidate.tier });
|
|
26941
|
+
}
|
|
26942
|
+
return { resolved, unresolved };
|
|
26943
|
+
}
|
|
26416
26944
|
function storeInstallGlobalPack(name, content) {
|
|
26417
26945
|
assertName(name);
|
|
26418
26946
|
assertDeclarative(content);
|
|
@@ -26559,12 +27087,32 @@ function installProjectPack(cfg, credential, project2, name, content) {
|
|
|
26559
27087
|
requireCap(cfg, credential, "project:admin", "project", project2, "Forbidden \u2014 installing a project pack requires project:admin over the project");
|
|
26560
27088
|
return executeApprovedInstallProjectPack(cfg, project2, name, content);
|
|
26561
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
|
+
}
|
|
26562
27107
|
function executeApprovedListProjectPacks(cfg, project2) {
|
|
26563
27108
|
return runWithProjectRoot(boundProject2(cfg, project2), () => storeListProjectPacks());
|
|
26564
27109
|
}
|
|
26565
27110
|
function executeApprovedInstallProjectPack(cfg, project2, name, content) {
|
|
26566
27111
|
return runWithProjectRoot(boundProject2(cfg, project2), () => storeInstallProjectPack(name, content));
|
|
26567
27112
|
}
|
|
27113
|
+
function executeApprovedResolveGlobalPacks(names) {
|
|
27114
|
+
return storeResolveGlobalPacks(names);
|
|
27115
|
+
}
|
|
26568
27116
|
function removeProjectPack(cfg, credential, project2, name) {
|
|
26569
27117
|
requireCap(cfg, credential, "project:admin", "project", project2, "Forbidden \u2014 removing a project pack requires project:admin over the project");
|
|
26570
27118
|
runWithProjectRoot(boundProject2(cfg, project2), () => storeRemoveProjectPack(name));
|
|
@@ -27476,6 +28024,7 @@ function removeIdentityProviderRecord(dataDir, id) {
|
|
|
27476
28024
|
}
|
|
27477
28025
|
var PROJECT_CREATE_CAPABILITY = "project:create";
|
|
27478
28026
|
var PROJECT_WRITE_CAPABILITY = "project:write";
|
|
28027
|
+
var PROJECT_READ_CAPABILITY = "project:read";
|
|
27479
28028
|
var POLICY_MANAGE_CAPABILITY = "project:admin";
|
|
27480
28029
|
function carriesInstancePermission(cfg, principal, capability) {
|
|
27481
28030
|
return authorize(cfg.dataDir, principal, capability, "instance", "").value === "yes";
|
|
@@ -27515,27 +28064,16 @@ function tryAppendAudit2(cfg, event) {
|
|
|
27515
28064
|
);
|
|
27516
28065
|
}
|
|
27517
28066
|
}
|
|
27518
|
-
|
|
27519
|
-
|
|
27520
|
-
|
|
27521
|
-
function readGlobalPackContent(name) {
|
|
27522
|
-
const dir = hostCore.globalPacksDir();
|
|
27523
|
-
for (const candidate of [path47.join(dir, `${name}.yaml`), path47.join(dir, `${name}.yml`)]) {
|
|
27524
|
-
if (fs38.existsSync(candidate) && fs38.statSync(candidate).isFile()) {
|
|
27525
|
-
return fs38.readFileSync(candidate, "utf8");
|
|
27526
|
-
}
|
|
27527
|
-
}
|
|
27528
|
-
const match = hostCore.discoverPacks(dir).find((ref) => packStem(ref) === name || path47.basename(ref) === name);
|
|
27529
|
-
if (match && fs38.statSync(match).isFile()) return fs38.readFileSync(match, "utf8");
|
|
27530
|
-
return null;
|
|
28067
|
+
var PACK_ENFORCEMENT_MODES = ["warn", "block", "auto_reconcile"];
|
|
28068
|
+
function requiredDefaultNames(policy) {
|
|
28069
|
+
return [.../* @__PURE__ */ new Set([...policy.requiredGlobalPacks, ...policy.defaultProjectPacks])];
|
|
27531
28070
|
}
|
|
27532
28071
|
function installedPackNames(cfg, projectId) {
|
|
27533
28072
|
return executeApprovedListProjectPacks(cfg, projectId).map((d) => d.name);
|
|
27534
28073
|
}
|
|
27535
|
-
function
|
|
27536
|
-
for (const
|
|
27537
|
-
|
|
27538
|
-
if (content) executeApprovedInstallProjectPack(cfg, projectId, name, content);
|
|
28074
|
+
function installResolvedPacks(cfg, projectId, resolution) {
|
|
28075
|
+
for (const pack of resolution.resolved) {
|
|
28076
|
+
executeApprovedInstallProjectPack(cfg, projectId, pack.name, pack.content);
|
|
27539
28077
|
}
|
|
27540
28078
|
}
|
|
27541
28079
|
function readProjectProfileSelection(root) {
|
|
@@ -27551,6 +28089,19 @@ function recordProjectProfileSelection(root, selection) {
|
|
|
27551
28089
|
writeYamlFile(AI_PATHS.projectConfig(), raw);
|
|
27552
28090
|
});
|
|
27553
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
|
+
}
|
|
27554
28105
|
function requestPackNames(request) {
|
|
27555
28106
|
const sel = request.profileSelection;
|
|
27556
28107
|
if (!sel) return [];
|
|
@@ -27568,10 +28119,25 @@ function resolvedSelection(request, policy, selectedBy) {
|
|
|
27568
28119
|
return selection;
|
|
27569
28120
|
}
|
|
27570
28121
|
function buildEvaluation(input) {
|
|
27571
|
-
const {
|
|
28122
|
+
const {
|
|
28123
|
+
policy,
|
|
28124
|
+
presentPackNames,
|
|
28125
|
+
selectedProfileIds,
|
|
28126
|
+
hasSelection,
|
|
28127
|
+
countMissingPacksAsViolation,
|
|
28128
|
+
requiredDefaultResolution
|
|
28129
|
+
} = input;
|
|
27572
28130
|
const present = new Set(presentPackNames);
|
|
27573
|
-
|
|
27574
|
-
|
|
28131
|
+
let missingPackNames;
|
|
28132
|
+
let unresolvedPacks;
|
|
28133
|
+
if (requiredDefaultResolution) {
|
|
28134
|
+
unresolvedPacks = requiredDefaultResolution.unresolved;
|
|
28135
|
+
const canonical = [...new Set(requiredDefaultResolution.resolved.map((r) => r.name))];
|
|
28136
|
+
missingPackNames = canonical.filter((n) => !present.has(n));
|
|
28137
|
+
} else {
|
|
28138
|
+
missingPackNames = requiredDefaultNames(policy).filter((n) => !present.has(n));
|
|
28139
|
+
unresolvedPacks = [];
|
|
28140
|
+
}
|
|
27575
28141
|
const blocked = new Set(policy.blockedPackNames ?? []);
|
|
27576
28142
|
const blockedPackNames = presentPackNames.filter((n) => blocked.has(n));
|
|
27577
28143
|
const requiredProfileIds = policy.requiredProfileIds ?? [];
|
|
@@ -27585,16 +28151,20 @@ function buildEvaluation(input) {
|
|
|
27585
28151
|
messages.push("Profile selection is required by policy but none was provided.");
|
|
27586
28152
|
}
|
|
27587
28153
|
for (const n of missingPackNames) messages.push(`Required pack "${n}" is not installed.`);
|
|
28154
|
+
for (const n of unresolvedPacks) {
|
|
28155
|
+
messages.push(`Required pack "${n}" is not available on the instance (no matching server-global pack).`);
|
|
28156
|
+
}
|
|
27588
28157
|
for (const n of blockedPackNames) messages.push(`Pack "${n}" is blocked by policy.`);
|
|
27589
28158
|
for (const id of missingProfileIds) messages.push(`Required profile "${id}" is not selected.`);
|
|
27590
28159
|
for (const id of disallowedProfileIds) messages.push(`Profile "${id}" is not permitted by policy.`);
|
|
27591
|
-
const violation = selectionRequiredUnmet || blockedPackNames.length > 0 || missingProfileIds.length > 0 || disallowedProfileIds.length > 0 || countMissingPacksAsViolation && missingPackNames.length > 0;
|
|
28160
|
+
const violation = selectionRequiredUnmet || blockedPackNames.length > 0 || missingProfileIds.length > 0 || disallowedProfileIds.length > 0 || countMissingPacksAsViolation && (missingPackNames.length > 0 || unresolvedPacks.length > 0);
|
|
27592
28161
|
return {
|
|
27593
28162
|
compliant: !violation,
|
|
27594
28163
|
mode: policy.enforcementMode,
|
|
27595
28164
|
missingPackNames,
|
|
27596
28165
|
blockedPackNames,
|
|
27597
28166
|
missingProfileIds,
|
|
28167
|
+
unresolvedPacks,
|
|
27598
28168
|
messages
|
|
27599
28169
|
};
|
|
27600
28170
|
}
|
|
@@ -27616,11 +28186,15 @@ function performInit(cfg, request, principal) {
|
|
|
27616
28186
|
request.ownerUnitId,
|
|
27617
28187
|
principal ? principalSubject3(principal) : void 0
|
|
27618
28188
|
);
|
|
27619
|
-
|
|
27620
|
-
|
|
27621
|
-
|
|
27622
|
-
|
|
27623
|
-
|
|
28189
|
+
installResolvedPacks(
|
|
28190
|
+
cfg,
|
|
28191
|
+
record2.id,
|
|
28192
|
+
executeApprovedResolveGlobalPacks([
|
|
28193
|
+
...policy.requiredGlobalPacks,
|
|
28194
|
+
...policy.defaultProjectPacks,
|
|
28195
|
+
...requestPackNames(request)
|
|
28196
|
+
])
|
|
28197
|
+
);
|
|
27624
28198
|
const selectedBy = principal ? principalSubject3(principal) : void 0;
|
|
27625
28199
|
recordProjectProfileSelection(record2.rootPath, resolvedSelection(request, policy, selectedBy));
|
|
27626
28200
|
const actor = principal ? principalSubject3(principal) : SYSTEM_SUBJECT;
|
|
@@ -27678,7 +28252,8 @@ function evaluateProjectPolicy(cfg, credential, projectId) {
|
|
|
27678
28252
|
presentPackNames: installedPackNames(cfg, projectId),
|
|
27679
28253
|
selectedProfileIds: selection?.profileIds ?? [],
|
|
27680
28254
|
hasSelection: !!selection,
|
|
27681
|
-
countMissingPacksAsViolation: true
|
|
28255
|
+
countMissingPacksAsViolation: true,
|
|
28256
|
+
requiredDefaultResolution: executeApprovedResolveGlobalPacks(requiredDefaultNames(policy))
|
|
27682
28257
|
});
|
|
27683
28258
|
}
|
|
27684
28259
|
function reconcileProjectPolicy(cfg, credential, projectId) {
|
|
@@ -27692,11 +28267,12 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
|
|
|
27692
28267
|
if (!root) throw new Error(`Unknown project "${projectId}".`);
|
|
27693
28268
|
const policy = effectivePolicy(cfg.dataDir);
|
|
27694
28269
|
const selection = readProjectProfileSelection(root);
|
|
28270
|
+
const resolution = executeApprovedResolveGlobalPacks(requiredDefaultNames(policy));
|
|
27695
28271
|
let installed = installedPackNames(cfg, projectId);
|
|
27696
|
-
const
|
|
27697
|
-
const
|
|
27698
|
-
if (
|
|
27699
|
-
|
|
28272
|
+
const installedSet = new Set(installed);
|
|
28273
|
+
const toApply = resolution.resolved.filter((p) => !installedSet.has(p.name));
|
|
28274
|
+
if (toApply.length > 0) {
|
|
28275
|
+
installResolvedPacks(cfg, projectId, { resolved: toApply, unresolved: [] });
|
|
27700
28276
|
installed = installedPackNames(cfg, projectId);
|
|
27701
28277
|
}
|
|
27702
28278
|
const result = buildEvaluation({
|
|
@@ -27704,7 +28280,8 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
|
|
|
27704
28280
|
presentPackNames: installed,
|
|
27705
28281
|
selectedProfileIds: selection?.profileIds ?? [],
|
|
27706
28282
|
hasSelection: !!selection,
|
|
27707
|
-
countMissingPacksAsViolation: true
|
|
28283
|
+
countMissingPacksAsViolation: true,
|
|
28284
|
+
requiredDefaultResolution: resolution
|
|
27708
28285
|
});
|
|
27709
28286
|
tryAppendAudit2(
|
|
27710
28287
|
cfg,
|
|
@@ -27719,6 +28296,32 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
|
|
|
27719
28296
|
);
|
|
27720
28297
|
return result;
|
|
27721
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
|
+
}
|
|
27722
28325
|
function getPackPolicy(cfg, credential) {
|
|
27723
28326
|
requirePrincipal4(cfg, credential);
|
|
27724
28327
|
return getPackPolicyRecord(cfg.dataDir) ?? PERMISSIVE_DEFAULT_POLICY;
|
|
@@ -27728,6 +28331,11 @@ function setPackPolicy(cfg, credential, policy) {
|
|
|
27728
28331
|
if (!carriesInstancePermission(cfg, principal, POLICY_MANAGE_CAPABILITY)) {
|
|
27729
28332
|
throw new ForbiddenError("policy administration requires instance-level project:admin");
|
|
27730
28333
|
}
|
|
28334
|
+
if (!PACK_ENFORCEMENT_MODES.includes(policy.enforcementMode)) {
|
|
28335
|
+
throw new Error(
|
|
28336
|
+
`invalid enforcementMode "${policy.enforcementMode}" \u2014 must be one of ${PACK_ENFORCEMENT_MODES.join(", ")}`
|
|
28337
|
+
);
|
|
28338
|
+
}
|
|
27731
28339
|
const stamped = { ...policy, updatedBy: principalSubject3(principal) };
|
|
27732
28340
|
const stored = setPackPolicyRecord(cfg.dataDir, stamped);
|
|
27733
28341
|
tryAppendAudit2(
|
|
@@ -27777,7 +28385,7 @@ function handlePolicyRequest(cfg, credential, req, res, body, url) {
|
|
|
27777
28385
|
|
|
27778
28386
|
// src/server/identity.ts
|
|
27779
28387
|
var PROJECT_ADMIN_CAPABILITY2 = "project:admin";
|
|
27780
|
-
var
|
|
28388
|
+
var PROJECT_READ_CAPABILITY2 = "project:read";
|
|
27781
28389
|
var PROJECT_WRITE_CAPABILITY2 = "project:write";
|
|
27782
28390
|
function requireInstanceProjectAdmin(cfg, principal, what) {
|
|
27783
28391
|
if (authorize(cfg.dataDir, principal, PROJECT_ADMIN_CAPABILITY2, "instance", "").value !== "yes") {
|
|
@@ -27923,7 +28531,7 @@ function revokeToken(cfg, credential, tokenId) {
|
|
|
27923
28531
|
}
|
|
27924
28532
|
function mintSelfToken(cfg, credential, projectId, write) {
|
|
27925
28533
|
const principal = requirePrincipal5(cfg, credential);
|
|
27926
|
-
if (authorize(cfg.dataDir, principal,
|
|
28534
|
+
if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2, "project", projectId).value !== "yes") {
|
|
27927
28535
|
throw new ForbiddenError("caller lacks project:read on the requested project");
|
|
27928
28536
|
}
|
|
27929
28537
|
if (write && authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY2, "project", projectId).value !== "yes") {
|
|
@@ -28569,7 +29177,7 @@ function audienceDistance(resolution, targetProjectId) {
|
|
|
28569
29177
|
// src/server/landscape.ts
|
|
28570
29178
|
var yamlLib = __toESM(require("js-yaml"));
|
|
28571
29179
|
var PROJECT_ADMIN_CAPABILITY3 = "project:admin";
|
|
28572
|
-
var
|
|
29180
|
+
var PROJECT_READ_CAPABILITY3 = "project:read";
|
|
28573
29181
|
var PROJECT_WRITE_CAPABILITY3 = "project:write";
|
|
28574
29182
|
function requirePrincipal6(cfg, credential) {
|
|
28575
29183
|
const principal = authenticateCredential(cfg.dataDir, credential);
|
|
@@ -28583,7 +29191,7 @@ function readView(cfg, principal) {
|
|
|
28583
29191
|
if (isInstanceAdmin(principal)) {
|
|
28584
29192
|
return { all: true, projectIds: /* @__PURE__ */ new Set(), unitIds: /* @__PURE__ */ new Set(), contextUnitIds: /* @__PURE__ */ new Set() };
|
|
28585
29193
|
}
|
|
28586
|
-
const read = visibleScopes(cfg.dataDir, principal,
|
|
29194
|
+
const read = visibleScopes(cfg.dataDir, principal, PROJECT_READ_CAPABILITY3);
|
|
28587
29195
|
const admin = visibleScopes(cfg.dataDir, principal, PROJECT_ADMIN_CAPABILITY3);
|
|
28588
29196
|
const unitIds = /* @__PURE__ */ new Set([...actionableUnitIds(read), ...actionableUnitIds(admin)]);
|
|
28589
29197
|
const contextUnitIds = new Set(
|
|
@@ -28598,13 +29206,13 @@ function readView(cfg, principal) {
|
|
|
28598
29206
|
}
|
|
28599
29207
|
function requireLandscapeReach(cfg, principal, view, what) {
|
|
28600
29208
|
if (view.all || view.projectIds.size > 0 || view.unitIds.size > 0) return;
|
|
28601
|
-
if (permitsCap(cfg, principal,
|
|
29209
|
+
if (permitsCap(cfg, principal, PROJECT_READ_CAPABILITY3, "instance", "") || permitsCap(cfg, principal, PROJECT_ADMIN_CAPABILITY3, "instance", "")) {
|
|
28602
29210
|
return;
|
|
28603
29211
|
}
|
|
28604
29212
|
throw new ForbiddenError(`${what} requires project:read reach`);
|
|
28605
29213
|
}
|
|
28606
29214
|
function requireObserverScope(cfg, principal, observerProjectId) {
|
|
28607
|
-
const covered = permitsCap(cfg, principal,
|
|
29215
|
+
const covered = permitsCap(cfg, principal, PROJECT_READ_CAPABILITY3, "project", observerProjectId) || permitsCap(cfg, principal, PROJECT_WRITE_CAPABILITY3, "project", observerProjectId);
|
|
28608
29216
|
if (!covered) {
|
|
28609
29217
|
throw new ForbiddenError("caller lacks scope over the current project");
|
|
28610
29218
|
}
|
|
@@ -30641,6 +31249,15 @@ function installGlobalPackArchive2(cfg, credential, archive, name) {
|
|
|
30641
31249
|
function installProjectPackArchive2(cfg, credential, project2, archive, name) {
|
|
30642
31250
|
return installProjectPackArchive(cfg, credential, project2, archive, name);
|
|
30643
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
|
+
}
|
|
30644
31261
|
function getPackPolicy2(cfg, credential) {
|
|
30645
31262
|
return getPackPolicy(cfg, credential);
|
|
30646
31263
|
}
|
|
@@ -30653,6 +31270,12 @@ function evaluateProjectPolicy2(cfg, credential, projectId) {
|
|
|
30653
31270
|
function reconcileProjectPolicy2(cfg, credential, projectId) {
|
|
30654
31271
|
return reconcileProjectPolicy(cfg, credential, projectId);
|
|
30655
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
|
+
}
|
|
30656
31279
|
function listProducers2(cfg, credential, project2) {
|
|
30657
31280
|
return listProducers(cfg, credential, project2);
|
|
30658
31281
|
}
|
|
@@ -31774,7 +32397,7 @@ function getWebProjectCanvasModel(cfg, sessionId, projectId) {
|
|
|
31774
32397
|
}
|
|
31775
32398
|
return runWithProjectRoot(root, () => hostCore.buildCanvasDataModel());
|
|
31776
32399
|
}
|
|
31777
|
-
function getWebProjectOpenApi(cfg, sessionId, projectId) {
|
|
32400
|
+
function getWebProjectOpenApi(cfg, sessionId, projectId, portalId) {
|
|
31778
32401
|
const principal = authenticateSession(cfg.dataDir, sessionId);
|
|
31779
32402
|
if (!principal.authenticated) throw new UnauthenticatedError();
|
|
31780
32403
|
if (!listProjects2(cfg, sessionId).some((r) => r.id === projectId)) {
|
|
@@ -31783,7 +32406,19 @@ function getWebProjectOpenApi(cfg, sessionId, projectId) {
|
|
|
31783
32406
|
const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
|
|
31784
32407
|
if (!root) throw new ForbiddenError("project not authorized or unknown");
|
|
31785
32408
|
const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface("project", "openapi"));
|
|
31786
|
-
|
|
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>`;
|
|
31787
32422
|
}
|
|
31788
32423
|
function reshapeLandscapeGraph(model, level) {
|
|
31789
32424
|
const unitNodeIds = new Set(model.nodes.filter((n) => n.nodeKind === "orgUnit").map((n) => n.id));
|
|
@@ -33934,6 +34569,15 @@ function opsInstallGlobalPackArchive(cfg, sessionId, req, body, res) {
|
|
|
33934
34569
|
function opsInstallProjectPackArchive(cfg, sessionId, req, url, body, res) {
|
|
33935
34570
|
sendJson(res, 200, installProjectPackArchive2(cfg, sessionId, q(url, "projectId") ?? "", archiveBody(body), packNameOverride(req)));
|
|
33936
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
|
+
}
|
|
33937
34581
|
function opsGetPackPolicy(cfg, sessionId, res) {
|
|
33938
34582
|
sendJson(res, 200, getPackPolicy2(cfg, sessionId));
|
|
33939
34583
|
}
|
|
@@ -33946,6 +34590,12 @@ function opsPolicyEvaluate(cfg, sessionId, url, res) {
|
|
|
33946
34590
|
function opsPolicyReconcile(cfg, sessionId, body, res) {
|
|
33947
34591
|
sendJson(res, 200, reconcileProjectPolicy2(cfg, sessionId, String(body?.projectId ?? "")));
|
|
33948
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
|
+
}
|
|
33949
34599
|
function opsListProducers(cfg, sessionId, url, res) {
|
|
33950
34600
|
sendJson(res, 200, { producers: listProducers2(cfg, sessionId, q(url, "projectId") ?? "") });
|
|
33951
34601
|
}
|
|
@@ -34130,7 +34780,8 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
|
|
|
34130
34780
|
}
|
|
34131
34781
|
if (req.method === "GET" && parts.length === 2 && parts[1] === "openapi") {
|
|
34132
34782
|
const projectId = url.searchParams.get("projectId") ?? "";
|
|
34133
|
-
const
|
|
34783
|
+
const spec = url.searchParams.get("spec") ?? void 0;
|
|
34784
|
+
const html = getWebProjectOpenApi(cfg, sessionId, projectId, spec);
|
|
34134
34785
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
34135
34786
|
res.end(html);
|
|
34136
34787
|
return;
|
|
@@ -34172,6 +34823,18 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
|
|
|
34172
34823
|
if (req.method === "POST" && parts.length === 4 && parts[2] === "packs" && parts[3] === "remove") {
|
|
34173
34824
|
return opsRemoveProjectPack(cfg, sessionId, body, res);
|
|
34174
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
|
+
}
|
|
34175
34838
|
if (req.method === "GET" && parts.length === 3 && parts[2] === "policy") {
|
|
34176
34839
|
return opsPolicyEvaluate(cfg, sessionId, url, res);
|
|
34177
34840
|
}
|
|
@@ -34285,6 +34948,9 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
|
|
|
34285
34948
|
if (req.method === "POST" && parts.length === 4 && parts[2] === "packs" && parts[3] === "remove") {
|
|
34286
34949
|
return opsRemoveGlobalPack(cfg, sessionId, body, res);
|
|
34287
34950
|
}
|
|
34951
|
+
if (req.method === "GET" && parts.length === 3 && parts[2] === "profiles") {
|
|
34952
|
+
return opsListAvailableProfiles(cfg, sessionId, res);
|
|
34953
|
+
}
|
|
34288
34954
|
if (req.method === "GET" && parts.length === 3 && parts[2] === "policy") {
|
|
34289
34955
|
return opsGetPackPolicy(cfg, sessionId, res);
|
|
34290
34956
|
}
|
|
@@ -35086,6 +35752,7 @@ var WEB_MUTATION_PATHS = /* @__PURE__ */ new Set([
|
|
|
35086
35752
|
"/web/projects/packs",
|
|
35087
35753
|
"/web/projects/packs/upload",
|
|
35088
35754
|
"/web/projects/packs/remove",
|
|
35755
|
+
"/web/projects/packs/adopt",
|
|
35089
35756
|
"/web/projects/policy/reconcile",
|
|
35090
35757
|
"/web/projects/producers",
|
|
35091
35758
|
"/web/projects/producers/remove",
|