@topolo/sdk 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -255,4 +255,290 @@ describe('P2PModule.releaseHold', () => {
255
255
  expect(sent.resolution).toBe('seller');
256
256
  expect(sent.reason).toBe('arbitration');
257
257
  });
258
+
259
+ it('rejects an empty holdId', async () => {
260
+ const { impl } = stubFetch([]);
261
+ const p2p = new P2PModule(clientWith(impl));
262
+ await expect(p2p.releaseHold('', 'buyer')).rejects.toThrow('holdId is required');
263
+ });
264
+
265
+ it('defaults reason to null when omitted', async () => {
266
+ const { impl, calls } = stubFetch([
267
+ { body: { success: true, data: { hold: { hold_id: 'h2' }, resolution: 'buyer', cancel_entries: [] } } },
268
+ ]);
269
+ const p2p = new P2PModule(clientWith(impl));
270
+ await p2p.releaseHold('h2', 'buyer');
271
+ const sent = JSON.parse(String(requestCalls(calls)[0]!.init.body));
272
+ expect(sent.reason).toBeNull();
273
+ });
274
+ });
275
+
276
+ describe('P2PModule.getCapability (additional)', () => {
277
+ it('rejects an empty capabilityId', async () => {
278
+ const { impl } = stubFetch([]);
279
+ const p2p = new P2PModule(clientWith(impl));
280
+ await expect(p2p.getCapability('')).rejects.toThrow('capabilityId is required');
281
+ });
282
+
283
+ it('returns the unwrapped capability on success', async () => {
284
+ const { impl, calls } = stubFetch([
285
+ { body: { success: true, data: { capability: { capability_id: 'cap_ok' } } } },
286
+ ]);
287
+ const p2p = new P2PModule(clientWith(impl));
288
+ const cap = await p2p.getCapability('cap ok/1');
289
+ expect(cap!.capability_id).toBe('cap_ok');
290
+ expect(requestCalls(calls)[0]!.url).toContain('/api/directory/capabilities/cap%20ok%2F1');
291
+ });
292
+
293
+ it('rethrows non-404 errors', async () => {
294
+ const { impl } = stubFetch([{ status: 500, body: { success: false, error: 'boom' } }]);
295
+ const p2p = new P2PModule(clientWith(impl));
296
+ await expect(p2p.getCapability('cap_x')).rejects.toThrow();
297
+ });
298
+ });
299
+
300
+ describe('P2PModule.listActions', () => {
301
+ it('GETs /api/actions forwarding state + limit', async () => {
302
+ const { impl, calls } = stubFetch([
303
+ { body: { success: true, data: { actions: [{ request_id: 'req_1' }] } } },
304
+ ]);
305
+ const p2p = new P2PModule(clientWith(impl));
306
+ const actions = await p2p.listActions({ state: 'pending', limit: 5 });
307
+ expect(actions).toHaveLength(1);
308
+ const url = new URL(requestCalls(calls)[0]!.url);
309
+ expect(url.pathname).toBe('/api/actions');
310
+ expect(url.searchParams.get('state')).toBe('pending');
311
+ expect(url.searchParams.get('limit')).toBe('5');
312
+ });
313
+ });
314
+
315
+ describe('P2PModule.getAction', () => {
316
+ it('rejects an empty requestId', async () => {
317
+ const { impl } = stubFetch([]);
318
+ const p2p = new P2PModule(clientWith(impl));
319
+ await expect(p2p.getAction('')).rejects.toThrow('requestId is required');
320
+ });
321
+
322
+ it('returns the action detail on success', async () => {
323
+ const { impl } = stubFetch([
324
+ { body: { success: true, data: { request: { request_id: 'req_2' } } } },
325
+ ]);
326
+ const p2p = new P2PModule(clientWith(impl));
327
+ const detail = await p2p.getAction('req_2');
328
+ expect(detail).toMatchObject({ request: { request_id: 'req_2' } });
329
+ });
330
+
331
+ it('returns null on 404', async () => {
332
+ const { impl } = stubFetch([{ status: 404, body: { success: false, error: 'not_found' } }]);
333
+ const p2p = new P2PModule(clientWith(impl));
334
+ expect(await p2p.getAction('req_missing')).toBeNull();
335
+ });
336
+ });
337
+
338
+ describe('P2PModule.requestAction (validation + happy path)', () => {
339
+ it('rejects a missing capability_id', async () => {
340
+ const { impl } = stubFetch([]);
341
+ const p2p = new P2PModule(clientWith(impl));
342
+ await expect(
343
+ p2p.requestAction({ capability_id: '', idempotency_key: 'k', inputs: {} }),
344
+ ).rejects.toThrow('capability_id is required');
345
+ });
346
+
347
+ it('POSTs the request and defaults inputs to {}', async () => {
348
+ const { impl, calls } = stubFetch([
349
+ { body: { success: true, data: { request_id: 'req_new', status: 'requested' } } },
350
+ ]);
351
+ const p2p = new P2PModule(clientWith(impl));
352
+ const result = await p2p.requestAction({ capability_id: 'cap_1', idempotency_key: 'idem_1' });
353
+ expect(result).toMatchObject({ request_id: 'req_new' });
354
+ const call = requestCalls(calls)[0]!;
355
+ expect(new URL(call.url).pathname).toBe('/api/actions/request');
356
+ const sent = JSON.parse(String(call.init.body));
357
+ expect(sent.idempotency_key).toBe('idem_1');
358
+ expect(sent.inputs).toEqual({});
359
+ });
360
+ });
361
+
362
+ describe('P2PModule.respondAction / completeAction / disputeAction', () => {
363
+ it('respondAction POSTs response + extras and unwraps request', async () => {
364
+ const { impl, calls } = stubFetch([
365
+ { body: { success: true, data: { request: { request_id: 'req_3', state: 'accepted' } } } },
366
+ ]);
367
+ const p2p = new P2PModule(clientWith(impl));
368
+ const row = await p2p.respondAction('req_3', 'accepted', { quote_minor: 100 });
369
+ expect(row).toMatchObject({ request_id: 'req_3', state: 'accepted' });
370
+ const call = requestCalls(calls)[0]!;
371
+ expect(new URL(call.url).pathname).toBe('/api/actions/req_3/respond');
372
+ const sent = JSON.parse(String(call.init.body));
373
+ expect(sent.response).toBe('accepted');
374
+ expect(sent.quote_minor).toBe(100);
375
+ });
376
+
377
+ it('respondAction rejects an empty requestId', async () => {
378
+ const { impl } = stubFetch([]);
379
+ const p2p = new P2PModule(clientWith(impl));
380
+ await expect(p2p.respondAction('', 'rejected')).rejects.toThrow('requestId is required');
381
+ });
382
+
383
+ it('completeAction POSTs the result payload', async () => {
384
+ const { impl, calls } = stubFetch([
385
+ { body: { success: true, data: { request: { request_id: 'req_4' } } } },
386
+ ]);
387
+ const p2p = new P2PModule(clientWith(impl));
388
+ await p2p.completeAction('req_4', { output: 'done' });
389
+ const call = requestCalls(calls)[0]!;
390
+ expect(new URL(call.url).pathname).toBe('/api/actions/req_4/complete');
391
+ expect(JSON.parse(String(call.init.body)).result).toEqual({ output: 'done' });
392
+ });
393
+
394
+ it('completeAction rejects an empty requestId', async () => {
395
+ const { impl } = stubFetch([]);
396
+ const p2p = new P2PModule(clientWith(impl));
397
+ await expect(p2p.completeAction('')).rejects.toThrow('requestId is required');
398
+ });
399
+
400
+ it('disputeAction POSTs the reason', async () => {
401
+ const { impl, calls } = stubFetch([
402
+ { body: { success: true, data: { request: { request_id: 'req_5' } } } },
403
+ ]);
404
+ const p2p = new P2PModule(clientWith(impl));
405
+ await p2p.disputeAction('req_5', 'wrong output');
406
+ const call = requestCalls(calls)[0]!;
407
+ expect(new URL(call.url).pathname).toBe('/api/actions/req_5/dispute');
408
+ expect(JSON.parse(String(call.init.body)).reason).toBe('wrong output');
409
+ });
410
+
411
+ it('disputeAction rejects an empty requestId', async () => {
412
+ const { impl } = stubFetch([]);
413
+ const p2p = new P2PModule(clientWith(impl));
414
+ await expect(p2p.disputeAction('', 'reason')).rejects.toThrow('requestId is required');
415
+ });
416
+ });
417
+
418
+ describe('P2PModule.getPolicy / updatePolicy', () => {
419
+ it('getPolicy GETs /api/policies and unwraps the policy', async () => {
420
+ const { impl, calls } = stubFetch([
421
+ { body: { success: true, data: { policy: { organization_id: 'org_a', auto_accept: false } } } },
422
+ ]);
423
+ const p2p = new P2PModule(clientWith(impl));
424
+ const policy = await p2p.getPolicy();
425
+ expect(policy).toMatchObject({ organization_id: 'org_a' });
426
+ expect(new URL(requestCalls(calls)[0]!.url).pathname).toBe('/api/policies');
427
+ });
428
+
429
+ it('updatePolicy PUTs the partial policy', async () => {
430
+ const { impl, calls } = stubFetch([
431
+ { body: { success: true, data: { policy: { organization_id: 'org_a', auto_accept: true } } } },
432
+ ]);
433
+ const p2p = new P2PModule(clientWith(impl));
434
+ const policy = await p2p.updatePolicy({ auto_accept: true } as never);
435
+ expect(policy).toMatchObject({ auto_accept: true });
436
+ const call = requestCalls(calls)[0]!;
437
+ expect((call.init.method ?? '').toUpperCase()).toBe('PUT');
438
+ expect(JSON.parse(String(call.init.body)).auto_accept).toBe(true);
439
+ });
440
+ });
441
+
442
+ describe('P2PModule connections', () => {
443
+ it('listConnections GETs /api/connections', async () => {
444
+ const { impl, calls } = stubFetch([
445
+ { body: { success: true, data: { connections: [{ connection_id: 'conn_1' }] } } },
446
+ ]);
447
+ const p2p = new P2PModule(clientWith(impl));
448
+ const conns = await p2p.listConnections();
449
+ expect(conns).toHaveLength(1);
450
+ expect(new URL(requestCalls(calls)[0]!.url).pathname).toBe('/api/connections');
451
+ });
452
+
453
+ it('createConnection POSTs counterparty + default active status', async () => {
454
+ const { impl, calls } = stubFetch([
455
+ { body: { success: true, data: { connection: { connection_id: 'conn_2', status: 'active' } } } },
456
+ ]);
457
+ const p2p = new P2PModule(clientWith(impl));
458
+ const conn = await p2p.createConnection('org_other');
459
+ expect(conn.status).toBe('active');
460
+ const sent = JSON.parse(String(requestCalls(calls)[0]!.init.body));
461
+ expect(sent.counterparty_org_id).toBe('org_other');
462
+ expect(sent.status).toBe('active');
463
+ });
464
+
465
+ it('createConnection honors an explicit status', async () => {
466
+ const { impl, calls } = stubFetch([
467
+ { body: { success: true, data: { connection: { connection_id: 'conn_3', status: 'pending' } } } },
468
+ ]);
469
+ const p2p = new P2PModule(clientWith(impl));
470
+ await p2p.createConnection('org_other', { status: 'pending' });
471
+ expect(JSON.parse(String(requestCalls(calls)[0]!.init.body)).status).toBe('pending');
472
+ });
473
+
474
+ it('createConnection rejects an empty counterpartyOrgId', async () => {
475
+ const { impl } = stubFetch([]);
476
+ const p2p = new P2PModule(clientWith(impl));
477
+ await expect(p2p.createConnection('')).rejects.toThrow('counterpartyOrgId is required');
478
+ });
479
+
480
+ it('revokeConnection DELETEs the encoded connection id', async () => {
481
+ const { impl, calls } = stubFetch([{ body: { success: true, data: {} } }]);
482
+ const p2p = new P2PModule(clientWith(impl));
483
+ await p2p.revokeConnection('conn/9');
484
+ const call = requestCalls(calls)[0]!;
485
+ expect((call.init.method ?? '').toUpperCase()).toBe('DELETE');
486
+ expect(call.url).toContain('/api/connections/conn%2F9');
487
+ });
488
+
489
+ it('revokeConnection rejects an empty connectionId', async () => {
490
+ const { impl } = stubFetch([]);
491
+ const p2p = new P2PModule(clientWith(impl));
492
+ await expect(p2p.revokeConnection('')).rejects.toThrow('connectionId is required');
493
+ });
494
+ });
495
+
496
+ describe('P2PModule ledger + settlements', () => {
497
+ it('listLedger GETs /api/ledger and unwraps entries', async () => {
498
+ const { impl, calls } = stubFetch([
499
+ { body: { success: true, data: { ledger_entries: [{ entry_id: 'led_1' }] } } },
500
+ ]);
501
+ const p2p = new P2PModule(clientWith(impl));
502
+ const entries = await p2p.listLedger();
503
+ expect(entries).toHaveLength(1);
504
+ expect(new URL(requestCalls(calls)[0]!.url).pathname).toBe('/api/ledger');
505
+ });
506
+
507
+ it('previewSettlements POSTs filters and unwraps groups', async () => {
508
+ const { impl, calls } = stubFetch([
509
+ { body: { success: true, data: { groups: [{ buyer_org_id: 'org_b', seller_org_id: 'org_s' }] } } },
510
+ ]);
511
+ const p2p = new P2PModule(clientWith(impl));
512
+ const groups = await p2p.previewSettlements({ currency: 'USD' });
513
+ expect(groups).toHaveLength(1);
514
+ const call = requestCalls(calls)[0]!;
515
+ expect(new URL(call.url).pathname).toBe('/api/settlements/preview');
516
+ expect(JSON.parse(String(call.init.body)).currency).toBe('USD');
517
+ });
518
+
519
+ it('createSettlement defaults trigger to manual and unwraps batch + group', async () => {
520
+ const { impl, calls } = stubFetch([
521
+ {
522
+ body: {
523
+ success: true,
524
+ data: { batch: { batch_id: 'bat_1' }, group: { buyer_org_id: 'org_b' } },
525
+ },
526
+ },
527
+ ]);
528
+ const p2p = new P2PModule(clientWith(impl));
529
+ const result = await p2p.createSettlement({ buyer_org_id: 'org_b' });
530
+ expect(result.batch.batch_id).toBe('bat_1');
531
+ const sent = JSON.parse(String(requestCalls(calls)[0]!.init.body));
532
+ expect(sent.trigger).toBe('manual');
533
+ expect(sent.buyer_org_id).toBe('org_b');
534
+ });
535
+
536
+ it('createSettlement honors an explicit trigger override', async () => {
537
+ const { impl, calls } = stubFetch([
538
+ { body: { success: true, data: { batch: { batch_id: 'bat_2' }, group: {} } } },
539
+ ]);
540
+ const p2p = new P2PModule(clientWith(impl));
541
+ await p2p.createSettlement({ trigger: 'schedule' });
542
+ expect(JSON.parse(String(requestCalls(calls)[0]!.init.body)).trigger).toBe('schedule');
543
+ });
258
544
  });
package/src/oauth.test.ts CHANGED
@@ -109,5 +109,108 @@ describe('TopoloOAuth.requestDeviceCode', () => {
109
109
  });
110
110
  });
111
111
 
112
+ describe('TopoloOAuth.exchangeAuthorizationCode', () => {
113
+ it('POSTs grant_type=authorization_code with PKCE verifier + secret', async () => {
114
+ const { impl, calls } = stubFetch({
115
+ status: 200,
116
+ body: { access_token: 'at', token_type: 'Bearer', expires_in: 3600 },
117
+ });
118
+ const oauth = new TopoloOAuth({ fetch: impl });
119
+ const res = await oauth.exchangeAuthorizationCode({
120
+ clientId: 'doac_x',
121
+ code: 'auth_code_1',
122
+ redirectUri: 'https://app/cb',
123
+ codeVerifier: 'verifier_1',
124
+ clientSecret: 'secret_1',
125
+ });
126
+ expect(res.access_token).toBe('at');
127
+ const body = new URLSearchParams(calls[0]!.init.body as string);
128
+ expect(body.get('grant_type')).toBe('authorization_code');
129
+ expect(body.get('code')).toBe('auth_code_1');
130
+ expect(body.get('redirect_uri')).toBe('https://app/cb');
131
+ expect(body.get('code_verifier')).toBe('verifier_1');
132
+ expect(body.get('client_secret')).toBe('secret_1');
133
+ });
134
+
135
+ it('omits optional PKCE verifier + secret when not provided', async () => {
136
+ const { impl, calls } = stubFetch({
137
+ status: 200,
138
+ body: { access_token: 'at', token_type: 'Bearer', expires_in: 3600 },
139
+ });
140
+ const oauth = new TopoloOAuth({ fetch: impl });
141
+ await oauth.exchangeAuthorizationCode({
142
+ clientId: 'doac_x',
143
+ code: 'c',
144
+ redirectUri: 'https://app/cb',
145
+ });
146
+ const body = new URLSearchParams(calls[0]!.init.body as string);
147
+ expect(body.has('code_verifier')).toBe(false);
148
+ expect(body.has('client_secret')).toBe(false);
149
+ });
150
+ });
151
+
152
+ describe('TopoloOAuth.requestDeviceCode (single scope + omitted)', () => {
153
+ function deviceBody() {
154
+ return {
155
+ device_code: 'dc',
156
+ user_code: 'X',
157
+ verification_uri: 'https://ex/dev',
158
+ expires_in: 600,
159
+ interval: 5,
160
+ };
161
+ }
162
+
163
+ it('passes a string scope through unchanged', async () => {
164
+ const { impl, calls } = stubFetch({ status: 200, body: deviceBody() });
165
+ const oauth = new TopoloOAuth({ fetch: impl });
166
+ await oauth.requestDeviceCode({ clientId: 'doac_x', scope: 'read' });
167
+ expect(new URLSearchParams(calls[0]!.init.body as string).get('scope')).toBe('read');
168
+ });
169
+
170
+ it('omits scope when not provided', async () => {
171
+ const { impl, calls } = stubFetch({ status: 200, body: deviceBody() });
172
+ const oauth = new TopoloOAuth({ fetch: impl });
173
+ await oauth.requestDeviceCode({ clientId: 'doac_x' });
174
+ expect(new URLSearchParams(calls[0]!.init.body as string).has('scope')).toBe(false);
175
+ });
176
+ });
177
+
178
+ describe('TopoloOAuth #postForm content negotiation', () => {
179
+ it('parses a non-JSON (text/plain) successful body as a string', async () => {
180
+ const impl = vi.fn(async () =>
181
+ new Response('plain-ok', { status: 200, headers: { 'Content-Type': 'text/plain' } }),
182
+ ) as unknown as typeof fetch;
183
+ const oauth = new TopoloOAuth({ fetch: impl });
184
+ const res = await oauth.pollDeviceToken({ clientId: 'doac_x', deviceCode: 'dc_1' });
185
+ expect(res as unknown).toBe('plain-ok');
186
+ });
187
+
188
+ it('wraps a non-OAuth text error body in TopoloHttpError', async () => {
189
+ const impl = vi.fn(async () =>
190
+ new Response('upstream exploded', {
191
+ status: 502,
192
+ headers: { 'Content-Type': 'text/plain' },
193
+ }),
194
+ ) as unknown as typeof fetch;
195
+ const oauth = new TopoloOAuth({ fetch: impl });
196
+ await expect(
197
+ oauth.pollDeviceToken({ clientId: 'doac_x', deviceCode: 'dc_1' }),
198
+ ).rejects.toBeInstanceOf(TopoloHttpError);
199
+ });
200
+
201
+ it('normalizes a base URL that already ends with a slash', async () => {
202
+ const { impl, calls } = stubFetch({
203
+ status: 200,
204
+ body: { access_token: 'at', token_type: 'Bearer', expires_in: 1 },
205
+ });
206
+ const oauth = new TopoloOAuth({
207
+ fetch: impl,
208
+ serviceUrls: { auth: 'https://auth.example/' },
209
+ });
210
+ await oauth.pollDeviceToken({ clientId: 'doac_x', deviceCode: 'dc_1' });
211
+ expect(calls[0]!.url).toBe('https://auth.example/api/developer-oauth/token');
212
+ });
213
+ });
214
+
112
215
  // Keep the TopoloAuthError import non-dead for type inference
113
216
  void TopoloAuthError;
@@ -3,54 +3,53 @@
3
3
  // Regenerate: npm run generate:services
4
4
 
5
5
  export const SERVICES = {
6
- agent: "https://agent-api.topolo.app",
6
+ agent: "https://agent.topolo.app",
7
7
  auth: "https://auth.topolo.app",
8
8
  backup: "https://backup.topolo.app",
9
9
  books: "https://books.topolo.app",
10
10
  bugfix: "https://bugfix.topolo.app",
11
- bytes: "https://bytes-api.topolo.app",
11
+ bytes: "https://bytes.topolo.app",
12
12
  calendar: "https://calendar.topolo.app",
13
13
  capacity: "https://capacity.topolo.app",
14
14
  chat: "https://chat.topolo.app",
15
- commerce: "https://commerce-api.topolo.app",
15
+ commerce: "https://commerce.topolo.app",
16
16
  compose: "https://compose.topolo.app",
17
17
  consent: "https://consent.topolo.app",
18
- crm: "https://crm-api.topolo.app",
18
+ crm: "https://crm.topolo.app",
19
19
  developers: "https://developers.topolo.app",
20
20
  director: "https://director.topolo.app",
21
- feed: "https://feed-api.topolo.app",
21
+ feed: "https://feed.topolo.app",
22
22
  flow: "https://flow.topolo.app",
23
- forecast: "https://forecast-api.topolo.app",
23
+ forecast: "https://forecast.topolo.app",
24
24
  forms: "https://forms.topolo.app",
25
25
  home: "https://home.topolo.app",
26
26
  insights: "https://insights.topolo.app",
27
27
  inventory: "https://inventory.topolo.app",
28
- learn: "https://learn-api.topolo.app",
28
+ learn: "https://learn.topolo.app",
29
29
  localize: "https://localize.topolo.app",
30
- mail: "https://mail-api.topolo.app",
31
- mdm: "https://mdm-api.topolo.app",
30
+ mail: "https://mail.topolo.app",
31
+ mdm: "https://mdm.topolo.app",
32
32
  messages: "https://messages.topolo.app",
33
33
  nexus: "https://nexus-api.topolo.app",
34
34
  notify: "https://notify.topolo.app",
35
35
  observability_watch: "https://observability-watch.topolo.app",
36
- one: "https://one.topolo.app",
36
+ one: "https://www.topolo.app",
37
37
  p2p: "https://p2p.topolo.app",
38
38
  pay: "https://pay.topolo.app",
39
39
  people: "https://people.topolo.app",
40
- pulse: "https://pulse.topolo.app",
41
40
  "quro.api": "https://api.ol0.me",
42
41
  "quro.redirect": "https://ol0.me",
43
- roadmapper: "https://roadmapper-api.topolo.app",
42
+ roadmapper: "https://roadmapper.topolo.app",
44
43
  sign: "https://sign.topolo.app",
45
- social_studio: "https://studio-api.topolo.app",
46
- socialize: "https://socialize-api.topolo.app",
44
+ social_studio: "https://studio.topolo.app",
45
+ socialize: "https://socialize.topolo.app",
47
46
  spaces: "https://spaces.topolo.app",
48
47
  status: "https://status.topolo.app",
49
48
  success: "https://success.topolo.app",
50
49
  support: "https://support.topolo.app",
51
50
  survey: "https://survey.topolo.app",
52
51
  voice: "https://voice.topolo.app",
53
- web: "https://web-api.topolo.app",
52
+ web: "https://web.topolo.app",
54
53
  } as const;
55
54
 
56
55
  export type ServiceId = keyof typeof SERVICES;
@@ -48,7 +48,6 @@ const EXPECTED_APPLICATION_IDS = [
48
48
  'p2p',
49
49
  'pay',
50
50
  'people',
51
- 'pulse',
52
51
  'quro',
53
52
  'roadmapper',
54
53
  'sign',
@@ -69,10 +68,10 @@ describe('service registry', () => {
69
68
  calendar: 'https://calendar.topolo.app',
70
69
  books: 'https://books.topolo.app',
71
70
  consent: 'https://consent.topolo.app',
72
- feed: 'https://feed-api.topolo.app',
73
- mail: 'https://mail-api.topolo.app',
74
- one: 'https://one.topolo.app',
75
- web: 'https://web-api.topolo.app',
71
+ feed: 'https://feed.topolo.app',
72
+ mail: 'https://mail.topolo.app',
73
+ one: 'https://www.topolo.app',
74
+ web: 'https://web.topolo.app',
76
75
  });
77
76
  expect(DEFAULT_SERVICE_URLS).toMatchObject({
78
77
  success: 'https://success.topolo.app',
@@ -130,7 +129,7 @@ describe('service registry', () => {
130
129
  });
131
130
 
132
131
  describe('application catalog', () => {
133
- it('covers every current Topolo application directory', () => {
132
+ it('covers every production-routable Topolo application directory', () => {
134
133
  expect(Object.keys(APPLICATIONS).sort()).toEqual(EXPECTED_APPLICATION_IDS);
135
134
  });
136
135
 
@@ -149,9 +148,15 @@ describe('application catalog', () => {
149
148
 
150
149
  it('treats TopoloWeb runtime as a standalone deploy, not an agent-callable API service', () => {
151
150
  expect(APPLICATIONS.web.services).toEqual(['web']);
151
+ // Front-door standardization: the callable `web` service is served by the
152
+ // studio web worker (web.topolo.app/api); the API worker is de-routed and
153
+ // no longer carries a public service registration.
152
154
  expect(
153
- APPLICATIONS.web.deployTargets.find((target) => target.name === 'topolo-web-api')?.serviceId,
155
+ APPLICATIONS.web.deployTargets.find((target) => target.name === 'topolo-web-studio')?.serviceId,
154
156
  ).toBe('web');
157
+ expect(
158
+ APPLICATIONS.web.deployTargets.find((target) => target.name === 'topolo-web-api')?.serviceId,
159
+ ).toBeNull();
155
160
  // topolo-web-runtime (Astro, sites.topolo.app) deploys from its own wrangler
156
161
  // config and is intentionally listed as non-callable, so it can never be an
157
162
  // agent-callable service.
@@ -166,9 +171,15 @@ describe('application catalog', () => {
166
171
 
167
172
  it('treats TopoloFeed analytics as telemetry, not an agent-callable API service', () => {
168
173
  expect(APPLICATIONS.feed.services).toEqual(['feed']);
174
+ // Front-door standardization: the callable `feed` service is served by the
175
+ // feed web worker (feed.topolo.app/api); the API worker is de-routed.
169
176
  expect(
170
- APPLICATIONS.feed.deployTargets.find((target) => target.name === 'topolo-feed-api')?.serviceId,
177
+ APPLICATIONS.feed.deployTargets.find((target) => target.name === 'topolo-feed-web')?.serviceId,
171
178
  ).toBe('feed');
179
+ expect(
180
+ APPLICATIONS.feed.deployTargets.find((target) => target.name === 'topolo-feed-api')?.serviceId,
181
+ ).toBeNull();
182
+ // The analytics ingestion worker keeps its own host and is non-callable.
172
183
  expect(
173
184
  APPLICATIONS.feed.deployTargets.find((target) => target.name === 'topolo-feed-analytics-api')?.serviceId,
174
185
  ).toBeNull();