Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/graph-slice-urls-first-class.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@executor-js/plugin-openapi": patch
---

Make Microsoft Graph slice URLs first-class spec sources instead of a hidden substitution. Catalog tiles now point directly at the slice release assets, the stored specUrl is exactly what gets fetched, and selection narrowing travels visibly in the URL fragment; requesting the upstream monolith URL fetches the monolith, never a silently swapped slice.
6 changes: 3 additions & 3 deletions e2e/scenarios/microsoft-graph-default.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { composePluginApi } from "@executor-js/api/server";
import {
MICROSOFT_AUTH_TEMPLATE_SLUG,
MICROSOFT_AUTHORIZATION_URL,
MICROSOFT_GRAPH_OPENAPI_URL,
MICROSOFT_TOKEN_URL,
microsoftCatalog,
microsoftGraphAdapter,
microsoftGraphSliceUrl,
} from "@executor-js/plugin-openapi/providers/microsoft";
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
import {
Expand Down Expand Up @@ -60,7 +60,7 @@ scenario(
// isolate the same way it did the production one.
const preview = yield* client.openapi.previewSpec({
payload: {
spec: `${MICROSOFT_GRAPH_OPENAPI_URL}#preset=${MICROSOFT_FILES_PRESET_ID}`,
spec: microsoftGraphSliceUrl(MICROSOFT_FILES_PRESET_ID),
specFormat: "microsoft-graph",
},
});
Expand All @@ -77,7 +77,7 @@ scenario(
payload: {
spec: {
kind: "url",
url: `${MICROSOFT_GRAPH_OPENAPI_URL}#preset=${MICROSOFT_FILES_PRESET_ID}`,
url: microsoftGraphSliceUrl(MICROSOFT_FILES_PRESET_ID),
},
slug: integration,
name: "Microsoft Graph Files",
Expand Down
4 changes: 2 additions & 2 deletions e2e/scenarios/microsoft-graph-full.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { Effect } from "effect";
import { composePluginApi } from "@executor-js/api/server";
import {
MICROSOFT_AUTH_TEMPLATE_SLUG,
MICROSOFT_GRAPH_OPENAPI_URL,
microsoftCatalog,
microsoftGraphAdapter,
microsoftGraphSliceUrl,
} from "@executor-js/plugin-openapi/providers/microsoft";
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk/shared";
Expand Down Expand Up @@ -57,7 +57,7 @@ scenario(
payload: {
spec: {
kind: "url",
url: `${MICROSOFT_GRAPH_OPENAPI_URL}#preset=${MICROSOFT_FILES_PRESET_ID}`,
url: microsoftGraphSliceUrl(MICROSOFT_FILES_PRESET_ID),
},
slug: integration,
name: "Microsoft Graph Files",
Expand Down
6 changes: 4 additions & 2 deletions e2e/scenarios/provider-plugins-ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ scenario(

await step("A Microsoft service preset opens the OpenAPI add flow", async () => {
await page.goto(
"/integrations/add/openapi?preset=microsoft-files&url=https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoftgraph%2Fmsgraph-metadata%2Fmaster%2Fopenapi%2Fv1.0%2Fopenapi.yaml%23preset%3Dfiles",
"/integrations/add/openapi?preset=microsoft-files&url=https%3A%2F%2Fgithub.com%2FUsefulSoftwareCo%2Fexecutor%2Freleases%2Fdownload%2Fgraph-slices%2Ffiles.yaml",
{ waitUntil: "domcontentloaded" },
);
await page.getByRole("heading", { name: "Add OpenAPI integration" }).waitFor();
await expect.poll(() => page.locator("textarea").inputValue()).toContain("preset=files");
await expect
.poll(() => page.locator("textarea").inputValue())
.toContain("graph-slices/files.yaml");
});
});
}),
Expand Down
48 changes: 28 additions & 20 deletions packages/plugins/openapi/src/providers/microsoft/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import {
} from "../../sdk/split";
import type { Authentication } from "../../sdk/types";

import { fetchMicrosoftGraphSlice, microsoftGraphSliceAssetForSelection } from "./slices";
import {
fetchMicrosoftGraphSlice,
microsoftGraphPresetIdsForSliceAsset,
microsoftGraphSliceAssetFromUrl,
} from "./slices";
import {
MICROSOFT_AUTHORIZATION_URL,
MICROSOFT_AUTH_TEMPLATE_SLUG,
Expand Down Expand Up @@ -244,6 +248,10 @@ const normalizeMicrosoftGraphSpecUrl = (
policy?: MicrosoftGraphUrlPolicy,
): string | null => {
if (value === MICROSOFT_GRAPH_OPENAPI_URL) return value;
const sliceAsset = microsoftGraphSliceAssetFromUrl(value);
if (sliceAsset !== null && microsoftGraphPresetIdsForSliceAsset(sliceAsset) !== null) {
return value;
}
return allowUnsafeUrl(value, policy) ?? null;
};

Expand Down Expand Up @@ -759,26 +767,26 @@ export const buildMicrosoftGraphOpenApiSpec = (
urlPolicy?: MicrosoftGraphUrlPolicy,
): Effect.Effect<MicrosoftGraphSpecBuild, OpenApiParseError> =>
Effect.gen(function* () {
const selection = yield* validateSelectionUrls(normalizeSelection(input), urlPolicy);
// Covered selections read a precomputed slice (sub-MB) instead of the 43MB
// monolith: in production, the monolith fetch alone almost never survives
// the 128MB isolate (once in the 30 days before 2026-08-26). Slices apply
// only to the pinned Microsoft URL — an override (local Graph emulators)
// serves its own document. A missing/failed slice (asset not yet published,
// release unreachable) falls back to the monolith path, which is the prior
// behavior for the selections a slice would have covered.
const sliceAsset =
selection.specUrl === MICROSOFT_GRAPH_OPENAPI_URL
? microsoftGraphSliceAssetForSelection(selection)
: null;
// A slice URL carries its own selection: when the caller passes no preset
// ids, the asset's selection applies (rather than the default bundle).
const inputSliceAsset = input.specUrl
? microsoftGraphSliceAssetFromUrl(input.specUrl.trim())
: null;
const inputSlicePresetIds =
inputSliceAsset !== null ? microsoftGraphPresetIdsForSliceAsset(inputSliceAsset) : null;
const selectionInput =
inputSlicePresetIds !== null && (!input.presetIds || input.presetIds.length === 0)
? { ...input, presetIds: inputSlicePresetIds }
: input;
const selection = yield* validateSelectionUrls(normalizeSelection(selectionInput), urlPolicy);
// The URL is the byte source, never substituted. Catalog selections point
// at precomputed slice URLs (the 43MB monolith cannot be processed in a
// 128MB isolate — its fetch completed once in the 30 days before
// 2026-08-26); the monolith and emulator-override URLs fetch exactly what
// they name.
const sourceText =
sliceAsset !== null
? yield* fetchMicrosoftGraphSlice(sliceAsset).pipe(
Effect.catchTag("OpenApiParseError", () =>
fetchMicrosoftGraphOpenApiSpec(selection.specUrl),
),
Effect.provide(httpClientLayer),
)
microsoftGraphSliceAssetFromUrl(selection.specUrl) !== null
? yield* fetchMicrosoftGraphSlice(selection.specUrl).pipe(Effect.provide(httpClientLayer))
: yield* fetchMicrosoftGraphOpenApiSpec(selection.specUrl).pipe(
Effect.provide(httpClientLayer),
);
Expand Down
8 changes: 8 additions & 0 deletions packages/plugins/openapi/src/providers/microsoft/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,11 @@ export {
type MicrosoftGraphSpecBuild,
} from "./graph";
export { microsoftGraphAdapter } from "./spec-format-adapter";
export {
MICROSOFT_GRAPH_DEFAULT_SLICE_ASSET,
MICROSOFT_GRAPH_SLICE_BASE_URL,
MICROSOFT_GRAPH_SLICE_RELEASE_TAG,
microsoftGraphPresetIdsForSliceAsset,
microsoftGraphSliceAssetFromUrl,
microsoftGraphSliceUrl,
} from "./slices";
8 changes: 6 additions & 2 deletions packages/plugins/openapi/src/providers/microsoft/presets.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { IntegrationPreset } from "@executor-js/sdk/core";

import { microsoftGraphSliceUrl } from "./slice-urls";

export interface MicrosoftGraphPreset {
readonly id: string;
readonly name: string;
Expand Down Expand Up @@ -528,8 +530,10 @@ export const microsoftGraphTagPrefixesForPresetIds = (
export const microsoftServiceSlug = (presetId: string): string =>
`microsoft_${presetId.replaceAll("-", "_")}`;

const microsoftGraphCatalogUrl = (presetId: string): string =>
`${MICROSOFT_GRAPH_OPENAPI_URL}#preset=${encodeURIComponent(presetId)}`;
// Catalog tiles point at the slice URL itself: the URL a user sees (and the
// integration stores) is exactly what gets fetched — no server-side source
// substitution. The slice asset name carries the selection.
const microsoftGraphCatalogUrl = (presetId: string): string => microsoftGraphSliceUrl(presetId);

const microsoftGraphCatalogAuthTemplate = (preset: MicrosoftGraphScopePreset) => [
{
Expand Down
37 changes: 37 additions & 0 deletions packages/plugins/openapi/src/providers/microsoft/slice-urls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Microsoft Graph slice URL vocabulary. Leaf module (no preset imports) shared
* by the catalog (`presets.ts`), the runtime fetch (`slices.ts`), and the
* adapter's URL classification.
*
* Slice URLs are first-class spec sources: the catalog points at them
* directly, what the integration stores as `specUrl` is what was fetched, and
* any narrowing within a slice travels visibly in the URL fragment
* (`#preset=mail,calendar`). The 43MB upstream monolith URL is never silently
* substituted — requesting it fetches it.
*/

export const MICROSOFT_GRAPH_SLICE_RELEASE_TAG = "graph-slices";

export const MICROSOFT_GRAPH_SLICE_BASE_URL = `https://github.com/UsefulSoftwareCo/executor/releases/download/${MICROSOFT_GRAPH_SLICE_RELEASE_TAG}`;

/** Asset covering the default catalog bundle (`MICROSOFT_GRAPH_DEFAULT_PRESET_IDS`). */
export const MICROSOFT_GRAPH_DEFAULT_SLICE_ASSET = "default";

export const microsoftGraphSliceUrl = (asset: string): string =>
`${MICROSOFT_GRAPH_SLICE_BASE_URL}/${encodeURIComponent(asset)}.yaml`;

/** The asset a slice URL names, or null for any other URL. Fragment and query
* are ignored — callers strip the fragment into a selection separately. */
export const microsoftGraphSliceAssetFromUrl = (url: string): string | null => {
if (!URL.canParse(url)) return null;
const parsed = new URL(url);
parsed.hash = "";
const href = parsed.toString();
if (!href.startsWith(`${MICROSOFT_GRAPH_SLICE_BASE_URL}/`) || !href.endsWith(".yaml")) {
return null;
}
const asset = decodeURIComponent(
href.slice(`${MICROSOFT_GRAPH_SLICE_BASE_URL}/`.length, -".yaml".length),
);
return asset.length > 0 && !asset.includes("/") ? asset : null;
};
80 changes: 25 additions & 55 deletions packages/plugins/openapi/src/providers/microsoft/slices.test.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,40 @@
import { describe, expect, it } from "@effect/vitest";

import { MICROSOFT_GRAPH_ALL_PRESET_IDS, MICROSOFT_GRAPH_DEFAULT_PRESET_IDS } from "./presets";
import { MICROSOFT_GRAPH_DEFAULT_PRESET_IDS, MICROSOFT_GRAPH_OPENAPI_URL } from "./presets";
import {
MICROSOFT_GRAPH_DEFAULT_SLICE_ASSET,
microsoftGraphSliceAssetForSelection,
microsoftGraphPresetIdsForSliceAsset,
microsoftGraphSliceAssetFromUrl,
microsoftGraphSliceUrl,
} from "./slices";

describe("microsoftGraphSliceAssetForSelection", () => {
it("maps a single catalog preset to its asset", () => {
describe("microsoftGraphSliceAssetFromUrl", () => {
it("round-trips slice URLs and ignores fragments", () => {
expect(microsoftGraphSliceAssetFromUrl(microsoftGraphSliceUrl("mail"))).toBe("mail");
expect(
microsoftGraphSliceAssetForSelection({
coversFullGraph: false,
presetIds: ["mail"],
customScopes: [],
}),
).toBe("mail");
microsoftGraphSliceAssetFromUrl(`${microsoftGraphSliceUrl("default")}#preset=mail,calendar`),
).toBe(MICROSOFT_GRAPH_DEFAULT_SLICE_ASSET);
});

it("maps the default bundle in any order to the default asset", () => {
expect(
microsoftGraphSliceAssetForSelection({
coversFullGraph: false,
presetIds: [...MICROSOFT_GRAPH_DEFAULT_PRESET_IDS].reverse(),
customScopes: [],
}),
).toBe(MICROSOFT_GRAPH_DEFAULT_SLICE_ASSET);
it("rejects non-slice URLs", () => {
expect(microsoftGraphSliceAssetFromUrl(MICROSOFT_GRAPH_OPENAPI_URL)).toBeNull();
expect(microsoftGraphSliceAssetFromUrl("https://example.com/mail.yaml")).toBeNull();
expect(microsoftGraphSliceAssetFromUrl("not a url")).toBeNull();
});
});

it("serves combinations within the default bundle from the default slice", () => {
expect(
microsoftGraphSliceAssetForSelection({
coversFullGraph: false,
presetIds: ["mail", "calendar"],
customScopes: [],
}),
).toBe(MICROSOFT_GRAPH_DEFAULT_SLICE_ASSET);
describe("microsoftGraphPresetIdsForSliceAsset", () => {
it("maps a preset asset to its single preset", () => {
expect(microsoftGraphPresetIdsForSliceAsset("mail")).toEqual(["mail"]);
});

it("needs the monolith for full-graph, custom scopes, unknown presets, and combinations outside the default bundle", () => {
expect(
microsoftGraphSliceAssetForSelection({
coversFullGraph: true,
presetIds: [...MICROSOFT_GRAPH_ALL_PRESET_IDS],
customScopes: [],
}),
).toBeNull();
expect(
microsoftGraphSliceAssetForSelection({
coversFullGraph: false,
presetIds: ["mail"],
customScopes: ["Chat.Read"],
}),
).toBeNull();
expect(
microsoftGraphSliceAssetForSelection({
coversFullGraph: false,
presetIds: ["not-a-preset"],
customScopes: [],
}),
).toBeNull();
expect(
microsoftGraphSliceAssetForSelection({
coversFullGraph: false,
presetIds: ["mail", "users"],
customScopes: [],
}),
).toBeNull();
it("maps the default asset to the default bundle", () => {
expect(microsoftGraphPresetIdsForSliceAsset(MICROSOFT_GRAPH_DEFAULT_SLICE_ASSET)).toEqual(
MICROSOFT_GRAPH_DEFAULT_PRESET_IDS,
);
});

it("returns null for unknown assets", () => {
expect(microsoftGraphPresetIdsForSliceAsset("not-a-preset")).toBeNull();
});
});
Loading
Loading