Skip to content

Commit cc5948e

Browse files
committed
Merge origin/main into claude/issue-23938-reactions-changelog-amend
Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP Co-authored-by: Claude <noreply@anthropic.com>
2 parents 232bc2b + 68ffa5d commit cc5948e

13 files changed

Lines changed: 260 additions & 90 deletions

‎packages/qa/dogfood/test/authz-conformance.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1630,7 +1630,8 @@ describe('#9084 — the rest-server.ts transport tripwire keys off mechanics', (
16301630
// [#23050 slice 6] Length 6 → 7 (`security-routes.ts`), still 0 over it.
16311631
// [#23050 slice 7] Length 7 → 8 (`discovery-routes.ts`), still 0 over it.
16321632
// [#23050 slice 8] Length 8 → 9 (`email-routes.ts`), still 0 over it.
1633-
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(9);
1633+
// [#23050 slice 9] Length 9 → 10 (`search-routes.ts`), still 0 over it.
1634+
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(10);
16341635
expect(REST_SERVER_REGISTRAR_PATHS[0]).toBe(REST_SERVER);
16351636
const listed = REST_SERVER_REGISTRAR_PATHS.map((file) => readFileSync(join(REPO_ROOT, file), 'utf8'));
16361637
for (const probe of [MECHANICS, ROUTES]) {

‎packages/qa/dogfood/test/authz-probe-blind-spot.census.ts‎

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -520,11 +520,12 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
520520
'terminator spelling), PLUS `registerPerItemRoute(` call sites; counted over this file AND every ' +
521521
'module on the registrar list (`packages/rest/src/rest-server-registrar-files.ts`) — ' +
522522
'`data-action-routes.ts`, `batch-routes.ts`, `approvals-routes.ts`, `sharing-routes.ts`, ' +
523-
'`analytics-routes.ts`, `security-routes.ts`, `discovery-routes.ts` and `email-routes.ts`, the modules ' +
524-
'the bodies of `registerDataActionEndpoints`, `registerBatchEndpoints`, `registerApprovalsEndpoints`, ' +
525-
'`registerSharingEndpoints`, `registerSharingRuleEndpoints`, `registerAnalyticsEndpoints`, ' +
526-
'`registerSecurityExplainEndpoints`, `registerSecurityEndpoints`, `registerDiscoveryEndpoints`, ' +
527-
'`registerOpenApiEndpoints` and `registerEmailEndpoints` moved to; ' +
523+
'`analytics-routes.ts`, `security-routes.ts`, `discovery-routes.ts`, `email-routes.ts` and ' +
524+
'`search-routes.ts`, the modules the bodies of `registerDataActionEndpoints`, `registerBatchEndpoints`, ' +
525+
'`registerApprovalsEndpoints`, `registerSharingEndpoints`, `registerSharingRuleEndpoints`, ' +
526+
'`registerAnalyticsEndpoints`, `registerSecurityExplainEndpoints`, `registerSecurityEndpoints`, ' +
527+
'`registerDiscoveryEndpoints`, `registerOpenApiEndpoints`, `registerEmailEndpoints` and ' +
528+
'`registerSearchEndpoints` moved to; ' +
528529
'reachable = those inside registerMetadataEndpoints',
529530
// [#15542 / #15854] ⭐ THE POPULATION RULE LEARNED A SECOND SPELLING, and
530531
// the numbers it produces did NOT move: 80 / 19 / 61, exactly as before.
@@ -745,10 +746,22 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
745746
// new occurrences are this slice's seam, the same two spellings once (the
746747
// host literal's forwarder, two; the host interface's member, two). No call
747748
// site was added.
749+
// [#23050 slice 9] 70 / 19 / 51 UNCHANGED, the same measurement again:
750+
// `registerSearchEndpoints`' body (one registration site, `GET
751+
// {base}/search`, outside `registerMetadataEndpoints`, so in the blind spot)
752+
// moved to `search-routes.ts`, which joins the list (length 9 -> 10) with
753+
// its own control. `this.routeManager.register(` stays 63 over the ten
754+
// files (21 here + 9 + 5 + 7 + 8 + 1 + 6 + 4 + 1 + 1),
755+
// `private register*Endpoints(` stays 16 (the registrar is still declared
756+
// here), and `enforceAuth` 100 -> 104 is RE-ANCHORED, not relaxed: the one
757+
// call site moved with its route and is counted in both readings; the four
758+
// new occurrences are this slice's seam, the same two spellings once (the
759+
// host literal's forwarder, two; the host interface's member, two). No call
760+
// site was added.
748761
controls: {
749762
'private register*Endpoints(': 16,
750763
'this.routeManager.register(': 63,
751-
'registrar list length': 9,
764+
'registrar list length': 10,
752765
'data-action-routes.ts: this.routeManager.register(': 9,
753766
'batch-routes.ts: this.routeManager.register(': 5,
754767
'approvals-routes.ts: this.routeManager.register(': 7,
@@ -757,14 +770,15 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
757770
'security-routes.ts: this.routeManager.register(': 6,
758771
'discovery-routes.ts: this.routeManager.register(': 4,
759772
'email-routes.ts: this.routeManager.register(': 1,
773+
'search-routes.ts: this.routeManager.register(': 1,
760774
// Both halves of the new rule carry their own control, so neither can go
761775
// silently to zero: a helper deleted and its routes inlined back would
762776
// still read population 80, and only these two controls would notice the
763777
// shape moved and force this provenance to be re-read.
764778
'registerPerItemRoute(': 8,
765779
'const registerPerItemRoute =': 1,
766780
'forwarder slice: this.routeManager.register(': 1,
767-
enforceAuth: 100,
781+
enforceAuth: 104,
768782
},
769783
note:
770784
'The single non-tripwire probe names ONE registrar of 16. The other 15 can never mint a key: ' +

‎packages/rest/src/execctx-consumer-census.test.ts‎

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ const SOURCE = readFileSync(resolve(HERE, 'rest-server.ts'), 'utf8');
106106
*
107107
* [#23050 slice 8] …and `registerEmailEndpoints`' body to `email-routes.ts`,
108108
* with its one.
109+
*
110+
* [#23050 slice 9] …and `registerSearchEndpoints`' body to `search-routes.ts`,
111+
* with its one.
109112
*/
110113
const SITE_FILES = REST_SERVER_REGISTRAR_FILES;
111114
const SOURCES = new Map<string, string>(SITE_FILES.map((file) => [
@@ -119,12 +122,13 @@ const siteKey = (file: string, line: number) => `${file}:${line}`;
119122
* `registerApprovalsEndpoints`, `registerSharingEndpoints`,
120123
* `registerSharingRuleEndpoints`, `registerAnalyticsEndpoints`,
121124
* `registerSecurityExplainEndpoints`, `registerSecurityEndpoints`,
122-
* `registerOpenApiEndpoints`, `registerEmailEndpoints`) hands its moved body
123-
* forwards each member to the server with `(...a) => this.<member>(...a)`, so
124-
* a moved site's stack shows that forwarder above the site itself. The site is
125-
* the first censused frame that is not one. Found by their spelling, and §2
126-
* pins ONE per moved REGISTRAR whose body reads the seam — one per moved
127-
* module, plus {@link EXTRA_MOVED_REGISTRARS}, less {@link MOVED_REGISTRARS_READING_NO_EXECCTX}
125+
* `registerOpenApiEndpoints`, `registerEmailEndpoints`,
126+
* `registerSearchEndpoints`) hands its moved body forwards each member to the
127+
* server with `(...a) => this.<member>(...a)`, so a moved site's stack shows
128+
* that forwarder above the site itself. The site is the first censused frame
129+
* that is not one. Found by their spelling, and §2 pins ONE per moved
130+
* REGISTRAR whose body reads the seam — one per moved module, plus
131+
* {@link EXTRA_MOVED_REGISTRARS}, less {@link MOVED_REGISTRARS_READING_NO_EXECCTX}
128132
* — so a respelt forwarder — or a module moved without joining `SITE_FILES` —
129133
* is a control failure there rather than every moved site silently attributed
130134
* to it.
@@ -666,9 +670,19 @@ describe('[#13160] §2 the consumer surface, counted from the tree', () => {
666670
// `rest-server.ts` goes 57 → 58 (one site out, one forwarder's two
667671
// mentions in). Ten forwarders now: (9 - 1) + 3 - 1, the module carrying
668672
// one registrar that reads the seam, so neither map gains an entry.
669-
expect(SITE_FILES).toHaveLength(9);
673+
//
674+
// [#23050 slice 9] 62 sites UNCHANGED / 117 → 121 mentions — the same MOVE.
675+
// `registerSearchEndpoints`' body left for `search-routes.ts` with its one
676+
// BARE site: 23 + 9 + 5 + 7 + 8 + 1 + 5 + 2 + 1 + 1 = 62 sites. The four
677+
// new mentions are this slice's seam, the two spellings once (its host
678+
// literal's forwarder, two; its host interface's member, two):
679+
// `rest-server.ts` goes 58 → 59 (one site out, one forwarder's two
680+
// mentions in). Eleven forwarders now: (10 - 1) + 3 - 1, the module
681+
// carrying one registrar that reads the seam, so neither map gains an
682+
// entry.
683+
expect(SITE_FILES).toHaveLength(10);
670684
expect(SITES.length).toBe(62);
671-
expect([...SOURCES.values()].reduce((n, text) => n + text.split('resolveExecCtx').length - 1, 0)).toBe(117);
685+
expect([...SOURCES.values()].reduce((n, text) => n + text.split('resolveExecCtx').length - 1, 0)).toBe(121);
672686
const extraMovedRegistrars = Object.values(EXTRA_MOVED_REGISTRARS).reduce((n, k) => n + (k ?? 0), 0);
673687
expect(extraMovedRegistrars).toBe(3);
674688
const registrarsReadingNoExecCtx = Object.values(MOVED_REGISTRARS_READING_NO_EXECCTX).reduce((n, k) => n + (k ?? 0), 0);

‎packages/rest/src/meta-draft-read-door-census.test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,10 @@ describe('[#20338] census: every draft switch in rest-server.ts is ledgered', ()
251251
// length 6 → 7. [#23050 slice 7] Nor do the discovery and OpenAPI bodies
252252
// (`discovery-routes.ts`): 8 = 6 + 0 + 0 + 0 + 0 + 2 + 0 + 0, length 7 → 8.
253253
// [#23050 slice 8] Nor does the email send body (`email-routes.ts`):
254-
// 8 = 6 + 0 + 0 + 0 + 0 + 2 + 0 + 0 + 0, length 8 → 9.
255-
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(9);
254+
// 8 = 6 + 0 + 0 + 0 + 0 + 2 + 0 + 0 + 0, length 8 → 9. [#23050 slice 9]
255+
// Nor does the global search body (`search-routes.ts`):
256+
// 8 = 6 + 0 + 0 + 0 + 0 + 2 + 0 + 0 + 0 + 0, length 9 → 10.
257+
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(10);
256258
expect(SOURCE_FILES.map((file) => file.fileName)).toEqual([...REST_SERVER_REGISTRAR_FILES]);
257259
expect(SITES).toHaveLength(8);
258260
});

‎packages/rest/src/rest-server-canonical-query-ast.test.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ interface CensusEntry {
149149
* [#23050 slice 8] 4 = 1 + 3 + 0 + 0 + 0 + 0 + 0 + 0 + 0: the email send body
150150
* (`email-routes.ts`) builds no `query:` literal either — it hands the email
151151
* service the caller's own JSON body, never a `query:` slot.
152+
*
153+
* [#23050 slice 9] 4 = 1 + 3 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0: the global search
154+
* body (`search-routes.ts`) builds no `query:` literal either — it hands
155+
* `searchAll` a `q` / `objects` / `limit` / `perObject` request read off
156+
* `req.query`, never a `query:` slot.
152157
*/
153158
const REGISTRAR_QUERY_SLOT_FLOORS: Readonly<Record<RestServerRegistrarFile, number>> = {
154159
'rest-server.ts': 1,
@@ -160,6 +165,7 @@ const REGISTRAR_QUERY_SLOT_FLOORS: Readonly<Record<RestServerRegistrarFile, numb
160165
'security-routes.ts': 0,
161166
'discovery-routes.ts': 0,
162167
'email-routes.ts': 0,
168+
'search-routes.ts': 0,
163169
};
164170

165171
const CENSUS: CensusEntry[] = [
@@ -352,7 +358,8 @@ describe('[#16638] §1 CONTROLS on the census instrument itself', () => {
352358
// [#23050 slice 6] Length 6 → 7, the sum unchanged.
353359
// [#23050 slice 7] Length 7 → 8, the sum unchanged.
354360
// [#23050 slice 8] Length 8 → 9, the sum unchanged.
355-
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(9);
361+
// [#23050 slice 9] Length 9 → 10, the sum unchanged.
362+
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(10);
356363
expect(Object.keys(REGISTRAR_QUERY_SLOT_FLOORS).sort()).toEqual([...REST_SERVER_REGISTRAR_FILES].sort());
357364
expect(CENSUS.filter(({ file }) => REGISTRAR_SOURCES.has(file)).map(({ file }) => file))
358365
.toEqual([...REST_SERVER_REGISTRAR_FILES]);

‎packages/rest/src/rest-server-docblock-position.test.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@ describe('rest-server.ts docblock position', () => {
170170
* analytics modules do: the module header, the host interface's block and the
171171
* function's. The email send body held no TSDoc block of its own, so
172172
* `rest-server.ts` keeps every block it had.
173+
*
174+
* [#23050 slice 9] `search-routes.ts` carries three, as the email module does:
175+
* the module header, the host interface's block and the function's. The global
176+
* search body held no TSDoc block of its own, so `rest-server.ts` keeps every
177+
* block it had (164).
173178
*/
174179
const MOVED_FILE_FLOORS: Readonly<Record<Exclude<RestServerRegistrarFile, typeof FILE>, number>> = {
175180
'data-action-routes.ts': 4,
@@ -180,6 +185,7 @@ const MOVED_FILE_FLOORS: Readonly<Record<Exclude<RestServerRegistrarFile, typeof
180185
'security-routes.ts': 8,
181186
'discovery-routes.ts': 5,
182187
'email-routes.ts': 3,
188+
'search-routes.ts': 3,
183189
};
184190
const MOVED_FILES: ReadonlyArray<{ file: string; minBlocks: number }> = REST_SERVER_REGISTRAR_FILES
185191
.filter((file): file is Exclude<RestServerRegistrarFile, typeof FILE> => file !== FILE)
@@ -212,9 +218,10 @@ describe('[#23050 slice 3] the registrar list this pin reads', () => {
212218
// [#23050 slice 6] 4 + 3 + 3 + 8 + 3 + 8 = 29 over six, length 6 → 7.
213219
// [#23050 slice 7] 4 + 3 + 3 + 8 + 3 + 8 + 5 = 34 over seven, length 7 → 8.
214220
// [#23050 slice 8] 4 + 3 + 3 + 8 + 3 + 8 + 5 + 3 = 37 over eight, length 8 → 9.
215-
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(9);
221+
// [#23050 slice 9] 4 + 3 + 3 + 8 + 3 + 8 + 5 + 3 + 3 = 40 over nine, length 9 → 10.
222+
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(10);
216223
expect(Object.keys(MOVED_FILE_FLOORS).sort())
217224
.toEqual(REST_SERVER_REGISTRAR_FILES.filter((file) => file !== FILE).sort());
218-
expect(MOVED_FILES.reduce((n, { minBlocks }) => n + minBlocks, 0)).toBe(37);
225+
expect(MOVED_FILES.reduce((n, { minBlocks }) => n + minBlocks, 0)).toBe(40);
219226
});
220227
});

‎packages/rest/src/rest-server-query-number-census.test.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,11 @@ describe('#20139 census — every numeric coercion in rest-server.ts is classifi
326326
// the discovery and OpenAPI bodies (`discovery-routes.ts`):
327327
// 9 = 8 + 1 + 0 + 0 + 0 + 0 + 0 + 0, length 7 → 8. [#23050 slice 8] Nor
328328
// does the email send body (`email-routes.ts`):
329-
// 9 = 8 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0, length 8 → 9.
330-
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(9);
329+
// 9 = 8 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0, length 8 → 9. [#23050 slice 9]
330+
// Nor does the global search body (`search-routes.ts`): it READS its two
331+
// caps through `readDeclaredQueryNumber`, never coerces one:
332+
// 9 = 8 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0, length 9 → 10.
333+
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(10);
331334
expect(SOURCES.map(([file]) => file)).toEqual([...REST_SERVER_REGISTRAR_FILES]);
332335
expect(sites).toHaveLength(9);
333336
});

‎packages/rest/src/rest-server-registrar-files.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export const REST_SERVER_REGISTRAR_FILES = [
4545
'discovery-routes.ts',
4646
// [#23050 slice 8] `registerEmailEndpoints`' body.
4747
'email-routes.ts',
48+
// [#23050 slice 9] `registerSearchEndpoints`' body.
49+
'search-routes.ts',
4850
] as const;
4951

5052
/** One file on {@link REST_SERVER_REGISTRAR_FILES}. */

‎packages/rest/src/rest-server.ts‎

Lines changed: 13 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,8 @@ import {
341341
} from './discovery-routes.js';
342342
// [#23050 slice 8] The email route's body moved to its own module — see its header.
343343
import { registerEmailEndpoints, type EmailRouteHost } from './email-routes.js';
344+
// [#23050 slice 9] The global search route's body moved to its own module — see its header.
345+
import { registerSearchEndpoints, type SearchRouteHost } from './search-routes.js';
344346
import { loadExcelJs, type Worksheet } from './xlsx-module.js';
345347
// [#18386] `?template=true` on the export door: the import template's column
346348
// rule, request reading and workbook. See the module header.
@@ -8995,71 +8997,17 @@ export class RestServer {
89958997
* GET {basePath}/search?q=acme&objects=lead,account&limit=20&perObject=5
89968998
*/
89978999
private registerSearchEndpoints(basePath: string): void {
8998-
const isScoped = basePath.includes('/environments/:environmentId');
8999-
this.routeManager.register({
9000-
method: 'GET',
9001-
path: `${basePath}/search`,
9002-
handler: async (req: any, res: any) => {
9003-
try {
9004-
const environmentId = isScoped ? req.params?.environmentId : undefined;
9005-
const p = await this.resolveProtocol(environmentId, req);
9006-
const context = await this.resolveExecCtx(environmentId, req);
9007-
if (this.enforceAuth(req, res, context)) return;
9008-
const searchAll = (p as any).searchAll;
9009-
if (typeof searchAll !== 'function') {
9010-
res.status(501).json({ code: 'NOT_IMPLEMENTED', message: 'Search not supported by this protocol' });
9011-
return;
9012-
}
9013-
// [#6877] `String(['a','b'])` searched for the literal
9014-
// term `'a,b'`. `objects` is NOT listed: the next line reads
9015-
// its array arm deliberately — a cross-object search over a
9016-
// LIST of objects is the whole point of the parameter.
9017-
// [#7606] Recognition before arity, same order and same
9018-
// envelope as the export route — see `query-allowlist.ts`.
9019-
// A dropped `?objects=` is the widening case at its worst:
9020-
// the term fans out across every searchable object while the
9021-
// caller believes they scoped it to one.
9022-
if (refuseUnknownQueryParams(req, res, GLOBAL_SEARCH_PARAMS)) return;
9023-
if (refuseRepeatedQueryParams(req, res, ['q', 'query', 'limit', 'perObject'])) return;
9024-
const q = String(req.query?.q ?? req.query?.query ?? '');
9025-
const objectsParam = req.query?.objects;
9026-
const objects = typeof objectsParam === 'string'
9027-
? objectsParam.split(',').map((s: string) => s.trim()).filter(Boolean)
9028-
: Array.isArray(objectsParam) ? objectsParam : undefined;
9029-
// [#20062] Read, not coerced: `?limit=abc` used to reach
9030-
// `searchAll` as `NaN`, and `hits.length >= NaN` is never true,
9031-
// so the overall cap was silently gone. No request schema is
9032-
// declared for this door, so the reading is "a whole number";
9033-
// `searchAll`'s own `[1, 100]` clamp is unchanged, and an empty
9034-
// `?limit=` stays absent as the old falsy guard had it.
9035-
const limit = readDeclaredQueryNumber(req.query, 'limit',
9036-
UNDECLARED_WHOLE_NUMBER_PARAM, { emptyIsAbsent: true });
9037-
// [#20139] `perObject`, the same way: `?perObject=abc` reached
9038-
// `searchAll` as `NaN`, its `Math.max(1, Math.min(25, NaN))` is
9039-
// `NaN`, and the per-object cap was silently gone. Whole number;
9040-
// the `[1, 25]` clamp stays `searchAll`'s own, and an empty
9041-
// `?perObject=` stays absent as the old falsy guard had it.
9042-
const perObject = readDeclaredQueryNumber(req.query, 'perObject',
9043-
UNDECLARED_WHOLE_NUMBER_PARAM, { emptyIsAbsent: true });
9044-
const result = await searchAll.call(p, {
9045-
q,
9046-
objects,
9047-
limit,
9048-
perObject,
9049-
...(context ? { context } : {}),
9050-
});
9051-
res.json(result);
9052-
} catch (error: any) {
9053-
const mapped = mapDataError(error);
9054-
logUnexpectedRouteError(error, mapped);
9055-
res.status(mapped.status).json(mapped.body);
9056-
}
9057-
},
9058-
metadata: {
9059-
summary: 'Global cross-object search',
9060-
tags: ['search'],
9061-
},
9062-
});
9000+
// [#23050 slice 9] The body moved to `search-routes.ts`. This literal
9001+
// hands it every member it reads — the list IS its dependency on this
9002+
// class (see `SearchRouteHost`, and that module's header for why no
9003+
// member is a getter).
9004+
const host: SearchRouteHost = {
9005+
routeManager: this.routeManager,
9006+
resolveProtocol: (...a) => this.resolveProtocol(...a),
9007+
resolveExecCtx: (...a) => this.resolveExecCtx(...a),
9008+
enforceAuth: (...a) => this.enforceAuth(...a),
9009+
};
9010+
registerSearchEndpoints.call(host, basePath);
90639011
}
90649012

90659013
/**

0 commit comments

Comments
 (0)