@nospt/backstage-plugin-apigee-backend 1.1.0-rc2
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/README.md +118 -0
- package/config.d.ts +85 -0
- package/dist/index.cjs.js +22 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +511 -0
- package/dist/lib/api-hub-client.cjs.js +118 -0
- package/dist/lib/api-hub-client.cjs.js.map +1 -0
- package/dist/lib/apigee-client.cjs.js +40 -0
- package/dist/lib/apigee-client.cjs.js.map +1 -0
- package/dist/lib/apigee-stitching-processor.cjs.js +104 -0
- package/dist/lib/apigee-stitching-processor.cjs.js.map +1 -0
- package/dist/lib/entity-builder.cjs.js +347 -0
- package/dist/lib/entity-builder.cjs.js.map +1 -0
- package/dist/lib/entity-provider.cjs.js +502 -0
- package/dist/lib/entity-provider.cjs.js.map +1 -0
- package/dist/lib/fetch-utils.cjs.js +49 -0
- package/dist/lib/fetch-utils.cjs.js.map +1 -0
- package/dist/lib/sharedflow-client.cjs.js +21 -0
- package/dist/lib/sharedflow-client.cjs.js.map +1 -0
- package/dist/module.cjs.js +93 -0
- package/dist/module.cjs.js.map +1 -0
- package/dist/plugin.cjs.js +28 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/router.cjs.js +26 -0
- package/dist/router.cjs.js.map +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fetchUtils = require('./fetch-utils.cjs.js');
|
|
4
|
+
|
|
5
|
+
const APIGEE_BASE_URL = "https://apigee.googleapis.com/v1";
|
|
6
|
+
class ApigeeClient {
|
|
7
|
+
constructor(auth) {
|
|
8
|
+
this.auth = auth;
|
|
9
|
+
}
|
|
10
|
+
auth;
|
|
11
|
+
/**
|
|
12
|
+
* Lists all API proxies in the given Apigee organisation.
|
|
13
|
+
* Calls: GET /organizations/{org}/apis
|
|
14
|
+
*/
|
|
15
|
+
async listProxies(org) {
|
|
16
|
+
const url = `${APIGEE_BASE_URL}/organizations/${encodeURIComponent(org)}/apis`;
|
|
17
|
+
const body = await fetchUtils.fetchWithRetry(url, () => this.auth.getAccessToken());
|
|
18
|
+
return body.proxies ?? [];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Lists all environment deployments for a given proxy.
|
|
22
|
+
* Calls: GET /organizations/{org}/apis/{proxy}/deployments
|
|
23
|
+
*/
|
|
24
|
+
async listDeployments(org, proxy) {
|
|
25
|
+
const url = `${APIGEE_BASE_URL}/organizations/${encodeURIComponent(org)}/apis/${encodeURIComponent(proxy)}/deployments`;
|
|
26
|
+
const body = await fetchUtils.fetchWithRetry(url, () => this.auth.getAccessToken());
|
|
27
|
+
return body.deployments ?? [];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Fetches full detail for a specific proxy revision.
|
|
31
|
+
* Calls: GET /organizations/{org}/apis/{proxy}/revisions/{rev}
|
|
32
|
+
*/
|
|
33
|
+
async getProxyRevision(org, proxy, rev) {
|
|
34
|
+
const url = `${APIGEE_BASE_URL}/organizations/${encodeURIComponent(org)}/apis/${encodeURIComponent(proxy)}/revisions/${encodeURIComponent(rev)}`;
|
|
35
|
+
return await fetchUtils.fetchWithRetry(url, () => this.auth.getAccessToken());
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
exports.ApigeeClient = ApigeeClient;
|
|
40
|
+
//# sourceMappingURL=apigee-client.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apigee-client.cjs.js","sources":["../../src/lib/apigee-client.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport { GoogleAuth } from 'google-auth-library';\r\nimport { fetchWithRetry } from './fetch-utils';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Minimal response types — only the fields consumed in Phases 1-3.\r\n// Full Apigee Management API response shapes are at:\r\n// https://cloud.google.com/apigee/docs/reference/apis/apigee/rest/v1/organizations.apis\r\n// ---------------------------------------------------------------------------\r\n\r\n/** Represents a single Apigee API proxy entry from GET /organizations/{org}/apis */\r\nexport interface ApigeeProxy {\r\n /** Proxy name as registered in Apigee (may contain dots/underscores — slugify before use as metadata.name) */\r\n name: string;\r\n /** Array of revision numbers (strings), most recent last */\r\n revision?: string[];\r\n /** ISO 8601 creation timestamp */\r\n createdAt?: string;\r\n /** ISO 8601 last-modified timestamp */\r\n lastModifiedAt?: string;\r\n}\r\n\r\n/** Represents one environment deployment from GET /organizations/{org}/apis/{proxy}/deployments */\r\nexport interface ApigeeDeployment {\r\n /** Environment name (e.g. \"prod\", \"staging\") */\r\n environment: string;\r\n /** Deployed API proxy revision (string, e.g. \"1\") — field name matches the Apigee API. */\r\n revision?: string;\r\n /** Deployment state: READY | PROGRESSING | ERROR (omitted by the list endpoint). */\r\n state?: string;\r\n}\r\n\r\n/**\r\n * Represents a single proxy revision detail from\r\n * GET /organizations/{org}/apis/{proxy}/revisions/{rev}\r\n */\r\nexport interface ApigeeProxyRevision {\r\n name: string;\r\n revision?: string;\r\n /** Base paths declared in the proxy bundle (used for apigee.com/base-path annotation) */\r\n basepaths?: string[];\r\n description?: string;\r\n /** Resource files attached to this revision */\r\n resourceFiles?: { resourceFile?: Array<{ name: string; type: string }> };\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Client\r\n// ---------------------------------------------------------------------------\r\n\r\nconst APIGEE_BASE_URL = 'https://apigee.googleapis.com/v1';\r\n\r\n/**\r\n * Read-only HTTP client for the Apigee Management API v1.\r\n *\r\n * Auth: uses GCP Application Default Credentials via the injected GoogleAuth\r\n * instance. All calls are GET-only (AUTH-02: read-only IAM roles).\r\n *\r\n * Usage:\r\n * ```typescript\r\n * const auth = new GoogleAuth({ scopes: ['https://www.googleapis.com/auth/cloud-platform'] });\r\n * const client = new ApigeeClient(auth);\r\n * const proxies = await client.listProxies('my-apigee-org');\r\n * ```\r\n */\r\nexport class ApigeeClient {\r\n constructor(private readonly auth: GoogleAuth) {}\r\n\r\n /**\r\n * Lists all API proxies in the given Apigee organisation.\r\n * Calls: GET /organizations/{org}/apis\r\n */\r\n async listProxies(org: string): Promise<ApigeeProxy[]> {\r\n const url = `${APIGEE_BASE_URL}/organizations/${encodeURIComponent(org)}/apis`;\r\n const body = await fetchWithRetry(url, () => this.auth.getAccessToken());\r\n return (body as { proxies?: ApigeeProxy[] }).proxies ?? [];\r\n }\r\n\r\n /**\r\n * Lists all environment deployments for a given proxy.\r\n * Calls: GET /organizations/{org}/apis/{proxy}/deployments\r\n */\r\n async listDeployments(org: string, proxy: string): Promise<ApigeeDeployment[]> {\r\n const url =\r\n `${APIGEE_BASE_URL}/organizations/${encodeURIComponent(org)}` +\r\n `/apis/${encodeURIComponent(proxy)}/deployments`;\r\n const body = await fetchWithRetry(url, () => this.auth.getAccessToken());\r\n return (body as { deployments?: ApigeeDeployment[] }).deployments ?? [];\r\n }\r\n\r\n /**\r\n * Fetches full detail for a specific proxy revision.\r\n * Calls: GET /organizations/{org}/apis/{proxy}/revisions/{rev}\r\n */\r\n async getProxyRevision(\r\n org: string,\r\n proxy: string,\r\n rev: string,\r\n ): Promise<ApigeeProxyRevision> {\r\n const url =\r\n `${APIGEE_BASE_URL}/organizations/${encodeURIComponent(org)}` +\r\n `/apis/${encodeURIComponent(proxy)}/revisions/${encodeURIComponent(rev)}`;\r\n return (await fetchWithRetry(url, () => this.auth.getAccessToken())) as ApigeeProxyRevision;\r\n }\r\n}\r\n"],"names":["fetchWithRetry"],"mappings":";;;;AAkDA,MAAM,eAAA,GAAkB,kCAAA;AAejB,MAAM,YAAA,CAAa;AAAA,EACxB,YAA6B,IAAA,EAAkB;AAAlB,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA,EAAmB;AAAA,EAAnB,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM7B,MAAM,YAAY,GAAA,EAAqC;AACrD,IAAA,MAAM,MAAM,CAAA,EAAG,eAAe,CAAA,eAAA,EAAkB,kBAAA,CAAmB,GAAG,CAAC,CAAA,KAAA,CAAA;AACvE,IAAA,MAAM,IAAA,GAAO,MAAMA,yBAAA,CAAe,GAAA,EAAK,MAAM,IAAA,CAAK,IAAA,CAAK,gBAAgB,CAAA;AACvE,IAAA,OAAQ,IAAA,CAAqC,WAAW,EAAC;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eAAA,CAAgB,GAAA,EAAa,KAAA,EAA4C;AAC7E,IAAA,MAAM,GAAA,GACJ,CAAA,EAAG,eAAe,CAAA,eAAA,EAAkB,kBAAA,CAAmB,GAAG,CAAC,CAAA,MAAA,EAClD,kBAAA,CAAmB,KAAK,CAAC,CAAA,YAAA,CAAA;AACpC,IAAA,MAAM,IAAA,GAAO,MAAMA,yBAAA,CAAe,GAAA,EAAK,MAAM,IAAA,CAAK,IAAA,CAAK,gBAAgB,CAAA;AACvE,IAAA,OAAQ,IAAA,CAA8C,eAAe,EAAC;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,gBAAA,CACJ,GAAA,EACA,KAAA,EACA,GAAA,EAC8B;AAC9B,IAAA,MAAM,GAAA,GACJ,CAAA,EAAG,eAAe,CAAA,eAAA,EAAkB,mBAAmB,GAAG,CAAC,CAAA,MAAA,EAClD,kBAAA,CAAmB,KAAK,CAAC,CAAA,WAAA,EAAc,kBAAA,CAAmB,GAAG,CAAC,CAAA,CAAA;AACzE,IAAA,OAAQ,MAAMA,yBAAA,CAAe,GAAA,EAAK,MAAM,IAAA,CAAK,IAAA,CAAK,gBAAgB,CAAA;AAAA,EACpE;AACF;;;;"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var catalogModel = require('@backstage/catalog-model');
|
|
4
|
+
var pluginCatalogNode = require('@backstage/plugin-catalog-node');
|
|
5
|
+
var entityBuilder = require('./entity-builder.cjs.js');
|
|
6
|
+
|
|
7
|
+
const ANNOTATION_API_NAME = "nos.pt/apigee-api-name";
|
|
8
|
+
const ANNOTATION_PROJECT_ID = "nos.pt/apigee-project-id";
|
|
9
|
+
class ApigeeStitchingProcessor {
|
|
10
|
+
constructor(logger) {
|
|
11
|
+
this.logger = logger;
|
|
12
|
+
}
|
|
13
|
+
logger;
|
|
14
|
+
getProcessorName() {
|
|
15
|
+
return "ApigeeStitchingProcessor";
|
|
16
|
+
}
|
|
17
|
+
async postProcessEntity(entity, _location, emit) {
|
|
18
|
+
if (entity.kind !== "Component") {
|
|
19
|
+
return entity;
|
|
20
|
+
}
|
|
21
|
+
const annotations = entity.metadata.annotations ?? {};
|
|
22
|
+
const hasAnyApigeeAnnotation = Object.keys(annotations).some(
|
|
23
|
+
(key) => key.startsWith("nos.pt/apigee-")
|
|
24
|
+
);
|
|
25
|
+
if (!hasAnyApigeeAnnotation) {
|
|
26
|
+
return entity;
|
|
27
|
+
}
|
|
28
|
+
const target = this.resolveTarget(annotations, entity);
|
|
29
|
+
if (!target) {
|
|
30
|
+
return entity;
|
|
31
|
+
}
|
|
32
|
+
const componentRef = {
|
|
33
|
+
kind: "Component",
|
|
34
|
+
namespace: entity.metadata.namespace ?? "default",
|
|
35
|
+
name: entity.metadata.name
|
|
36
|
+
};
|
|
37
|
+
emit(
|
|
38
|
+
pluginCatalogNode.processingResult.relation({
|
|
39
|
+
source: componentRef,
|
|
40
|
+
target,
|
|
41
|
+
type: catalogModel.RELATION_PROVIDES_API
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
emit(
|
|
45
|
+
pluginCatalogNode.processingResult.relation({
|
|
46
|
+
source: target,
|
|
47
|
+
target: componentRef,
|
|
48
|
+
type: catalogModel.RELATION_API_PROVIDED_BY
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
return entity;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Strategy seam. Tries the TargetServer-preferred path first (deferred this
|
|
55
|
+
* phase), then falls back to the active direct-mapping path. First strategy
|
|
56
|
+
* to return a ref wins.
|
|
57
|
+
*/
|
|
58
|
+
resolveTarget(annotations, entity) {
|
|
59
|
+
return this.resolveViaTargetServer(annotations, entity) ?? this.resolveViaDirect(annotations, entity);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Deferred: TargetServer-preferred path (STITCH-02). See 04-CONTEXT D-01.
|
|
63
|
+
* No TargetServer data exists in the codebase yet, so this is the clean
|
|
64
|
+
* insertion point for the future branch and intentionally returns undefined.
|
|
65
|
+
*/
|
|
66
|
+
resolveViaTargetServer(_annotations, _entity) {
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Direct-mapping path. Requires BOTH the api-name and project-id annotations.
|
|
71
|
+
* The proxy API entity ref is deterministic — `api:default/<slugify(api-name)>` —
|
|
72
|
+
* so no live catalog lookup is needed (D-01). projectId is validated for
|
|
73
|
+
* presence to confirm intent but is not part of the deterministic ref.
|
|
74
|
+
*/
|
|
75
|
+
resolveViaDirect(annotations, entity) {
|
|
76
|
+
const apiName = annotations[ANNOTATION_API_NAME]?.trim();
|
|
77
|
+
const projectId = annotations[ANNOTATION_PROJECT_ID]?.trim();
|
|
78
|
+
const hasApiName = Boolean(apiName);
|
|
79
|
+
const hasProjectId = Boolean(projectId);
|
|
80
|
+
if (hasApiName !== hasProjectId) {
|
|
81
|
+
const missing = hasApiName ? ANNOTATION_PROJECT_ID : ANNOTATION_API_NAME;
|
|
82
|
+
this.logger.warn(
|
|
83
|
+
`[ApigeeStitchingProcessor] Component "${entity.metadata.name}" has a partial Apigee direct-mapping annotation set; missing "${missing}". Skipping stitching.`
|
|
84
|
+
);
|
|
85
|
+
return void 0;
|
|
86
|
+
}
|
|
87
|
+
if (!hasApiName) {
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
const slug = entityBuilder.slugify(apiName);
|
|
91
|
+
if (slug === "") {
|
|
92
|
+
this.logger.warn(
|
|
93
|
+
`[ApigeeStitchingProcessor] Component "${entity.metadata.name}" has an unresolvable "${ANNOTATION_API_NAME}" value; nothing to stitch.`
|
|
94
|
+
);
|
|
95
|
+
return void 0;
|
|
96
|
+
}
|
|
97
|
+
return { kind: "API", namespace: "default", name: slug };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
exports.ANNOTATION_API_NAME = ANNOTATION_API_NAME;
|
|
102
|
+
exports.ANNOTATION_PROJECT_ID = ANNOTATION_PROJECT_ID;
|
|
103
|
+
exports.ApigeeStitchingProcessor = ApigeeStitchingProcessor;
|
|
104
|
+
//# sourceMappingURL=apigee-stitching-processor.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apigee-stitching-processor.cjs.js","sources":["../../src/lib/apigee-stitching-processor.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport {\r\n RELATION_PROVIDES_API,\r\n RELATION_API_PROVIDED_BY,\r\n type Entity,\r\n} from '@backstage/catalog-model';\r\nimport {\r\n processingResult,\r\n type CatalogProcessor,\r\n type CatalogProcessorEmit,\r\n type LocationSpec,\r\n} from '@backstage/plugin-catalog-node';\r\nimport type { LoggerService } from '@backstage/backend-plugin-api';\r\nimport { slugify } from './entity-builder';\r\n\r\n/**\r\n * Annotation that names the Apigee proxy / API this Component provides.\r\n * Used by the direct-mapping resolution path.\r\n */\r\nexport const ANNOTATION_API_NAME = 'nos.pt/apigee-api-name';\r\n\r\n/**\r\n * Annotation carrying the owning GCP/Apigee project id. Required (alongside\r\n * {@link ANNOTATION_API_NAME}) for the direct-mapping path to disambiguate intent.\r\n */\r\nexport const ANNOTATION_PROJECT_ID = 'nos.pt/apigee-project-id';\r\n\r\n/**\r\n * Annotation carrying the TargetServer hostname. Reserved for the deferred\r\n * TargetServer-preferred resolution path (STITCH-02); not resolved this phase.\r\n */\r\nexport const ANNOTATION_TARGETSERVER_HOST = 'nos.pt/apigee-targetserver-hostname';\r\n\r\n/** Compound ref to a Backstage entity. */\r\ninterface EntityRef {\r\n kind: string;\r\n namespace: string;\r\n name: string;\r\n}\r\n\r\n/**\r\n * A Backstage {@link CatalogProcessor} that links a backend `Component` to its\r\n * proxy `API` entity by emitting the native `providesApi` / `apiProvidedBy`\r\n * relation pair, surfacing the standard \"Provided APIs\" / \"Providers\" cards\r\n * with zero custom UI (STITCH-01).\r\n *\r\n * Resolution dispatches through a strategy seam: a TargetServer-first path\r\n * (deferred — STITCH-02, see 04-CONTEXT D-01) followed by the active\r\n * direct-mapping path. The first strategy to return a target ref wins.\r\n */\r\nexport class ApigeeStitchingProcessor implements CatalogProcessor {\r\n constructor(private readonly logger: LoggerService) {}\r\n\r\n getProcessorName(): string {\r\n return 'ApigeeStitchingProcessor';\r\n }\r\n\r\n async postProcessEntity(\r\n entity: Entity,\r\n _location: LocationSpec,\r\n emit: CatalogProcessorEmit,\r\n ): Promise<Entity> {\r\n // The processor only stitches Components.\r\n if (entity.kind !== 'Component') {\r\n return entity;\r\n }\r\n\r\n const annotations = entity.metadata.annotations ?? {};\r\n\r\n // Untouched component: no nos.pt/apigee-* annotations at all → no-op, no warning.\r\n const hasAnyApigeeAnnotation = Object.keys(annotations).some(key =>\r\n key.startsWith('nos.pt/apigee-'),\r\n );\r\n if (!hasAnyApigeeAnnotation) {\r\n return entity;\r\n }\r\n\r\n const target = this.resolveTarget(annotations, entity);\r\n if (!target) {\r\n // A warning (if applicable) was already logged by the resolution strategy.\r\n return entity;\r\n }\r\n\r\n const componentRef: EntityRef = {\r\n kind: 'Component',\r\n namespace: entity.metadata.namespace ?? 'default',\r\n name: entity.metadata.name,\r\n };\r\n\r\n emit(\r\n processingResult.relation({\r\n source: componentRef,\r\n target,\r\n type: RELATION_PROVIDES_API,\r\n }),\r\n );\r\n emit(\r\n processingResult.relation({\r\n source: target,\r\n target: componentRef,\r\n type: RELATION_API_PROVIDED_BY,\r\n }),\r\n );\r\n\r\n // Relation-only work: never mutate the entity.\r\n return entity;\r\n }\r\n\r\n /**\r\n * Strategy seam. Tries the TargetServer-preferred path first (deferred this\r\n * phase), then falls back to the active direct-mapping path. First strategy\r\n * to return a ref wins.\r\n */\r\n private resolveTarget(\r\n annotations: Record<string, string>,\r\n entity: Entity,\r\n ): EntityRef | undefined {\r\n return (\r\n this.resolveViaTargetServer(annotations, entity) ??\r\n this.resolveViaDirect(annotations, entity)\r\n );\r\n }\r\n\r\n /**\r\n * Deferred: TargetServer-preferred path (STITCH-02). See 04-CONTEXT D-01.\r\n * No TargetServer data exists in the codebase yet, so this is the clean\r\n * insertion point for the future branch and intentionally returns undefined.\r\n */\r\n private resolveViaTargetServer(\r\n _annotations: Record<string, string>,\r\n _entity: Entity,\r\n ): undefined {\r\n return undefined;\r\n }\r\n\r\n /**\r\n * Direct-mapping path. Requires BOTH the api-name and project-id annotations.\r\n * The proxy API entity ref is deterministic — `api:default/<slugify(api-name)>` —\r\n * so no live catalog lookup is needed (D-01). projectId is validated for\r\n * presence to confirm intent but is not part of the deterministic ref.\r\n */\r\n private resolveViaDirect(\r\n annotations: Record<string, string>,\r\n entity: Entity,\r\n ): EntityRef | undefined {\r\n const apiName = annotations[ANNOTATION_API_NAME]?.trim();\r\n const projectId = annotations[ANNOTATION_PROJECT_ID]?.trim();\r\n\r\n const hasApiName = Boolean(apiName);\r\n const hasProjectId = Boolean(projectId);\r\n\r\n if (hasApiName !== hasProjectId) {\r\n const missing = hasApiName ? ANNOTATION_PROJECT_ID : ANNOTATION_API_NAME;\r\n this.logger.warn(\r\n `[ApigeeStitchingProcessor] Component \"${entity.metadata.name}\" has a partial ` +\r\n `Apigee direct-mapping annotation set; missing \"${missing}\". Skipping stitching.`,\r\n );\r\n return undefined;\r\n }\r\n\r\n if (!hasApiName) {\r\n // Neither direct annotation present — defer silently (e.g. TargetServer-only\r\n // annotations were supplied, handled by the deferred path).\r\n return undefined;\r\n }\r\n\r\n const slug = slugify(apiName as string);\r\n if (slug === '') {\r\n this.logger.warn(\r\n `[ApigeeStitchingProcessor] Component \"${entity.metadata.name}\" has an ` +\r\n `unresolvable \"${ANNOTATION_API_NAME}\" value; nothing to stitch.`,\r\n );\r\n return undefined;\r\n }\r\n\r\n return { kind: 'API', namespace: 'default', name: slug };\r\n }\r\n}\r\n"],"names":["processingResult","RELATION_PROVIDES_API","RELATION_API_PROVIDED_BY","slugify"],"mappings":"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"}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var catalogModel = require('@backstage/catalog-model');
|
|
4
|
+
|
|
5
|
+
const LIFECYCLE_RANK = {
|
|
6
|
+
production: 2,
|
|
7
|
+
experimental: 1,
|
|
8
|
+
deprecated: 0
|
|
9
|
+
};
|
|
10
|
+
const DEFAULT_DEPLOYED_LIFECYCLE = "experimental";
|
|
11
|
+
function resolveHighestLifecycle(orgs) {
|
|
12
|
+
if (orgs.length === 0) return "experimental";
|
|
13
|
+
return orgs.reduce((best, org) => {
|
|
14
|
+
const rank = LIFECYCLE_RANK[org.lifecycle] ?? 1;
|
|
15
|
+
const bestRank = LIFECYCLE_RANK[best] ?? 1;
|
|
16
|
+
return rank > bestRank ? org.lifecycle : best;
|
|
17
|
+
}, orgs[0].lifecycle);
|
|
18
|
+
}
|
|
19
|
+
function resolveLifecycleFromEnvironments(environments, environmentLifecycle) {
|
|
20
|
+
if (environments.length === 0) return "experimental";
|
|
21
|
+
const mapEnv = (env) => environmentLifecycle[env] ?? DEFAULT_DEPLOYED_LIFECYCLE;
|
|
22
|
+
return environments.reduce((best, env) => {
|
|
23
|
+
const mapped = mapEnv(env);
|
|
24
|
+
const rank = LIFECYCLE_RANK[mapped] ?? 1;
|
|
25
|
+
const bestRank = LIFECYCLE_RANK[best] ?? 1;
|
|
26
|
+
return rank > bestRank ? mapped : best;
|
|
27
|
+
}, mapEnv(environments[0]));
|
|
28
|
+
}
|
|
29
|
+
const APIGEE_LOCATION = "url:https://apigee.com";
|
|
30
|
+
function slugify(name) {
|
|
31
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
32
|
+
}
|
|
33
|
+
function selectSpec(versions) {
|
|
34
|
+
const all = versions ?? [];
|
|
35
|
+
if (all.length === 0) return void 0;
|
|
36
|
+
const active = all.filter((v) => v.lifecycle?.stage === "ACTIVE");
|
|
37
|
+
const candidates = active.length > 0 ? active : all.filter((v) => v.lifecycle?.stage !== "ARCHIVED");
|
|
38
|
+
if (candidates.length === 0) return void 0;
|
|
39
|
+
const sorted = [...candidates].sort((a, b) => {
|
|
40
|
+
const at = a.updateTime ?? "";
|
|
41
|
+
const bt = b.updateTime ?? "";
|
|
42
|
+
if (at !== bt) {
|
|
43
|
+
if (!at) return 1;
|
|
44
|
+
if (!bt) return -1;
|
|
45
|
+
return at < bt ? 1 : -1;
|
|
46
|
+
}
|
|
47
|
+
return (a.name ?? "").localeCompare(b.name ?? "");
|
|
48
|
+
});
|
|
49
|
+
const chosen = sorted[0];
|
|
50
|
+
const versionId = (chosen.name ?? "").split("/").pop() ?? "";
|
|
51
|
+
if (!versionId) return void 0;
|
|
52
|
+
return { versionId };
|
|
53
|
+
}
|
|
54
|
+
function ownerRef(owner, defaultOwner) {
|
|
55
|
+
if (owner?.displayName) {
|
|
56
|
+
return `group:default/${slugify(owner.displayName)}`;
|
|
57
|
+
}
|
|
58
|
+
return defaultOwner ?? "group:default/unknown";
|
|
59
|
+
}
|
|
60
|
+
function categoryTags(categories) {
|
|
61
|
+
const slugs = (categories ?? []).map((c) => slugify(c)).filter((c) => c.length > 0);
|
|
62
|
+
return [...new Set(slugs)].sort();
|
|
63
|
+
}
|
|
64
|
+
function categoryAnnotation(categories) {
|
|
65
|
+
const tags = categoryTags(categories);
|
|
66
|
+
return tags.length > 0 ? tags.join(",") : void 0;
|
|
67
|
+
}
|
|
68
|
+
const BUSINESS_UNIT_ATTR = /(^|\/)(system-)?business[-_]?unit$/i;
|
|
69
|
+
function attributeValue(values) {
|
|
70
|
+
if (!values) return void 0;
|
|
71
|
+
const enumValue = values.enumValues?.values?.find(
|
|
72
|
+
(v) => (v.displayName ?? v.id ?? "").trim() !== ""
|
|
73
|
+
);
|
|
74
|
+
if (enumValue) return (enumValue.displayName ?? enumValue.id)?.trim();
|
|
75
|
+
return values.stringValues?.values?.find((v) => v.trim() !== "")?.trim();
|
|
76
|
+
}
|
|
77
|
+
function businessUnitValue(hubApi) {
|
|
78
|
+
const systemValue = attributeValue(hubApi.businessUnit);
|
|
79
|
+
if (systemValue) return systemValue;
|
|
80
|
+
const attributes = hubApi.attributes;
|
|
81
|
+
if (!attributes) return void 0;
|
|
82
|
+
for (const [key, values] of Object.entries(attributes)) {
|
|
83
|
+
if (!BUSINESS_UNIT_ATTR.test(key)) continue;
|
|
84
|
+
const v = attributeValue(values);
|
|
85
|
+
if (v) return v;
|
|
86
|
+
}
|
|
87
|
+
return void 0;
|
|
88
|
+
}
|
|
89
|
+
function departmentLabel(hubApi) {
|
|
90
|
+
const raw = businessUnitValue(hubApi);
|
|
91
|
+
if (!raw) return void 0;
|
|
92
|
+
const slug = slugify(raw).slice(0, 63).replace(/-+$/g, "");
|
|
93
|
+
return slug || void 0;
|
|
94
|
+
}
|
|
95
|
+
function resolveGithubLink(input) {
|
|
96
|
+
const httpUrl = /^https?:\/\//i;
|
|
97
|
+
if (typeof input.documentationUri === "string" && httpUrl.test(input.documentationUri)) {
|
|
98
|
+
return { url: input.documentationUri, title: "Source repository" };
|
|
99
|
+
}
|
|
100
|
+
const sources = (input.sourceMetadata ?? []).filter(
|
|
101
|
+
(s) => typeof s.value === "string" && httpUrl.test(s.value)
|
|
102
|
+
);
|
|
103
|
+
if (sources.length > 0) {
|
|
104
|
+
const preferred = sources.find((s) => /repo|git|source/i.test(s.sourceType ?? "")) ?? sources[0];
|
|
105
|
+
return { url: preferred.value, title: "Source repository" };
|
|
106
|
+
}
|
|
107
|
+
if (input.githubOrgSlug) {
|
|
108
|
+
return {
|
|
109
|
+
url: `https://github.com/${input.githubOrgSlug}/${input.entityName}`,
|
|
110
|
+
title: "Source repository"
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
function extractSpecVersion(definition) {
|
|
116
|
+
if (!definition) return void 0;
|
|
117
|
+
const trimmed = definition.trim();
|
|
118
|
+
if (!trimmed) return void 0;
|
|
119
|
+
if (trimmed.startsWith("{")) {
|
|
120
|
+
try {
|
|
121
|
+
const doc = JSON.parse(trimmed);
|
|
122
|
+
const v = doc.info?.version;
|
|
123
|
+
return v == null ? void 0 : String(v).trim() || void 0;
|
|
124
|
+
} catch {
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
let infoIndent = -1;
|
|
129
|
+
let childIndent = -1;
|
|
130
|
+
for (const raw of definition.split(/\r?\n/)) {
|
|
131
|
+
const line = raw.replace(/\t/g, " ");
|
|
132
|
+
const trimmedLine = line.trim();
|
|
133
|
+
if (trimmedLine === "" || trimmedLine.startsWith("#")) continue;
|
|
134
|
+
const indent = line.length - line.trimStart().length;
|
|
135
|
+
if (infoIndent < 0) {
|
|
136
|
+
if (indent === 0 && /^info\s*:\s*(#.*)?$/.test(trimmedLine)) {
|
|
137
|
+
infoIndent = indent;
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (indent <= infoIndent) break;
|
|
142
|
+
if (childIndent < 0) childIndent = indent;
|
|
143
|
+
if (indent !== childIndent) continue;
|
|
144
|
+
const m = /^version\s*:\s*(.*)$/.exec(trimmedLine);
|
|
145
|
+
if (m) {
|
|
146
|
+
let v = m[1].trim();
|
|
147
|
+
if (!/^["']/.test(v)) v = v.replace(/\s+#.*$/, "").trim();
|
|
148
|
+
v = v.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1").trim();
|
|
149
|
+
return v || void 0;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return void 0;
|
|
153
|
+
}
|
|
154
|
+
function applyHubMetadata(api, hubApi, spec, githubOrgSlug) {
|
|
155
|
+
const annotations = api.metadata.annotations ?? {};
|
|
156
|
+
const tags = categoryTags(hubApi.categories);
|
|
157
|
+
if (tags.length > 0) {
|
|
158
|
+
api.metadata.tags = tags;
|
|
159
|
+
}
|
|
160
|
+
const catAnno = categoryAnnotation(hubApi.categories);
|
|
161
|
+
if (catAnno) {
|
|
162
|
+
annotations["apigee.com/category"] = catAnno;
|
|
163
|
+
}
|
|
164
|
+
const department = departmentLabel(hubApi);
|
|
165
|
+
if (department) {
|
|
166
|
+
api.metadata.labels = { ...api.metadata.labels ?? {}, department };
|
|
167
|
+
}
|
|
168
|
+
const link = resolveGithubLink({
|
|
169
|
+
documentationUri: hubApi.documentation?.externalUri,
|
|
170
|
+
sourceMetadata: hubApi.sourceMetadata,
|
|
171
|
+
githubOrgSlug,
|
|
172
|
+
entityName: api.metadata.name
|
|
173
|
+
});
|
|
174
|
+
if (link) {
|
|
175
|
+
const links = api.metadata.links ?? [];
|
|
176
|
+
if (!links.some((l) => l.url === link.url)) {
|
|
177
|
+
links.push(link);
|
|
178
|
+
}
|
|
179
|
+
api.metadata.links = links;
|
|
180
|
+
}
|
|
181
|
+
if (spec?.definition) {
|
|
182
|
+
api.spec.definition = spec.definition;
|
|
183
|
+
annotations["apigee.com/has-spec"] = "true";
|
|
184
|
+
} else if (spec?.specUrl) {
|
|
185
|
+
annotations["apigee.com/spec-url"] = spec.specUrl;
|
|
186
|
+
annotations["apigee.com/has-spec"] = "true";
|
|
187
|
+
} else {
|
|
188
|
+
annotations["apigee.com/has-spec"] = "false";
|
|
189
|
+
}
|
|
190
|
+
if (spec?.version) {
|
|
191
|
+
annotations["apigee.com/version"] = spec.version;
|
|
192
|
+
}
|
|
193
|
+
api.metadata.annotations = annotations;
|
|
194
|
+
}
|
|
195
|
+
function enrichMatchedApiEntity(baseApi, hubApi, spec, githubOrgSlug, options) {
|
|
196
|
+
const api = {
|
|
197
|
+
...baseApi,
|
|
198
|
+
metadata: {
|
|
199
|
+
...baseApi.metadata,
|
|
200
|
+
annotations: { ...baseApi.metadata.annotations ?? {} },
|
|
201
|
+
...baseApi.metadata.tags ? { tags: [...baseApi.metadata.tags] } : {},
|
|
202
|
+
...baseApi.metadata.links ? { links: baseApi.metadata.links.map((l) => ({ ...l })) } : {}
|
|
203
|
+
},
|
|
204
|
+
spec: { ...baseApi.spec ?? {} }
|
|
205
|
+
};
|
|
206
|
+
if (hubApi.displayName) {
|
|
207
|
+
api.metadata.title = hubApi.displayName;
|
|
208
|
+
}
|
|
209
|
+
api.metadata.description = hubApi.description ?? "";
|
|
210
|
+
api.spec.owner = ownerRef(
|
|
211
|
+
hubApi.owner,
|
|
212
|
+
options?.defaultOwner
|
|
213
|
+
);
|
|
214
|
+
applyHubMetadata(api, hubApi, spec, githubOrgSlug);
|
|
215
|
+
return api;
|
|
216
|
+
}
|
|
217
|
+
function hubOnlyToEntity(hubApi, orgName, spec, githubOrgSlug, options) {
|
|
218
|
+
const apiId = (hubApi.name ?? "").split("/").pop() ?? "";
|
|
219
|
+
const display = hubApi.displayName ?? apiId;
|
|
220
|
+
const slug = slugify(display);
|
|
221
|
+
const api = {
|
|
222
|
+
apiVersion: "backstage.io/v1alpha1",
|
|
223
|
+
kind: "API",
|
|
224
|
+
metadata: {
|
|
225
|
+
name: slug,
|
|
226
|
+
title: display,
|
|
227
|
+
namespace: "default",
|
|
228
|
+
description: hubApi.description ?? "",
|
|
229
|
+
annotations: {
|
|
230
|
+
[catalogModel.ANNOTATION_LOCATION]: APIGEE_LOCATION,
|
|
231
|
+
[catalogModel.ANNOTATION_ORIGIN_LOCATION]: APIGEE_LOCATION,
|
|
232
|
+
// Full API Hub resource name (D-04) — distinguishes hub-only entities.
|
|
233
|
+
"apigee.com/api-hub-id": hubApi.name,
|
|
234
|
+
"apigee.com/org": orgName
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
spec: {
|
|
238
|
+
type: hubApi.apiType ?? "other",
|
|
239
|
+
lifecycle: "experimental",
|
|
240
|
+
owner: ownerRef(hubApi.owner, options?.defaultOwner),
|
|
241
|
+
definition: ""
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
applyHubMetadata(api, hubApi, spec, githubOrgSlug);
|
|
245
|
+
return api;
|
|
246
|
+
}
|
|
247
|
+
function proxyToEntities(merged, apiType, options) {
|
|
248
|
+
const slug = slugify(merged.proxy.name);
|
|
249
|
+
const lifecycle = resolveHighestLifecycle(merged.orgs);
|
|
250
|
+
const owner = options?.defaultOwner ?? "group:default/unknown";
|
|
251
|
+
const orgsAnnotation = merged.orgs.map((o) => o.name).join(",");
|
|
252
|
+
const perOrgAnnotations = {};
|
|
253
|
+
for (const org of merged.orgs) {
|
|
254
|
+
const safe = slugify(org.slug || org.name);
|
|
255
|
+
perOrgAnnotations[`apigee.com/${safe}/lifecycle`] = org.lifecycle;
|
|
256
|
+
for (const env of org.environments) {
|
|
257
|
+
const envSlug = slugify(env.name);
|
|
258
|
+
perOrgAnnotations[`apigee.com/${safe}/environment/${envSlug}/revision`] = env.revision;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const sharedAnnotations = {
|
|
262
|
+
[catalogModel.ANNOTATION_LOCATION]: APIGEE_LOCATION,
|
|
263
|
+
[catalogModel.ANNOTATION_ORIGIN_LOCATION]: APIGEE_LOCATION,
|
|
264
|
+
"apigee.com/proxy-name": merged.proxy.name,
|
|
265
|
+
"apigee.com/orgs": orgsAnnotation
|
|
266
|
+
};
|
|
267
|
+
const component = {
|
|
268
|
+
apiVersion: "backstage.io/v1alpha1",
|
|
269
|
+
kind: "Component",
|
|
270
|
+
metadata: {
|
|
271
|
+
name: slug,
|
|
272
|
+
title: merged.proxy.name,
|
|
273
|
+
namespace: "default",
|
|
274
|
+
annotations: {
|
|
275
|
+
...sharedAnnotations,
|
|
276
|
+
"apigee.com/base-path": merged.basePaths.join(","),
|
|
277
|
+
"apigee.com/has-spec": "false",
|
|
278
|
+
...perOrgAnnotations
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
spec: {
|
|
282
|
+
type: "api-proxy",
|
|
283
|
+
lifecycle,
|
|
284
|
+
owner
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
const api = {
|
|
288
|
+
apiVersion: "backstage.io/v1alpha1",
|
|
289
|
+
kind: "API",
|
|
290
|
+
metadata: {
|
|
291
|
+
name: slug,
|
|
292
|
+
title: merged.proxy.name,
|
|
293
|
+
namespace: "default",
|
|
294
|
+
annotations: {
|
|
295
|
+
...sharedAnnotations,
|
|
296
|
+
// Default false; 03-03 enrichment overrides to 'true' when a spec is found (META-05, SC #2).
|
|
297
|
+
"apigee.com/has-spec": "false"
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
spec: {
|
|
301
|
+
type: apiType ?? "openapi",
|
|
302
|
+
lifecycle,
|
|
303
|
+
owner,
|
|
304
|
+
definition: ""
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
return [component, api];
|
|
308
|
+
}
|
|
309
|
+
function sharedflowToEntity(sharedflow, orgName, options) {
|
|
310
|
+
const slug = slugify(sharedflow.name);
|
|
311
|
+
const owner = options?.defaultOwner ?? "group:default/unknown";
|
|
312
|
+
return {
|
|
313
|
+
apiVersion: "backstage.io/v1alpha1",
|
|
314
|
+
kind: "Component",
|
|
315
|
+
metadata: {
|
|
316
|
+
name: slug,
|
|
317
|
+
title: sharedflow.name,
|
|
318
|
+
namespace: "apigee-sharedflows",
|
|
319
|
+
annotations: {
|
|
320
|
+
[catalogModel.ANNOTATION_LOCATION]: APIGEE_LOCATION,
|
|
321
|
+
[catalogModel.ANNOTATION_ORIGIN_LOCATION]: APIGEE_LOCATION,
|
|
322
|
+
"apigee.com/proxy-name": sharedflow.name,
|
|
323
|
+
"apigee.com/orgs": orgName
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
spec: {
|
|
327
|
+
type: "library",
|
|
328
|
+
lifecycle: "production",
|
|
329
|
+
owner
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
exports.categoryAnnotation = categoryAnnotation;
|
|
335
|
+
exports.categoryTags = categoryTags;
|
|
336
|
+
exports.departmentLabel = departmentLabel;
|
|
337
|
+
exports.enrichMatchedApiEntity = enrichMatchedApiEntity;
|
|
338
|
+
exports.extractSpecVersion = extractSpecVersion;
|
|
339
|
+
exports.hubOnlyToEntity = hubOnlyToEntity;
|
|
340
|
+
exports.ownerRef = ownerRef;
|
|
341
|
+
exports.proxyToEntities = proxyToEntities;
|
|
342
|
+
exports.resolveGithubLink = resolveGithubLink;
|
|
343
|
+
exports.resolveLifecycleFromEnvironments = resolveLifecycleFromEnvironments;
|
|
344
|
+
exports.selectSpec = selectSpec;
|
|
345
|
+
exports.sharedflowToEntity = sharedflowToEntity;
|
|
346
|
+
exports.slugify = slugify;
|
|
347
|
+
//# sourceMappingURL=entity-builder.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-builder.cjs.js","sources":["../../src/lib/entity-builder.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport {\r\n ANNOTATION_LOCATION,\r\n ANNOTATION_ORIGIN_LOCATION,\r\n type Entity,\r\n} from '@backstage/catalog-model';\r\n\r\nimport type { ApigeeProxy } from './apigee-client';\r\nimport type { ApigeeSharedflow } from './sharedflow-client';\r\nimport type {\r\n ApiHubApi,\r\n ApiHubAttributeValues,\r\n ApiHubVersion,\r\n} from './api-hub-client';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Public types\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Per-organisation data gathered during a sync run.\r\n * Populated by ApigeeEntityProvider.run() before calling proxyToEntities().\r\n */\r\n/** One environment this proxy is deployed to, with the revision deployed there. */\r\nexport interface EnvDeployment {\r\n /** Environment name (e.g. \"dev-1\") */\r\n name: string;\r\n /** Deployed API proxy revision in this environment (e.g. \"1\") */\r\n revision: string;\r\n}\r\n\r\nexport interface OrgSyncData {\r\n /** Slugified org identifier (used as annotation key segment) */\r\n slug: string;\r\n /** Raw org name from config (e.g. \"my-apigee-org\") */\r\n name: string;\r\n /** GCP project ID owning this Apigee organisation */\r\n projectId: string;\r\n /** Environments this proxy is deployed to in this org, each with its revision */\r\n environments: EnvDeployment[];\r\n /** Resolved Backstage lifecycle for this org: production | experimental | deprecated */\r\n lifecycle: string;\r\n}\r\n\r\n/**\r\n * A proxy that has been deduplicated across organisations (D-03).\r\n * Multiple orgs may share the same proxy name — they are merged into one entity.\r\n */\r\nexport interface MergedProxy {\r\n /** Proxy data from the first org that declared this proxy */\r\n proxy: ApigeeProxy;\r\n /** Union of all base paths declared across all org revisions */\r\n basePaths: string[];\r\n /** All orgs that contain this proxy */\r\n orgs: OrgSyncData[];\r\n}\r\n\r\n/** Options forwarded from plugin configuration. */\r\nexport interface EntityBuildOptions {\r\n /** Default spec.owner when not provided by API Hub. Must be a Backstage entity ref. */\r\n defaultOwner?: string;\r\n /** Inline spec size limit in bytes; default 512000 applied by the provider (D-03). */\r\n specMaxBytes?: number;\r\n /**\r\n * Optional map of Apigee environment name → Backstage lifecycle\r\n * (production | experimental | deprecated). When set, a proxy's lifecycle is\r\n * derived from the environments it is deployed to (highest-ranked wins).\r\n */\r\n environmentLifecycle?: Record<string, string>;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Internal helpers\r\n// ---------------------------------------------------------------------------\r\n\r\nconst LIFECYCLE_RANK: Record<string, number> = {\r\n production: 2,\r\n experimental: 1,\r\n deprecated: 0,\r\n};\r\n\r\n/** Lifecycle applied to a deployed environment absent from the configured map. */\r\nconst DEFAULT_DEPLOYED_LIFECYCLE = 'experimental';\r\n\r\n/**\r\n * Returns the highest-priority lifecycle value across all org sync data.\r\n * Precedence: production > experimental > deprecated.\r\n */\r\nfunction resolveHighestLifecycle(orgs: OrgSyncData[]): string {\r\n if (orgs.length === 0) return 'experimental';\r\n return orgs.reduce<string>((best, org) => {\r\n const rank = LIFECYCLE_RANK[org.lifecycle] ?? 1;\r\n const bestRank = LIFECYCLE_RANK[best] ?? 1;\r\n return rank > bestRank ? org.lifecycle : best;\r\n }, orgs[0].lifecycle);\r\n}\r\n\r\n/**\r\n * Resolves a Backstage lifecycle from the environments a proxy is deployed to,\r\n * using a configured environment→lifecycle map. The highest-ranked lifecycle\r\n * across all deployed environments wins (production > experimental > deprecated).\r\n * Environments absent from the map contribute the default ('experimental').\r\n * Returns 'experimental' when there are no environments.\r\n */\r\nexport function resolveLifecycleFromEnvironments(\r\n environments: string[],\r\n environmentLifecycle: Record<string, string>,\r\n): string {\r\n if (environments.length === 0) return 'experimental';\r\n const mapEnv = (env: string) =>\r\n environmentLifecycle[env] ?? DEFAULT_DEPLOYED_LIFECYCLE;\r\n return environments.reduce<string>((best, env) => {\r\n const mapped = mapEnv(env);\r\n const rank = LIFECYCLE_RANK[mapped] ?? 1;\r\n const bestRank = LIFECYCLE_RANK[best] ?? 1;\r\n return rank > bestRank ? mapped : best;\r\n }, mapEnv(environments[0]));\r\n}\r\n\r\n/** Shared location annotation value for all Apigee-managed entities. */\r\nconst APIGEE_LOCATION = 'url:https://apigee.com';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Public functions\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Converts an arbitrary string into a valid Backstage metadata.name slug.\r\n * Lowercases, replaces non-alphanumeric runs with hyphens, trims leading/trailing hyphens.\r\n */\r\nexport function slugify(name: string): string {\r\n return name\r\n .toLowerCase()\r\n .replace(/[^a-z0-9]+/g, '-')\r\n .replace(/-{2,}/g, '-')\r\n .replace(/^-+|-+$/g, '');\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// API Hub enrichment helpers (pure, side-effect-free) — D-02, D-05, D-06\r\n// ---------------------------------------------------------------------------\r\n\r\n/** A deterministic version selection. The spec ID is resolved separately by\r\n * the provider via ApiHubClient.listSpecs(), because the API Hub versions list\r\n * does not reliably populate version.specIds (it is often null even when the\r\n * version has specs). */\r\nexport interface SpecSelection {\r\n versionId: string;\r\n}\r\n\r\n/** Input for resolving a GitHub source-repository link for an entity. */\r\nexport interface GithubLinkInput {\r\n /**\r\n * API Hub `documentation.externalUri` (the \"Documentation\" field) — the\r\n * authoritative repository URL when set. Takes precedence over the\r\n * sourceMetadata scan and the githubOrgSlug convention.\r\n */\r\n documentationUri?: string;\r\n sourceMetadata?: Array<{ sourceType?: string; value?: string }>;\r\n githubOrgSlug?: string;\r\n entityName: string;\r\n}\r\n\r\n/**\r\n * Deterministically selects one version from an API Hub version list (D-02).\r\n *\r\n * Selection rules:\r\n * - Prefer versions whose lifecycle.stage === 'ACTIVE'; if none are ACTIVE,\r\n * use candidates whose stage !== 'ARCHIVED'.\r\n * - Sort by updateTime descending (missing updateTime sorts last),\r\n * tie-break by version resource name ascending.\r\n *\r\n * Note: this intentionally does NOT filter on version.specIds. The API Hub\r\n * versions endpoint frequently returns specIds: null even when the version has\r\n * specs; the actual spec list is resolved separately via ApiHubClient.listSpecs().\r\n *\r\n * @returns the selected version, or undefined when there is no usable version.\r\n */\r\nexport function selectSpec(\r\n versions: ApiHubVersion[],\r\n): SpecSelection | undefined {\r\n const all = versions ?? [];\r\n if (all.length === 0) return undefined;\r\n\r\n const active = all.filter(v => v.lifecycle?.stage === 'ACTIVE');\r\n const candidates =\r\n active.length > 0\r\n ? active\r\n : all.filter(v => v.lifecycle?.stage !== 'ARCHIVED');\r\n if (candidates.length === 0) return undefined;\r\n\r\n const sorted = [...candidates].sort((a, b) => {\r\n // updateTime descending; missing sorts last\r\n const at = a.updateTime ?? '';\r\n const bt = b.updateTime ?? '';\r\n if (at !== bt) {\r\n if (!at) return 1;\r\n if (!bt) return -1;\r\n return at < bt ? 1 : -1;\r\n }\r\n // tie-break by name ascending\r\n return (a.name ?? '').localeCompare(b.name ?? '');\r\n });\r\n\r\n const chosen = sorted[0];\r\n const versionId = (chosen.name ?? '').split('/').pop() ?? '';\r\n if (!versionId) return undefined;\r\n return { versionId };\r\n}\r\n\r\n/**\r\n * Maps an API Hub owner to a Backstage owner entity reference (D-06).\r\n * owner.displayName => group:default/<slug>; otherwise defaultOwner, else\r\n * group:default/unknown.\r\n */\r\nexport function ownerRef(\r\n owner: { displayName?: string; email?: string } | undefined,\r\n defaultOwner?: string,\r\n): string {\r\n if (owner?.displayName) {\r\n return `group:default/${slugify(owner.displayName)}`;\r\n }\r\n return defaultOwner ?? 'group:default/unknown';\r\n}\r\n\r\n/**\r\n * Derives slugified, deduped, sorted tag values from raw API Hub category ids (D-05).\r\n * No extra API Hub call — operates only on the raw ids already present on the API.\r\n */\r\nexport function categoryTags(categories: string[] | undefined): string[] {\r\n const slugs = (categories ?? [])\r\n .map(c => slugify(c))\r\n .filter(c => c.length > 0);\r\n return [...new Set(slugs)].sort();\r\n}\r\n\r\n/**\r\n * Comma-joined category annotation value (D-05), or undefined when there are\r\n * no categories.\r\n */\r\nexport function categoryAnnotation(\r\n categories: string[] | undefined,\r\n): string | undefined {\r\n const tags = categoryTags(categories);\r\n return tags.length > 0 ? tags.join(',') : undefined;\r\n}\r\n\r\n/**\r\n * Matches a USER-defined API Hub attribute whose resource-name id denotes a\r\n * business unit. The system business unit is the top-level `businessUnit` field\r\n * (handled separately); this only backstops a user-modelled one.\r\n */\r\nconst BUSINESS_UNIT_ATTR = /(^|\\/)(system-)?business[-_]?unit$/i;\r\n\r\n/**\r\n * First non-empty value from an API Hub AttributeValues: the enum value's\r\n * displayName (else id), then a string value. Returns undefined when empty.\r\n */\r\nfunction attributeValue(\r\n values: ApiHubAttributeValues | undefined,\r\n): string | undefined {\r\n if (!values) return undefined;\r\n const enumValue = values.enumValues?.values?.find(\r\n v => (v.displayName ?? v.id ?? '').trim() !== '',\r\n );\r\n if (enumValue) return (enumValue.displayName ?? enumValue.id)?.trim();\r\n return values.stringValues?.values?.find(v => v.trim() !== '')?.trim();\r\n}\r\n\r\n/**\r\n * Extracts the raw \"Business unit\" value from an API Hub API. Cloud API Hub\r\n * exposes the system business unit as the TOP-LEVEL `businessUnit` field; a\r\n * user-modelled one in the `attributes` map is scanned as a fallback. Returns\r\n * undefined when neither is present.\r\n */\r\nfunction businessUnitValue(hubApi: ApiHubApi): string | undefined {\r\n const systemValue = attributeValue(hubApi.businessUnit);\r\n if (systemValue) return systemValue;\r\n const attributes = hubApi.attributes;\r\n if (!attributes) return undefined;\r\n for (const [key, values] of Object.entries(attributes)) {\r\n if (!BUSINESS_UNIT_ATTR.test(key)) continue;\r\n const v = attributeValue(values);\r\n if (v) return v;\r\n }\r\n return undefined;\r\n}\r\n\r\n/**\r\n * Resolves the Backstage `department` label value from an API Hub API's\r\n * \"Business unit\" attribute. The raw value is slugified and capped at 63 chars\r\n * so it is always a valid Backstage label value; returns undefined when the API\r\n * has no business unit.\r\n */\r\nexport function departmentLabel(hubApi: ApiHubApi): string | undefined {\r\n const raw = businessUnitValue(hubApi);\r\n if (!raw) return undefined;\r\n const slug = slugify(raw).slice(0, 63).replace(/-+$/g, '');\r\n return slug || undefined;\r\n}\r\n\r\n/**\r\n * Resolves a GitHub source-repository link for an entity (D-05).\r\n *\r\n * SECURITY: only http(s) URLs are ever returned. Any candidate value that does\r\n * not match ^https?:// is rejected to prevent javascript:/data: link injection\r\n * into rendered entity pages.\r\n *\r\n * Resolution order:\r\n * 1. API Hub `documentation.externalUri` (the \"Documentation\" field) — the\r\n * authoritative repository URL (e.g. .../apigee-x-proxy-<proxy-name>).\r\n * 2. First sourceMetadata entry whose value is an http(s) URL (entries whose\r\n * sourceType matches /repo|git|source/i are preferred when several qualify).\r\n * 3. githubOrgSlug convention: https://github.com/<slug>/<entityName>.\r\n * 4. undefined.\r\n */\r\nexport function resolveGithubLink(\r\n input: GithubLinkInput,\r\n): { url: string; title: string } | undefined {\r\n const httpUrl = /^https?:\\/\\//i;\r\n\r\n // 1. API Hub \"Documentation\" field — authoritative when it is an http(s) URL.\r\n if (\r\n typeof input.documentationUri === 'string' &&\r\n httpUrl.test(input.documentationUri)\r\n ) {\r\n return { url: input.documentationUri, title: 'Source repository' };\r\n }\r\n\r\n // 2. sourceMetadata http(s) entry (repo/git/source-typed preferred).\r\n const sources = (input.sourceMetadata ?? []).filter(\r\n s => typeof s.value === 'string' && httpUrl.test(s.value),\r\n );\r\n if (sources.length > 0) {\r\n const preferred =\r\n sources.find(s => /repo|git|source/i.test(s.sourceType ?? '')) ??\r\n sources[0];\r\n return { url: preferred.value as string, title: 'Source repository' };\r\n }\r\n\r\n // 3. githubOrgSlug convention.\r\n if (input.githubOrgSlug) {\r\n return {\r\n url: `https://github.com/${input.githubOrgSlug}/${input.entityName}`,\r\n title: 'Source repository',\r\n };\r\n }\r\n return undefined;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// API Hub entity composition (pure) — D-03, D-04, D-05, D-07\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * The outcome of resolving a spec for an API (computed by the provider, plan 03-04).\r\n * Either embedded inline (`definition`) or linked when over the size limit (`specUrl`).\r\n */\r\nexport interface SpecEmbed {\r\n /** true when a spec exists (embedded OR linked). */\r\n hasSpec: boolean;\r\n /** Decoded UTF-8 spec text when embedded inline. */\r\n definition?: string;\r\n /** API Hub spec resource name when the spec exceeds specMaxBytes (D-03). */\r\n specUrl?: string;\r\n /** Spec `info.version` when parseable; surfaced as `apigee.com/version`. */\r\n version?: string;\r\n}\r\n\r\n/**\r\n * Extracts `info.version` from an OpenAPI/AsyncAPI spec document. Handles JSON\r\n * and YAML without a YAML dependency: JSON is parsed directly; YAML is scanned\r\n * for the `version:` key that is a direct child of the top-level `info:` map.\r\n * Returns undefined when the spec is absent, unparseable, or has no version.\r\n */\r\nexport function extractSpecVersion(\r\n definition: string | undefined,\r\n): string | undefined {\r\n if (!definition) return undefined;\r\n const trimmed = definition.trim();\r\n if (!trimmed) return undefined;\r\n\r\n // JSON specs (a subset of YAML): parse and read info.version directly.\r\n if (trimmed.startsWith('{')) {\r\n try {\r\n const doc = JSON.parse(trimmed) as { info?: { version?: unknown } };\r\n const v = doc.info?.version;\r\n return v == null ? undefined : String(v).trim() || undefined;\r\n } catch {\r\n return undefined;\r\n }\r\n }\r\n\r\n // YAML specs: find the top-level `info:` block, then match `version:` at the\r\n // block's direct-child indent (so a `version` nested deeper is ignored).\r\n let infoIndent = -1;\r\n let childIndent = -1;\r\n for (const raw of definition.split(/\\r?\\n/)) {\r\n const line = raw.replace(/\\t/g, ' ');\r\n const trimmedLine = line.trim();\r\n if (trimmedLine === '' || trimmedLine.startsWith('#')) continue;\r\n const indent = line.length - line.trimStart().length;\r\n\r\n if (infoIndent < 0) {\r\n if (indent === 0 && /^info\\s*:\\s*(#.*)?$/.test(trimmedLine)) {\r\n infoIndent = indent;\r\n }\r\n continue;\r\n }\r\n if (indent <= infoIndent) break; // dedented out of the info block\r\n if (childIndent < 0) childIndent = indent;\r\n if (indent !== childIndent) continue; // deeper nested key — not info.version\r\n const m = /^version\\s*:\\s*(.*)$/.exec(trimmedLine);\r\n if (m) {\r\n let v = m[1].trim();\r\n if (!/^[\"']/.test(v)) v = v.replace(/\\s+#.*$/, '').trim(); // strip comment\r\n v = v.replace(/^\"(.*)\"$/, '$1').replace(/^'(.*)'$/, '$1').trim();\r\n return v || undefined;\r\n }\r\n }\r\n return undefined;\r\n}\r\n\r\n/**\r\n * Applies the shared API Hub metadata (tags, category annotation, GitHub link,\r\n * has-spec + spec.definition/spec-url) to an API entity in place.\r\n *\r\n * Used by both enrichMatchedApiEntity and hubOnlyToEntity to avoid divergence.\r\n * Callers pass a cloned/fresh entity — this never reads from a network.\r\n */\r\nfunction applyHubMetadata(\r\n api: Entity,\r\n hubApi: ApiHubApi,\r\n spec: SpecEmbed | undefined,\r\n githubOrgSlug: string | undefined,\r\n): void {\r\n const annotations = api.metadata.annotations ?? {};\r\n\r\n // Tags + category annotation (D-05, META-09)\r\n const tags = categoryTags(hubApi.categories);\r\n if (tags.length > 0) {\r\n api.metadata.tags = tags;\r\n }\r\n const catAnno = categoryAnnotation(hubApi.categories);\r\n if (catAnno) {\r\n annotations['apigee.com/category'] = catAnno;\r\n }\r\n\r\n // Department label from the API Hub \"Business unit\" attribute.\r\n const department = departmentLabel(hubApi);\r\n if (department) {\r\n api.metadata.labels = { ...(api.metadata.labels ?? {}), department };\r\n }\r\n\r\n // GitHub source link (D-05, META-10) — only ever http(s) per resolveGithubLink\r\n const link = resolveGithubLink({\r\n documentationUri: hubApi.documentation?.externalUri,\r\n sourceMetadata: hubApi.sourceMetadata,\r\n githubOrgSlug,\r\n entityName: api.metadata.name,\r\n });\r\n if (link) {\r\n const links = api.metadata.links ?? [];\r\n if (!links.some(l => l.url === link.url)) {\r\n links.push(link);\r\n }\r\n api.metadata.links = links;\r\n }\r\n\r\n // Spec embed-or-url (D-03, META-05/08)\r\n if (spec?.definition) {\r\n (api.spec as Record<string, unknown>).definition = spec.definition;\r\n annotations['apigee.com/has-spec'] = 'true';\r\n } else if (spec?.specUrl) {\r\n annotations['apigee.com/spec-url'] = spec.specUrl;\r\n annotations['apigee.com/has-spec'] = 'true';\r\n } else {\r\n annotations['apigee.com/has-spec'] = 'false';\r\n }\r\n\r\n // Spec version (info.version) — single API-level annotation.\r\n if (spec?.version) {\r\n annotations['apigee.com/version'] = spec.version;\r\n }\r\n\r\n api.metadata.annotations = annotations;\r\n}\r\n\r\n/**\r\n * Enriches a proxy-derived API entity with matched API Hub metadata (D-03/D-05/D-07).\r\n *\r\n * PURE: deep-copies `baseApi` and returns the copy; never mutates the input, so\r\n * repeated runs with identical inputs are idempotent (ROADMAP SC #4).\r\n *\r\n * @param baseApi - The API entity produced by proxyToEntities.\r\n * @param hubApi - The matched API Hub definition.\r\n * @param spec - Resolved spec embed/url (undefined when none).\r\n * @param githubOrgSlug - Org GitHub slug for link fallback.\r\n * @param options - Build-time options (defaultOwner).\r\n */\r\nexport function enrichMatchedApiEntity(\r\n baseApi: Entity,\r\n hubApi: ApiHubApi,\r\n spec: SpecEmbed | undefined,\r\n githubOrgSlug: string | undefined,\r\n options?: EntityBuildOptions,\r\n): Entity {\r\n const api: Entity = {\r\n ...baseApi,\r\n metadata: {\r\n ...baseApi.metadata,\r\n annotations: { ...(baseApi.metadata.annotations ?? {}) },\r\n ...(baseApi.metadata.tags ? { tags: [...baseApi.metadata.tags] } : {}),\r\n ...(baseApi.metadata.links\r\n ? { links: baseApi.metadata.links.map(l => ({ ...l })) }\r\n : {}),\r\n },\r\n spec: { ...(baseApi.spec ?? {}) },\r\n };\r\n\r\n // Metadata backfill (D-07, META-02/03/07)\r\n if (hubApi.displayName) {\r\n api.metadata.title = hubApi.displayName;\r\n }\r\n api.metadata.description = hubApi.description ?? '';\r\n (api.spec as Record<string, unknown>).owner = ownerRef(\r\n hubApi.owner,\r\n options?.defaultOwner,\r\n );\r\n\r\n applyHubMetadata(api, hubApi, spec, githubOrgSlug);\r\n return api;\r\n}\r\n\r\n/**\r\n * Builds a standalone API entity for an API Hub definition with no matching\r\n * Apigee proxy (D-04). Lifecycle is always 'experimental'; the full API Hub\r\n * resource name is recorded in `apigee.com/api-hub-id`.\r\n *\r\n * PURE: derives output solely from inputs (idempotent).\r\n *\r\n * @param hubApi - The unmatched API Hub definition.\r\n * @param orgName - Raw org name this definition was discovered under.\r\n * @param spec - Resolved spec embed/url (undefined when none).\r\n * @param githubOrgSlug - Org GitHub slug for link fallback.\r\n * @param options - Build-time options (defaultOwner).\r\n */\r\nexport function hubOnlyToEntity(\r\n hubApi: ApiHubApi,\r\n orgName: string,\r\n spec: SpecEmbed | undefined,\r\n githubOrgSlug: string | undefined,\r\n options?: EntityBuildOptions,\r\n): Entity {\r\n const apiId = (hubApi.name ?? '').split('/').pop() ?? '';\r\n const display = hubApi.displayName ?? apiId;\r\n const slug = slugify(display);\r\n\r\n const api: Entity = {\r\n apiVersion: 'backstage.io/v1alpha1',\r\n kind: 'API',\r\n metadata: {\r\n name: slug,\r\n title: display,\r\n namespace: 'default',\r\n description: hubApi.description ?? '',\r\n annotations: {\r\n [ANNOTATION_LOCATION]: APIGEE_LOCATION,\r\n [ANNOTATION_ORIGIN_LOCATION]: APIGEE_LOCATION,\r\n // Full API Hub resource name (D-04) — distinguishes hub-only entities.\r\n 'apigee.com/api-hub-id': hubApi.name,\r\n 'apigee.com/org': orgName,\r\n },\r\n },\r\n spec: {\r\n type: hubApi.apiType ?? 'other',\r\n lifecycle: 'experimental',\r\n owner: ownerRef(hubApi.owner, options?.defaultOwner),\r\n definition: '',\r\n },\r\n };\r\n\r\n applyHubMetadata(api, hubApi, spec, githubOrgSlug);\r\n return api;\r\n}\r\n\r\n/**\r\n * Builds a [Component, API] entity pair from a merged proxy (D-01, D-03).\r\n *\r\n * @param merged - Deduplicated proxy with per-org data.\r\n * @param apiType - API type from API Hub (D-01); defaults to 'openapi'.\r\n * @param options - Build-time options (defaultOwner, etc.)\r\n * @returns Tuple [componentEntity, apiEntity]\r\n */\r\nexport function proxyToEntities(\r\n merged: MergedProxy,\r\n apiType: string | undefined,\r\n options?: EntityBuildOptions,\r\n): [Entity, Entity] {\r\n const slug = slugify(merged.proxy.name);\r\n const lifecycle = resolveHighestLifecycle(merged.orgs);\r\n const owner = options?.defaultOwner ?? 'group:default/unknown';\r\n const orgsAnnotation = merged.orgs.map(o => o.name).join(',');\r\n\r\n // Per-org annotations (D-03):\r\n // apigee.com/<org-slug>/lifecycle\r\n // apigee.com/<org-slug>/environment/<env>/revision (one key per deployed env)\r\n const perOrgAnnotations: Record<string, string> = {};\r\n for (const org of merged.orgs) {\r\n const safe = slugify(org.slug || org.name);\r\n perOrgAnnotations[`apigee.com/${safe}/lifecycle`] = org.lifecycle;\r\n for (const env of org.environments) {\r\n const envSlug = slugify(env.name);\r\n perOrgAnnotations[\r\n `apigee.com/${safe}/environment/${envSlug}/revision`\r\n ] = env.revision;\r\n }\r\n }\r\n\r\n const sharedAnnotations: Record<string, string> = {\r\n [ANNOTATION_LOCATION]: APIGEE_LOCATION,\r\n [ANNOTATION_ORIGIN_LOCATION]: APIGEE_LOCATION,\r\n 'apigee.com/proxy-name': merged.proxy.name,\r\n 'apigee.com/orgs': orgsAnnotation,\r\n };\r\n\r\n const component: Entity = {\r\n apiVersion: 'backstage.io/v1alpha1',\r\n kind: 'Component',\r\n metadata: {\r\n name: slug,\r\n title: merged.proxy.name,\r\n namespace: 'default',\r\n annotations: {\r\n ...sharedAnnotations,\r\n 'apigee.com/base-path': merged.basePaths.join(','),\r\n 'apigee.com/has-spec': 'false',\r\n ...perOrgAnnotations,\r\n },\r\n },\r\n spec: {\r\n type: 'api-proxy',\r\n lifecycle,\r\n owner,\r\n },\r\n };\r\n\r\n const api: Entity = {\r\n apiVersion: 'backstage.io/v1alpha1',\r\n kind: 'API',\r\n metadata: {\r\n name: slug,\r\n title: merged.proxy.name,\r\n namespace: 'default',\r\n annotations: {\r\n ...sharedAnnotations,\r\n // Default false; 03-03 enrichment overrides to 'true' when a spec is found (META-05, SC #2).\r\n 'apigee.com/has-spec': 'false',\r\n },\r\n },\r\n spec: {\r\n type: apiType ?? 'openapi',\r\n lifecycle,\r\n owner,\r\n definition: '',\r\n },\r\n };\r\n\r\n return [component, api];\r\n}\r\n\r\n/**\r\n * Builds a Component entity from an Apigee sharedflow (D-04).\r\n * Placed in the 'apigee-sharedflows' namespace with spec.type 'library'.\r\n *\r\n * @param sharedflow - Sharedflow data from SharedflowClient.\r\n * @param orgName - Raw org name that owns this sharedflow.\r\n * @param options - Build-time options.\r\n */\r\nexport function sharedflowToEntity(\r\n sharedflow: ApigeeSharedflow,\r\n orgName: string,\r\n options?: EntityBuildOptions,\r\n): Entity {\r\n const slug = slugify(sharedflow.name);\r\n const owner = options?.defaultOwner ?? 'group:default/unknown';\r\n\r\n return {\r\n apiVersion: 'backstage.io/v1alpha1',\r\n kind: 'Component',\r\n metadata: {\r\n name: slug,\r\n title: sharedflow.name,\r\n namespace: 'apigee-sharedflows',\r\n annotations: {\r\n [ANNOTATION_LOCATION]: APIGEE_LOCATION,\r\n [ANNOTATION_ORIGIN_LOCATION]: APIGEE_LOCATION,\r\n 'apigee.com/proxy-name': sharedflow.name,\r\n 'apigee.com/orgs': orgName,\r\n },\r\n },\r\n spec: {\r\n type: 'library',\r\n lifecycle: 'production',\r\n owner,\r\n },\r\n };\r\n}\r\n"],"names":["ANNOTATION_LOCATION","ANNOTATION_ORIGIN_LOCATION"],"mappings":"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"}
|