Skip to content

Commit 37ddaa8

Browse files
committed
Make Microsoft Graph slice URLs first-class spec sources
1 parent 6574ab1 commit 37ddaa8

12 files changed

Lines changed: 214 additions & 144 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@executor-js/plugin-openapi": patch
3+
---
4+
5+
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.

e2e/scenarios/microsoft-graph-default.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { composePluginApi } from "@executor-js/api/server";
66
import {
77
MICROSOFT_AUTH_TEMPLATE_SLUG,
88
MICROSOFT_AUTHORIZATION_URL,
9-
MICROSOFT_GRAPH_OPENAPI_URL,
109
MICROSOFT_TOKEN_URL,
1110
microsoftCatalog,
1211
microsoftGraphAdapter,
12+
microsoftGraphSliceUrl,
1313
} from "@executor-js/plugin-openapi/providers/microsoft";
1414
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
1515
import {
@@ -60,7 +60,7 @@ scenario(
6060
// isolate the same way it did the production one.
6161
const preview = yield* client.openapi.previewSpec({
6262
payload: {
63-
spec: `${MICROSOFT_GRAPH_OPENAPI_URL}#preset=${MICROSOFT_FILES_PRESET_ID}`,
63+
spec: microsoftGraphSliceUrl(MICROSOFT_FILES_PRESET_ID),
6464
specFormat: "microsoft-graph",
6565
},
6666
});
@@ -77,7 +77,7 @@ scenario(
7777
payload: {
7878
spec: {
7979
kind: "url",
80-
url: `${MICROSOFT_GRAPH_OPENAPI_URL}#preset=${MICROSOFT_FILES_PRESET_ID}`,
80+
url: microsoftGraphSliceUrl(MICROSOFT_FILES_PRESET_ID),
8181
},
8282
slug: integration,
8383
name: "Microsoft Graph Files",

e2e/scenarios/microsoft-graph-full.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { Effect } from "effect";
55
import { composePluginApi } from "@executor-js/api/server";
66
import {
77
MICROSOFT_AUTH_TEMPLATE_SLUG,
8-
MICROSOFT_GRAPH_OPENAPI_URL,
98
microsoftCatalog,
109
microsoftGraphAdapter,
10+
microsoftGraphSliceUrl,
1111
} from "@executor-js/plugin-openapi/providers/microsoft";
1212
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
1313
import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk/shared";
@@ -57,7 +57,7 @@ scenario(
5757
payload: {
5858
spec: {
5959
kind: "url",
60-
url: `${MICROSOFT_GRAPH_OPENAPI_URL}#preset=${MICROSOFT_FILES_PRESET_ID}`,
60+
url: microsoftGraphSliceUrl(MICROSOFT_FILES_PRESET_ID),
6161
},
6262
slug: integration,
6363
name: "Microsoft Graph Files",

e2e/scenarios/provider-plugins-ui.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,13 @@ scenario(
5757

5858
await step("A Microsoft service preset opens the OpenAPI add flow", async () => {
5959
await page.goto(
60-
"/integrations/add/openapi?preset=microsoft-files&url=https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoftgraph%2Fmsgraph-metadata%2Fmaster%2Fopenapi%2Fv1.0%2Fopenapi.yaml%23preset%3Dfiles",
60+
"/integrations/add/openapi?preset=microsoft-files&url=https%3A%2F%2Fgithub.com%2FUsefulSoftwareCo%2Fexecutor%2Freleases%2Fdownload%2Fgraph-slices%2Ffiles.yaml",
6161
{ waitUntil: "domcontentloaded" },
6262
);
6363
await page.getByRole("heading", { name: "Add OpenAPI integration" }).waitFor();
64-
await expect.poll(() => page.locator("textarea").inputValue()).toContain("preset=files");
64+
await expect
65+
.poll(() => page.locator("textarea").inputValue())
66+
.toContain("graph-slices/files.yaml");
6567
});
6668
});
6769
}),

packages/plugins/openapi/src/providers/microsoft/graph.ts

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import {
1515
} from "../../sdk/split";
1616
import type { Authentication } from "../../sdk/types";
1717

18-
import { fetchMicrosoftGraphSlice, microsoftGraphSliceAssetForSelection } from "./slices";
18+
import {
19+
fetchMicrosoftGraphSlice,
20+
microsoftGraphPresetIdsForSliceAsset,
21+
microsoftGraphSliceAssetFromUrl,
22+
} from "./slices";
1923
import {
2024
MICROSOFT_AUTHORIZATION_URL,
2125
MICROSOFT_AUTH_TEMPLATE_SLUG,
@@ -244,6 +248,10 @@ const normalizeMicrosoftGraphSpecUrl = (
244248
policy?: MicrosoftGraphUrlPolicy,
245249
): string | null => {
246250
if (value === MICROSOFT_GRAPH_OPENAPI_URL) return value;
251+
const sliceAsset = microsoftGraphSliceAssetFromUrl(value);
252+
if (sliceAsset !== null && microsoftGraphPresetIdsForSliceAsset(sliceAsset) !== null) {
253+
return value;
254+
}
247255
return allowUnsafeUrl(value, policy) ?? null;
248256
};
249257

@@ -759,26 +767,26 @@ export const buildMicrosoftGraphOpenApiSpec = (
759767
urlPolicy?: MicrosoftGraphUrlPolicy,
760768
): Effect.Effect<MicrosoftGraphSpecBuild, OpenApiParseError> =>
761769
Effect.gen(function* () {
762-
const selection = yield* validateSelectionUrls(normalizeSelection(input), urlPolicy);
763-
// Covered selections read a precomputed slice (sub-MB) instead of the 43MB
764-
// monolith: in production, the monolith fetch alone almost never survives
765-
// the 128MB isolate (once in the 30 days before 2026-08-26). Slices apply
766-
// only to the pinned Microsoft URL — an override (local Graph emulators)
767-
// serves its own document. A missing/failed slice (asset not yet published,
768-
// release unreachable) falls back to the monolith path, which is the prior
769-
// behavior for the selections a slice would have covered.
770-
const sliceAsset =
771-
selection.specUrl === MICROSOFT_GRAPH_OPENAPI_URL
772-
? microsoftGraphSliceAssetForSelection(selection)
773-
: null;
770+
// A slice URL carries its own selection: when the caller passes no preset
771+
// ids, the asset's selection applies (rather than the default bundle).
772+
const inputSliceAsset = input.specUrl
773+
? microsoftGraphSliceAssetFromUrl(input.specUrl.trim())
774+
: null;
775+
const inputSlicePresetIds =
776+
inputSliceAsset !== null ? microsoftGraphPresetIdsForSliceAsset(inputSliceAsset) : null;
777+
const selectionInput =
778+
inputSlicePresetIds !== null && (!input.presetIds || input.presetIds.length === 0)
779+
? { ...input, presetIds: inputSlicePresetIds }
780+
: input;
781+
const selection = yield* validateSelectionUrls(normalizeSelection(selectionInput), urlPolicy);
782+
// The URL is the byte source, never substituted. Catalog selections point
783+
// at precomputed slice URLs (the 43MB monolith cannot be processed in a
784+
// 128MB isolate — its fetch completed once in the 30 days before
785+
// 2026-08-26); the monolith and emulator-override URLs fetch exactly what
786+
// they name.
774787
const sourceText =
775-
sliceAsset !== null
776-
? yield* fetchMicrosoftGraphSlice(sliceAsset).pipe(
777-
Effect.catchTag("OpenApiParseError", () =>
778-
fetchMicrosoftGraphOpenApiSpec(selection.specUrl),
779-
),
780-
Effect.provide(httpClientLayer),
781-
)
788+
microsoftGraphSliceAssetFromUrl(selection.specUrl) !== null
789+
? yield* fetchMicrosoftGraphSlice(selection.specUrl).pipe(Effect.provide(httpClientLayer))
782790
: yield* fetchMicrosoftGraphOpenApiSpec(selection.specUrl).pipe(
783791
Effect.provide(httpClientLayer),
784792
);

packages/plugins/openapi/src/providers/microsoft/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,11 @@ export {
3939
type MicrosoftGraphSpecBuild,
4040
} from "./graph";
4141
export { microsoftGraphAdapter } from "./spec-format-adapter";
42+
export {
43+
MICROSOFT_GRAPH_DEFAULT_SLICE_ASSET,
44+
MICROSOFT_GRAPH_SLICE_BASE_URL,
45+
MICROSOFT_GRAPH_SLICE_RELEASE_TAG,
46+
microsoftGraphPresetIdsForSliceAsset,
47+
microsoftGraphSliceAssetFromUrl,
48+
microsoftGraphSliceUrl,
49+
} from "./slices";

packages/plugins/openapi/src/providers/microsoft/presets.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { IntegrationPreset } from "@executor-js/sdk/core";
22

3+
import { microsoftGraphSliceUrl } from "./slice-urls";
4+
35
export interface MicrosoftGraphPreset {
46
readonly id: string;
57
readonly name: string;
@@ -528,8 +530,10 @@ export const microsoftGraphTagPrefixesForPresetIds = (
528530
export const microsoftServiceSlug = (presetId: string): string =>
529531
`microsoft_${presetId.replaceAll("-", "_")}`;
530532

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

534538
const microsoftGraphCatalogAuthTemplate = (preset: MicrosoftGraphScopePreset) => [
535539
{
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Microsoft Graph slice URL vocabulary. Leaf module (no preset imports) shared
3+
* by the catalog (`presets.ts`), the runtime fetch (`slices.ts`), and the
4+
* adapter's URL classification.
5+
*
6+
* Slice URLs are first-class spec sources: the catalog points at them
7+
* directly, what the integration stores as `specUrl` is what was fetched, and
8+
* any narrowing within a slice travels visibly in the URL fragment
9+
* (`#preset=mail,calendar`). The 43MB upstream monolith URL is never silently
10+
* substituted — requesting it fetches it.
11+
*/
12+
13+
export const MICROSOFT_GRAPH_SLICE_RELEASE_TAG = "graph-slices";
14+
15+
export const MICROSOFT_GRAPH_SLICE_BASE_URL = `https://github.com/UsefulSoftwareCo/executor/releases/download/${MICROSOFT_GRAPH_SLICE_RELEASE_TAG}`;
16+
17+
/** Asset covering the default catalog bundle (`MICROSOFT_GRAPH_DEFAULT_PRESET_IDS`). */
18+
export const MICROSOFT_GRAPH_DEFAULT_SLICE_ASSET = "default";
19+
20+
export const microsoftGraphSliceUrl = (asset: string): string =>
21+
`${MICROSOFT_GRAPH_SLICE_BASE_URL}/${encodeURIComponent(asset)}.yaml`;
22+
23+
/** The asset a slice URL names, or null for any other URL. Fragment and query
24+
* are ignored — callers strip the fragment into a selection separately. */
25+
export const microsoftGraphSliceAssetFromUrl = (url: string): string | null => {
26+
if (!URL.canParse(url)) return null;
27+
const parsed = new URL(url);
28+
parsed.hash = "";
29+
const href = parsed.toString();
30+
if (!href.startsWith(`${MICROSOFT_GRAPH_SLICE_BASE_URL}/`) || !href.endsWith(".yaml")) {
31+
return null;
32+
}
33+
const asset = decodeURIComponent(
34+
href.slice(`${MICROSOFT_GRAPH_SLICE_BASE_URL}/`.length, -".yaml".length),
35+
);
36+
return asset.length > 0 && !asset.includes("/") ? asset : null;
37+
};
Lines changed: 25 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,40 @@
11
import { describe, expect, it } from "@effect/vitest";
22

3-
import { MICROSOFT_GRAPH_ALL_PRESET_IDS, MICROSOFT_GRAPH_DEFAULT_PRESET_IDS } from "./presets";
3+
import { MICROSOFT_GRAPH_DEFAULT_PRESET_IDS, MICROSOFT_GRAPH_OPENAPI_URL } from "./presets";
44
import {
55
MICROSOFT_GRAPH_DEFAULT_SLICE_ASSET,
6-
microsoftGraphSliceAssetForSelection,
6+
microsoftGraphPresetIdsForSliceAsset,
7+
microsoftGraphSliceAssetFromUrl,
8+
microsoftGraphSliceUrl,
79
} from "./slices";
810

9-
describe("microsoftGraphSliceAssetForSelection", () => {
10-
it("maps a single catalog preset to its asset", () => {
11+
describe("microsoftGraphSliceAssetFromUrl", () => {
12+
it("round-trips slice URLs and ignores fragments", () => {
13+
expect(microsoftGraphSliceAssetFromUrl(microsoftGraphSliceUrl("mail"))).toBe("mail");
1114
expect(
12-
microsoftGraphSliceAssetForSelection({
13-
coversFullGraph: false,
14-
presetIds: ["mail"],
15-
customScopes: [],
16-
}),
17-
).toBe("mail");
15+
microsoftGraphSliceAssetFromUrl(`${microsoftGraphSliceUrl("default")}#preset=mail,calendar`),
16+
).toBe(MICROSOFT_GRAPH_DEFAULT_SLICE_ASSET);
1817
});
1918

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

30-
it("serves combinations within the default bundle from the default slice", () => {
31-
expect(
32-
microsoftGraphSliceAssetForSelection({
33-
coversFullGraph: false,
34-
presetIds: ["mail", "calendar"],
35-
customScopes: [],
36-
}),
37-
).toBe(MICROSOFT_GRAPH_DEFAULT_SLICE_ASSET);
26+
describe("microsoftGraphPresetIdsForSliceAsset", () => {
27+
it("maps a preset asset to its single preset", () => {
28+
expect(microsoftGraphPresetIdsForSliceAsset("mail")).toEqual(["mail"]);
3829
});
3930

40-
it("needs the monolith for full-graph, custom scopes, unknown presets, and combinations outside the default bundle", () => {
41-
expect(
42-
microsoftGraphSliceAssetForSelection({
43-
coversFullGraph: true,
44-
presetIds: [...MICROSOFT_GRAPH_ALL_PRESET_IDS],
45-
customScopes: [],
46-
}),
47-
).toBeNull();
48-
expect(
49-
microsoftGraphSliceAssetForSelection({
50-
coversFullGraph: false,
51-
presetIds: ["mail"],
52-
customScopes: ["Chat.Read"],
53-
}),
54-
).toBeNull();
55-
expect(
56-
microsoftGraphSliceAssetForSelection({
57-
coversFullGraph: false,
58-
presetIds: ["not-a-preset"],
59-
customScopes: [],
60-
}),
61-
).toBeNull();
62-
expect(
63-
microsoftGraphSliceAssetForSelection({
64-
coversFullGraph: false,
65-
presetIds: ["mail", "users"],
66-
customScopes: [],
67-
}),
68-
).toBeNull();
31+
it("maps the default asset to the default bundle", () => {
32+
expect(microsoftGraphPresetIdsForSliceAsset(MICROSOFT_GRAPH_DEFAULT_SLICE_ASSET)).toEqual(
33+
MICROSOFT_GRAPH_DEFAULT_PRESET_IDS,
34+
);
35+
});
36+
37+
it("returns null for unknown assets", () => {
38+
expect(microsoftGraphPresetIdsForSliceAsset("not-a-preset")).toBeNull();
6939
});
7040
});

0 commit comments

Comments
 (0)