Microsoft Teams accepts the new Power Automate webhook URL format
This commit is contained in:
3 files changed
+86
-7
No files matched your search
@@ -1,14 +1,12 @@
|
||||
/** Microsoft Power Automate Workflows (e.g. Microsoft Teams). workflows://. */
|
||||
import { parseWorkflowsUrl, buildWorkflowsHttpUrl } from '$lib/utils/notification-parsers';
|
||||
import { resolveWorkflowsHttpUrl } from '$lib/utils/notification-parsers';
|
||||
import { notificationFetch, drainResponse, type NotificationPayload, type NotificationResult } from './shared';
|
||||
|
||||
export async function sendWorkflows(appriseUrl: string, payload: NotificationPayload): Promise<NotificationResult> {
|
||||
const parsed = parseWorkflowsUrl(appriseUrl);
|
||||
if (!parsed) {
|
||||
return { success: false, error: 'Invalid Workflows URL format. Expected: workflows://hostname/workflow/signature' };
|
||||
const url = resolveWorkflowsHttpUrl(appriseUrl);
|
||||
if (!url) {
|
||||
return { success: false, error: 'Invalid Workflows URL format. Expected the full https:// webhook URL with the scheme changed to workflows://, or workflows://hostname/workflow/signature' };
|
||||
}
|
||||
|
||||
const url = buildWorkflowsHttpUrl(parsed.hostname, parsed.workflow, parsed.signature);
|
||||
const titleWithEnv = payload.environmentName ? `${payload.title} [${payload.environmentName}]` : payload.title;
|
||||
|
||||
try {
|
||||
|
||||
@@ -97,3 +97,40 @@ export function parseWorkflowsUrl(appriseUrl: string): { hostname: string; workf
|
||||
export function buildWorkflowsHttpUrl(hostname: string, workflow: string, signature: string): string {
|
||||
return `https://${hostname}/powerautomate/automations/direct/workflows/${workflow}/triggers/manual/paths/invoke?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=${signature}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a `workflows://` channel URL to the HTTPS endpoint to POST to.
|
||||
*
|
||||
* Two input shapes are accepted:
|
||||
* 1. A COMPLETE webhook URL with the schema swapped to `workflows://` - i.e. it
|
||||
* already carries the full `/.../triggers/manual/paths/invoke?...&sig=...` path
|
||||
* and query. Microsoft has changed this path over time (legacy logic.azure.com,
|
||||
* then `/powerautomate/automations/direct/`, then a `/cu/<n>/` segment on
|
||||
* *.powerplatform.com - #1512). We must NOT rebuild it from parts, or new
|
||||
* segments and query params (api-version, cu/<n>) get dropped -> 401. So when the
|
||||
* input already has the invoke path + a sig, pass the path and query through
|
||||
* verbatim, only swapping the scheme to https.
|
||||
* 2. The SHORT legacy form `workflows://host/<workflow>/<signature>` (what the UI
|
||||
* hint documents). No invoke path, so reconstruct the endpoint as before.
|
||||
*
|
||||
* Returns the https URL, or null when the input is neither shape.
|
||||
*/
|
||||
export function resolveWorkflowsHttpUrl(appriseUrl: string): string | null {
|
||||
const schemeMatch = appriseUrl.match(/^workflows?:\/\/(.*)$/is);
|
||||
if (!schemeMatch) return null;
|
||||
const rest = schemeMatch[1];
|
||||
|
||||
// Shape 1: a full webhook URL - it carries the invoke path. Pass it through
|
||||
// verbatim (only when it also has a sig=, else it's an unsigned/invalid webhook)
|
||||
// so any host/path/query shape Microsoft uses survives. An invoke path present but
|
||||
// no sig is invalid - do NOT fall through to short-form parsing, which would shred
|
||||
// the path into a bogus workflow/signature.
|
||||
if (/\/triggers\/manual\/paths\/invoke/i.test(rest)) {
|
||||
return /[?&]sig=[^&]/i.test(rest) ? `https://${rest}` : null;
|
||||
}
|
||||
|
||||
// Shape 2: the short legacy form host/<workflow>/<signature>.
|
||||
const parsed = parseWorkflowsUrl(appriseUrl);
|
||||
if (!parsed) return null;
|
||||
return buildWorkflowsHttpUrl(parsed.hostname, parsed.workflow, parsed.signature);
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { parseWorkflowsUrl, buildWorkflowsHttpUrl, parseTelegramUrl } from '../src/lib/utils/notification-parsers';
|
||||
import { parseWorkflowsUrl, buildWorkflowsHttpUrl, resolveWorkflowsHttpUrl, parseTelegramUrl } from '../src/lib/utils/notification-parsers';
|
||||
|
||||
// Telegram request body construction (mirrors notifications.ts)
|
||||
function buildTelegramBody(chatId: string, text: string, topicId?: number) {
|
||||
@@ -84,6 +84,50 @@ describe('Workflows HTTP URL Construction', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveWorkflowsHttpUrl (full-URL passthrough, #1512)', () => {
|
||||
test('new Power Automate URL with /cu/<n>/ passes through verbatim (only scheme swapped)', () => {
|
||||
// The exact shape from #1512: powerplatform.com host, a /cu/11/ segment,
|
||||
// api-version=1. Rebuilding from parts drops those -> 401. Passthrough keeps them.
|
||||
const full =
|
||||
'workflows://myenv.11.environment.api.powerplatform.com:443/powerautomate/automations/direct/cu/11/workflows/abc123/triggers/manual/paths/invoke?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=THE_SIGNATURE';
|
||||
const url = resolveWorkflowsHttpUrl(full);
|
||||
expect(url).toBe(
|
||||
'https://myenv.11.environment.api.powerplatform.com:443/powerautomate/automations/direct/cu/11/workflows/abc123/triggers/manual/paths/invoke?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=THE_SIGNATURE'
|
||||
);
|
||||
});
|
||||
|
||||
test('legacy logic.azure.com full URL passes through verbatim', () => {
|
||||
const full =
|
||||
'workflows://prod-161.westeurope.logic.azure.com:443/workflows/643e/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=KODu';
|
||||
const url = resolveWorkflowsHttpUrl(full)!;
|
||||
expect(url).toBe('https://' + full.slice('workflows://'.length));
|
||||
expect(url).toContain('api-version=2016-06-01');
|
||||
expect(url).toContain('sig=KODu');
|
||||
});
|
||||
|
||||
test('short legacy form host/workflow/sig is still reconstructed (back-compat)', () => {
|
||||
const url = resolveWorkflowsHttpUrl('workflows://prod.logic.azure.com/abc123/sig456');
|
||||
expect(url).toBe(
|
||||
'https://prod.logic.azure.com/powerautomate/automations/direct/workflows/abc123/triggers/manual/paths/invoke?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=sig456'
|
||||
);
|
||||
});
|
||||
|
||||
test('a full invoke path WITHOUT a sig is not treated as passthrough (falls back / rejects)', () => {
|
||||
// No sig= -> not a signed webhook; short-form parse also fails -> null.
|
||||
expect(resolveWorkflowsHttpUrl('workflows://host/workflows/x/triggers/manual/paths/invoke')).toBeNull();
|
||||
});
|
||||
|
||||
test('non-workflows scheme returns null', () => {
|
||||
expect(resolveWorkflowsHttpUrl('https://host/x/y')).toBeNull();
|
||||
expect(resolveWorkflowsHttpUrl('')).toBeNull();
|
||||
});
|
||||
|
||||
test('workflow:// (singular) full URL also passes through', () => {
|
||||
const url = resolveWorkflowsHttpUrl('workflow://host:443/workflows/w/triggers/manual/paths/invoke?sig=s')!;
|
||||
expect(url).toBe('https://host:443/workflows/w/triggers/manual/paths/invoke?sig=s');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workflows Adaptive Card Payload', () => {
|
||||
test('constructs valid Adaptive Card body', () => {
|
||||
const title = 'Container Started';
|
||||
|
||||
Reference in new issue
Block a user