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
31 changes: 31 additions & 0 deletions .changeset/delivery-payload-severity-closed-union.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"@objectstack/service-messaging": patch
---

fix(service-messaging): close `DeliveryPayload.severity` to `'info' | 'warning' | 'critical'` (#7174)

`DeliveryPayload.severity` was declared `'info' | 'warning' | 'critical' | string`.
TypeScript absorbs a literal union member into the wider primitive it is unioned
with, so this was exactly `string` — the three names read as a closed vocabulary
but enforced nothing. `severity: 'urgent'` (or `''`) type-checked with no error,
even though the value flows into `inbox-channel.ts`'s `n.severity ?? 'info'` and
the `sys_inbox_message.severity` select column, whose options are the three
names, and even though this package's three sibling declarations of the same
concept — `MessagingService['emit']`'s `EmitInput.severity`
(`messaging-service.ts`), `MessagingChannel`'s `Notification['severity']`
(`channel.ts`), and the `inbox-message` object's `severity` select field — are
already closed to exactly this set.

Dropping the trailing `| string` makes the type mean what it says. No runtime
behaviour changes — every real producer already writes `input.severity ?? 'info'`
where `input.severity` is itself the already-closed `EmitInput.severity`, so no
in-repo construction site changes shape. This is the type-level twin of #7086
(`NotifyConfigSchema.severity`, closed in PR #7192): a construction site that
would previously narrow silently through the collapsed union now gets a
compiler refusal instead, named as a `@ts-expect-error` pin.

This is a narrowing of a publicly exported type
(`packages/services/service-messaging/src/outbox.ts`), so a consumer assigning
an out-of-vocabulary literal directly to `DeliveryPayload.severity` would newly
fail to compile — hence `patch`, following the #7140 precedent for a type-side
enforcement tightening with no runtime behaviour change.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* #7174 — compile-time pin for `DeliveryPayload.severity`.
*
* The field used to be declared `'info' | 'warning' | 'critical' | string`,
* which TypeScript collapses to exactly `string` (a literal union member is
* absorbed by the wider primitive it is unioned with). The three names read
* as a closed vocabulary but enforced nothing — `severity: 'urgent'`
* type-checked with no error. This file is the guard-rail: every line below
* is a type-level assertion evaluated by `tsc --noEmit`, run as part of this
* package's ordinary `typecheck` script (this package's tsconfig does not
* exclude `*.test.ts`, so no separate `.pin.ts` is needed — see AGENTS.md's
* `PINS_CHECKED` invariant).
*/

import { describe, it, expect } from 'vitest';
import type { DeliveryPayload } from './outbox.js';

describe('DeliveryPayload.severity (#7174)', () => {
it('accepts the closed info | warning | critical vocabulary', () => {
const payloads: DeliveryPayload[] = [
{ severity: 'info' },
{ severity: 'warning' },
{ severity: 'critical' },
{}, // severity is optional
];
expect(payloads.map((p) => p.severity)).toEqual(['info', 'warning', 'critical', undefined]);
});

it('rejects an out-of-vocabulary literal at compile time', () => {
// @ts-expect-error 'urgent' is not a member of the closed severity vocabulary
const bad: DeliveryPayload = { severity: 'urgent' };
// Runtime side is unreachable in practice (a real construction site
// would fail to compile); assert only that the pin above is real —
// if the `| string` collapse ever comes back, this line stops being
// an error and `tsc --noEmit` goes red on the missing `@ts-expect-error`.
expect(bad.severity).toBe('urgent');
});
});
2 changes: 1 addition & 1 deletion packages/services/service-messaging/src/outbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type DeliveryStatus =
export interface DeliveryPayload {
title?: string;
body?: string;
severity?: 'info' | 'warning' | 'critical' | string;
severity?: 'info' | 'warning' | 'critical';
actionUrl?: string;
[k: string]: unknown;
}
Expand Down
Loading